diff --git a/.cspell/frigate-dictionary.txt b/.cspell/frigate-dictionary.txt
index f5292b167d..23ad525668 100644
--- a/.cspell/frigate-dictionary.txt
+++ b/.cspell/frigate-dictionary.txt
@@ -162,6 +162,7 @@ mpegts
mqtt
mse
msenc
+muxing
namedtuples
nbytes
nchw
@@ -197,6 +198,8 @@ OWASP
paddleocr
paho
passwordless
+PCMA
+PCMU
popleft
posthog
postprocess
@@ -222,7 +225,9 @@ radeontop
rawvideo
rcond
RDONLY
+realmonitor
rebranded
+recvonly
referer
reindex
Reolink
@@ -239,8 +244,11 @@ rocminfo
rootfs
rtmp
RTSP
+rtsps
+rtspx
ruamel
scroller
+sendonly
setproctitle
setpts
shms
@@ -251,6 +259,7 @@ SNDMORE
socs
sqliteq
sqlitevecq
+Srtp
ssdlite
statm
stimeout
diff --git a/.github/DISCUSSION_TEMPLATE/beta-support.yml b/.github/DISCUSSION_TEMPLATE/beta-support.yml
index e342127a02..bad899fe03 100644
--- a/.github/DISCUSSION_TEMPLATE/beta-support.yml
+++ b/.github/DISCUSSION_TEMPLATE/beta-support.yml
@@ -10,7 +10,8 @@ body:
Before submitting, read the [beta documentation][docs].
- [docs]: https://deploy-preview-19787--frigate-docs.netlify.app/
+ [docs]: https://docs-dev.frigate.video/
+ [discussions]: https://github.com/blakeblackshear/frigate/discussions
- type: textarea
id: description
attributes:
@@ -22,8 +23,8 @@ body:
id: version
attributes:
label: Beta Version
- description: Visible on the System page in the Web UI. Please include the full version including the build identifier (eg. 0.17.0-beta1)
- placeholder: "0.17.0-beta1"
+ description: Visible on the System Metrics page in the Web UI. Please include the full version including the build identifier (eg. 0.18.0-beta1, 0.18.0-8b72c7a, etc.)
+ placeholder: "0.18.0-beta1"
validations:
required: true
- type: dropdown
@@ -71,11 +72,12 @@ body:
attributes:
label: Install method
options:
- - Home Assistant Add-on
+ - Home Assistant App
- Docker Compose
- Docker CLI
- Proxmox via Docker
- - Proxmox via TTeck Script
+ - Proxmox via installation script
+ - Proxomox via VM
- Windows WSL2
validations:
required: true
diff --git a/.github/DISCUSSION_TEMPLATE/camera-support.yml b/.github/DISCUSSION_TEMPLATE/camera-support.yml
index 521d65ded5..2c06492a8c 100644
--- a/.github/DISCUSSION_TEMPLATE/camera-support.yml
+++ b/.github/DISCUSSION_TEMPLATE/camera-support.yml
@@ -87,11 +87,12 @@ body:
attributes:
label: Install method
options:
- - Home Assistant Add-on
+ - Home Assistant App
- Docker Compose
- Docker CLI
- Proxmox via Docker
- - Proxmox via TTeck Script
+ - Proxmox via installation script
+ - Proxomox via VM
- Windows WSL2
validations:
required: true
diff --git a/.github/DISCUSSION_TEMPLATE/config-support.yml b/.github/DISCUSSION_TEMPLATE/config-support.yml
index 575f7f640e..564c9083b9 100644
--- a/.github/DISCUSSION_TEMPLATE/config-support.yml
+++ b/.github/DISCUSSION_TEMPLATE/config-support.yml
@@ -73,11 +73,12 @@ body:
attributes:
label: Install method
options:
- - Home Assistant Add-on
+ - Home Assistant App
- Docker Compose
- Docker CLI
- Proxmox via Docker
- - Proxmox via TTeck Script
+ - Proxmox via installation script
+ - Proxomox via VM
- Windows WSL2
validations:
required: true
diff --git a/.github/DISCUSSION_TEMPLATE/detector-support.yml b/.github/DISCUSSION_TEMPLATE/detector-support.yml
index fb994500f4..12c806b5de 100644
--- a/.github/DISCUSSION_TEMPLATE/detector-support.yml
+++ b/.github/DISCUSSION_TEMPLATE/detector-support.yml
@@ -53,11 +53,12 @@ body:
attributes:
label: Install method
options:
- - Home Assistant Add-on
+ - Home Assistant App
- Docker Compose
- Docker CLI
- Proxmox via Docker
- - Proxmox via TTeck Script
+ - Proxmox via installation script
+ - Proxomox via VM
- Windows WSL2
validations:
required: true
diff --git a/.github/DISCUSSION_TEMPLATE/general-support.yml b/.github/DISCUSSION_TEMPLATE/general-support.yml
index 0b9f225b6b..78d1f8ba0c 100644
--- a/.github/DISCUSSION_TEMPLATE/general-support.yml
+++ b/.github/DISCUSSION_TEMPLATE/general-support.yml
@@ -73,11 +73,12 @@ body:
attributes:
label: Install method
options:
- - Home Assistant Add-on
+ - Home Assistant App
- Docker Compose
- Docker CLI
- Proxmox via Docker
- - Proxmox via TTeck Script
+ - Proxmox via installation script
+ - Proxmox via VM
- Windows WSL2
validations:
required: true
diff --git a/.github/DISCUSSION_TEMPLATE/hardware-acceleration-support.yml b/.github/DISCUSSION_TEMPLATE/hardware-acceleration-support.yml
index 861156696a..de8d216874 100644
--- a/.github/DISCUSSION_TEMPLATE/hardware-acceleration-support.yml
+++ b/.github/DISCUSSION_TEMPLATE/hardware-acceleration-support.yml
@@ -69,11 +69,12 @@ body:
attributes:
label: Install method
options:
- - Home Assistant Add-on
+ - Home Assistant App
- Docker Compose
- Docker CLI
- Proxmox via Docker
- - Proxmox via TTeck Script
+ - Proxmox via installation script
+ - Proxomox via VM
- Windows WSL2
validations:
required: true
diff --git a/.github/DISCUSSION_TEMPLATE/report-a-bug.yml b/.github/DISCUSSION_TEMPLATE/report-a-bug.yml
index de870ac0f7..012d4ea663 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.
- **⚠️ If you are running a beta version (0.17.0-beta or similar), please use the [Beta Support template](https://github.com/blakeblackshear/frigate/discussions/new?category=beta-support) instead.**
+ **⚠️ If you are running a beta version (0.18.0-beta or similar), please use the [Beta Support template](https://github.com/blakeblackshear/frigate/discussions/new?category=beta-support) instead.**
Before submitting your bug report, please ask the AI with the "Ask AI" button on the [official documentation site][ai] about your issue, [search the discussions][discussions], look at recent open and closed [pull requests][prs], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your bug has already been fixed by the developers or reported by the community.
@@ -116,9 +116,13 @@ body:
attributes:
label: Install method
options:
- - Home Assistant Add-on
+ - Home Assistant App
- Docker Compose
- Docker CLI
+ - Proxmox via Docker
+ - Proxmox via installation script
+ - Proxomox via VM
+ - Windows WSL2
validations:
required: true
- type: dropdown
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
deleted file mode 100644
index 0af9c249f1..0000000000
--- a/.github/copilot-instructions.md
+++ /dev/null
@@ -1,401 +0,0 @@
-# GitHub Copilot Instructions for Frigate NVR
-
-This document provides coding guidelines and best practices for contributing to Frigate NVR, a complete and local NVR designed for Home Assistant with AI object detection.
-
-## Project Overview
-
-Frigate NVR is a realtime object detection system for IP cameras that uses:
-
-- **Backend**: Python 3.13+ with FastAPI, OpenCV, TensorFlow/ONNX
-- **Frontend**: React with TypeScript, Vite, TailwindCSS
-- **Architecture**: Multiprocessing design with ZMQ and MQTT communication
-- **Focus**: Minimal resource usage with maximum performance
-
-## Code Review Guidelines
-
-When reviewing code, do NOT comment on:
-
-- Missing imports - Static analysis tooling catches these
-- Code formatting - Ruff (Python) and Prettier (TypeScript/React) handle formatting
-- Minor style inconsistencies already enforced by linters
-
-## Python Backend Standards
-
-### Python Requirements
-
-- **Compatibility**: Python 3.13+
-- **Language Features**: Use modern Python features:
- - Pattern matching
- - Type hints (comprehensive typing preferred)
- - f-strings (preferred over `%` or `.format()`)
- - Dataclasses
- - Async/await patterns
-
-### Code Quality Standards
-
-- **Formatting**: Ruff (configured in `pyproject.toml`)
-- **Linting**: Ruff with rules defined in project config
-- **Type Checking**: Use type hints consistently
-- **Testing**: unittest framework - use `python3 -u -m unittest` to run tests
-- **Language**: American English for all code, comments, and documentation
-
-### Logging Standards
-
-- **Logger Pattern**: Use module-level logger
-
- ```python
- import logging
-
- logger = logging.getLogger(__name__)
- ```
-
-- **Format Guidelines**:
- - No periods at end of log messages
- - No sensitive data (keys, tokens, passwords)
- - Use lazy logging: `logger.debug("Message with %s", variable)`
-- **Log Levels**:
- - `debug`: Development and troubleshooting information
- - `info`: Important runtime events (startup, shutdown, state changes)
- - `warning`: Recoverable issues that should be addressed
- - `error`: Errors that affect functionality but don't crash the app
- - `exception`: Use in except blocks to include traceback
-
-### Error Handling
-
-- **Exception Types**: Choose most specific exception available
-- **Try/Catch Best Practices**:
- - Only wrap code that can throw exceptions
- - Keep try blocks minimal - process data after the try/except
- - Avoid bare exceptions except in background tasks
-
- Bad pattern:
-
- ```python
- try:
- data = await device.get_data() # Can throw
- # ❌ Don't process data inside try block
- processed = data.get("value", 0) * 100
- result = processed
- except DeviceError:
- logger.error("Failed to get data")
- ```
-
- Good pattern:
-
- ```python
- try:
- data = await device.get_data() # Can throw
- except DeviceError:
- logger.error("Failed to get data")
- return
-
- # ✅ Process data outside try block
- processed = data.get("value", 0) * 100
- result = processed
- ```
-
-### Async Programming
-
-- **External I/O**: All external I/O operations must be async
-- **Best Practices**:
- - Avoid sleeping in loops - use `asyncio.sleep()` not `time.sleep()`
- - Avoid awaiting in loops - use `asyncio.gather()` instead
- - No blocking calls in async functions
- - Use `asyncio.create_task()` for background operations
-- **Thread Safety**: Use proper synchronization for shared state
-
-### Documentation Standards
-
-- **Module Docstrings**: Concise descriptions at top of files
- ```python
- """Utilities for motion detection and analysis."""
- ```
-- **Function Docstrings**: Required for public functions and methods
-
- ```python
- async def process_frame(frame: ndarray, config: Config) -> Detection:
- """Process a video frame for object detection.
-
- Args:
- frame: The video frame as numpy array
- config: Detection configuration
-
- Returns:
- Detection results with bounding boxes
- """
- ```
-
-- **Comment Style**:
- - Explain the "why" not just the "what"
- - Keep lines under 88 characters when possible
- - Use clear, descriptive comments
-
-### File Organization
-
-- **API Endpoints**: `frigate/api/` - FastAPI route handlers
-- **Configuration**: `frigate/config/` - Configuration parsing and validation
-- **Detectors**: `frigate/detectors/` - Object detection backends
-- **Events**: `frigate/events/` - Event management and storage
-- **Utilities**: `frigate/util/` - Shared utility functions
-
-## Frontend (React/TypeScript) Standards
-
-### Internationalization (i18n)
-
-- **CRITICAL**: Never write user-facing strings directly in components
-- **Always use react-i18next**: Import and use the `t()` function
-
- ```tsx
- import { useTranslation } from "react-i18next";
-
- function MyComponent() {
- const { t } = useTranslation(["views/live"]);
- return
{t("camera_not_found")}
;
- }
- ```
-
-- **Translation Files**: Add English strings to the appropriate json files in `web/public/locales/en`
-- **Namespaces**: Organize translations by feature/view (e.g., `views/live`, `common`, `views/system`)
-
-### Code Quality
-
-- **Linting**: ESLint (see `web/.eslintrc.cjs`)
-- **Formatting**: Prettier with Tailwind CSS plugin
-- **Type Safety**: TypeScript strict mode enabled
-- **Testing**: Vitest for unit tests
-
-### Component Patterns
-
-- **UI Components**: Use Radix UI primitives (in `web/src/components/ui/`)
-- **Styling**: TailwindCSS with `cn()` utility for class merging
-- **State Management**: React hooks (useState, useEffect, useCallback, useMemo)
-- **Data Fetching**: Custom hooks with proper loading and error states
-
-### ESLint Rules
-
-Key rules enforced:
-
-- `react-hooks/rules-of-hooks`: error
-- `react-hooks/exhaustive-deps`: error
-- `no-console`: error (use proper logging or remove)
-- `@typescript-eslint/no-explicit-any`: warn (always use proper types instead of `any`)
-- Unused variables must be prefixed with `_`
-- Comma dangles required for multiline objects/arrays
-
-### File Organization
-
-- **Pages**: `web/src/pages/` - Route components
-- **Views**: `web/src/views/` - Complex view components
-- **Components**: `web/src/components/` - Reusable components
-- **Hooks**: `web/src/hooks/` - Custom React hooks
-- **API**: `web/src/api/` - API client functions
-- **Types**: `web/src/types/` - TypeScript type definitions
-
-## Testing Requirements
-
-### Backend Testing
-
-- **Framework**: Python unittest
-- **Run Command**: `python3 -u -m unittest`
-- **Location**: `frigate/test/`
-- **Coverage**: Aim for comprehensive test coverage of core functionality
-- **Pattern**: Use `TestCase` classes with descriptive test method names
- ```python
- class TestMotionDetection(unittest.TestCase):
- def test_detects_motion_above_threshold(self):
- # Test implementation
- ```
-
-### Test Best Practices
-
-- Always have a way to test your work and confirm your changes
-- Write tests for bug fixes to prevent regressions
-- Test edge cases and error conditions
-- Mock external dependencies (cameras, APIs, hardware)
-- Use fixtures for test data
-
-## Development Commands
-
-### Python Backend
-
-```bash
-# Run all tests
-python3 -u -m unittest
-
-# Run specific test file
-python3 -u -m unittest frigate.test.test_ffmpeg_presets
-
-# Check formatting (Ruff)
-ruff format --check frigate/
-
-# Apply formatting
-ruff format frigate/
-
-# Run linter
-ruff check frigate/
-```
-
-### Frontend (from web/ directory)
-
-```bash
-# Start dev server (AI agents should never run this directly unless asked)
-npm run dev
-
-# Build for production
-npm run build
-
-# Run linter
-npm run lint
-
-# Fix linting issues
-npm run lint:fix
-
-# Format code
-npm run prettier:write
-```
-
-### Docker Development
-
-AI agents should never run these commands directly unless instructed.
-
-```bash
-# Build local image
-make local
-
-# Build debug image
-make debug
-```
-
-## Common Patterns
-
-### API Endpoint Pattern
-
-```python
-from fastapi import APIRouter, Request
-from frigate.api.defs.tags import Tags
-
-router = APIRouter(tags=[Tags.Events])
-
-@router.get("/events")
-async def get_events(request: Request, limit: int = 100):
- """Retrieve events from the database."""
- # Implementation
-```
-
-### Configuration Access
-
-```python
-# Access Frigate configuration
-config: FrigateConfig = request.app.frigate_config
-camera_config = config.cameras["front_door"]
-```
-
-### Database Queries
-
-```python
-from frigate.models import Event
-
-# Use Peewee ORM for database access
-events = (
- Event.select()
- .where(Event.camera == camera_name)
- .order_by(Event.start_time.desc())
- .limit(limit)
-)
-```
-
-## Common Anti-Patterns to Avoid
-
-### ❌ Avoid These
-
-```python
-# Blocking operations in async functions
-data = requests.get(url) # ❌ Use async HTTP client
-time.sleep(5) # ❌ Use asyncio.sleep()
-
-# Hardcoded strings in React components
-Camera not found
# ❌ Use t("camera_not_found")
-
-# Missing error handling
-data = await api.get_data() # ❌ No exception handling
-
-# Bare exceptions in regular code
-try:
- value = await sensor.read()
-except Exception: # ❌ Too broad
- logger.error("Failed")
-
-# Returning exceptions in JSON responses
-except ValueError as e:
- return JSONResponse(
- content={"success": False, "message": str(e)},
- )
-```
-
-### ✅ Use These Instead
-
-```python
-# Async operations
-import aiohttp
-async with aiohttp.ClientSession() as session:
- async with session.get(url) as response:
- data = await response.json()
-
-await asyncio.sleep(5) # ✅ Non-blocking
-
-# Translatable strings in React
-const { t } = useTranslation();
-{t("camera_not_found")}
# ✅ Translatable
-
-# Proper error handling
-try:
- data = await api.get_data()
-except ApiException as err:
- logger.error("API error: %s", err)
- raise
-
-# Specific exceptions
-try:
- value = await sensor.read()
-except SensorException as err: # ✅ Specific
- logger.exception("Failed to read sensor")
-
-# Safe error responses
-except ValueError:
- logger.exception("Invalid parameters for API request")
- return JSONResponse(
- content={
- "success": False,
- "message": "Invalid request parameters",
- },
- )
-```
-
-## Project-Specific Conventions
-
-### Configuration Files
-
-- Main config: `config/config.yml`
-
-### Directory Structure
-
-- Backend code: `frigate/`
-- Frontend code: `web/`
-- Docker files: `docker/`
-- Documentation: `docs/`
-- Database migrations: `migrations/`
-
-### Code Style Conformance
-
-Always conform new and refactored code to the existing coding style in the project:
-
-- Follow established patterns in similar files
-- Match indentation and formatting of surrounding code
-- Use consistent naming conventions (snake_case for Python, camelCase for TypeScript)
-- Maintain the same level of verbosity in comments and docstrings
-
-## Additional Resources
-
-- Documentation: https://docs.frigate.video
-- Main Repository: https://github.com/blakeblackshear/frigate
-- Home Assistant Integration: https://github.com/blakeblackshear/frigate-hass-integration
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 120000
index 0000000000..47dc3e3d86
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1 @@
+AGENTS.md
\ No newline at end of file
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 81d448f25f..482f9939ff 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -26,7 +26,7 @@ _Please read the [contributing guidelines](https://github.com/blakeblackshear/fr
- This PR fixes or closes issue: fixes #
- This PR is related to issue:
-- Link to discussion with maintainers (**required** for large/pinned features):
+- Link to discussion with maintainers (**required** for any large or "planned" features):
## For new features
diff --git a/.github/workflows/pr_template_check.yml b/.github/workflows/pr_template_check.yml
index 57db79ecce..c82b202ef5 100644
--- a/.github/workflows/pr_template_check.yml
+++ b/.github/workflows/pr_template_check.yml
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check PR description against template
- uses: actions/github-script@v7
+ uses: actions/github-script@v9
with:
script: |
const maintainers = ['blakeblackshear', 'NickM-27', 'hawkeye217', 'dependabot[bot]', 'weblate'];
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index 6fff665a4a..d2e279966e 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -72,7 +72,7 @@ jobs:
run: npm run e2e
working-directory: ./web
- name: Upload test artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
if: failure()
with:
name: playwright-report
@@ -125,5 +125,7 @@ jobs:
run: devcontainer up --workspace-folder .
- name: Run mypy in devcontainer
run: devcontainer exec --workspace-folder . bash -lc "python3 -u -m mypy --config-file frigate/mypy.ini frigate"
+ - name: Check API spec is up to date
+ run: devcontainer exec --workspace-folder . bash -lc "python3 generate_api_auth_spec.py --check"
- name: Run unit tests in devcontainer
run: devcontainer exec --workspace-folder . bash -lc "python3 -u -m unittest"
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 011f70afd3..39512f24a3 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -18,9 +18,9 @@ jobs:
close-issue-message: ""
days-before-stale: 30
days-before-close: 3
- exempt-draft-pr: true
- exempt-issue-labels: "pinned,security"
- exempt-pr-labels: "pinned,security,dependencies"
+ exempt-draft-pr: false
+ exempt-issue-labels: "planned,security"
+ exempt-pr-labels: "planned,security,dependencies"
operations-per-run: 120
- name: Print outputs
env:
diff --git a/.gitignore b/.gitignore
index c9db2929f8..7c97a23a0e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,8 @@ core
!/web/**/*.ts
.idea/*
.ipynb_checkpoints
+
+# Auto-generated Docker Compose Generator config files
+docs/src/components/DockerComposeGenerator/config/devices.ts
+docs/src/components/DockerComposeGenerator/config/hardware.ts
+docs/src/components/DockerComposeGenerator/config/ports.ts
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000000..41d4b6460b
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,450 @@
+# Agent Instructions for Frigate NVR
+
+This document provides coding guidelines and best practices for contributing to Frigate NVR, a complete and local NVR designed for Home Assistant with AI object detection.
+
+## Project Overview
+
+Frigate NVR is a realtime object detection system for IP cameras that uses:
+
+- **Backend**: Python 3.13+ with FastAPI, OpenCV, TensorFlow/ONNX
+- **Frontend**: React with TypeScript, Vite, TailwindCSS
+- **Architecture**: Multiprocessing design with ZMQ and MQTT communication
+- **Focus**: Minimal resource usage with maximum performance
+
+## Code Review Guidelines
+
+When reviewing code, do NOT comment on:
+
+- Missing imports - Static analysis tooling catches these
+- Code formatting - Ruff (Python) and Prettier (TypeScript/React) handle formatting
+- Minor style inconsistencies already enforced by linters
+
+## Python Backend Standards
+
+### Python Requirements
+
+- **Compatibility**: Python 3.13+
+- **Language Features**: Use modern Python features:
+ - Pattern matching
+ - Type hints (comprehensive typing preferred)
+ - f-strings (preferred over `%` or `.format()`)
+ - Dataclasses
+ - Async/await patterns
+
+### Code Quality Standards
+
+- **Formatting**: Ruff (configured in `pyproject.toml`)
+- **Linting**: Ruff with rules defined in project config
+- **Type Checking**: Use type hints consistently
+- **Testing**: unittest framework - use `python3 -u -m unittest` to run tests
+- **Language**: American English for all code, comments, and documentation
+- **Punctuation**: Do not use em dashes in documentation, comments, or strings; reword with standard punctuation (commas, colons, parentheses, or separate sentences)
+
+### Logging Standards
+
+- **Logger Pattern**: Use module-level logger
+
+ ```python
+ import logging
+
+ logger = logging.getLogger(__name__)
+ ```
+
+- **Format Guidelines**:
+ - No periods at end of log messages
+ - No sensitive data (keys, tokens, passwords)
+ - Use lazy logging: `logger.debug("Message with %s", variable)`
+- **Log Levels**:
+ - `debug`: Development and troubleshooting information
+ - `info`: Important runtime events (startup, shutdown, state changes)
+ - `warning`: Recoverable issues that should be addressed
+ - `error`: Errors that affect functionality but don't crash the app
+ - `exception`: Use in except blocks to include traceback
+
+### Error Handling
+
+- **Exception Types**: Choose most specific exception available
+- **Try/Catch Best Practices**:
+ - Only wrap code that can throw exceptions
+ - Keep try blocks minimal - process data after the try/except
+ - Avoid bare exceptions except in background tasks
+
+ Bad pattern:
+
+ ```python
+ try:
+ data = await device.get_data() # Can throw
+ # ❌ Don't process data inside try block
+ processed = data.get("value", 0) * 100
+ result = processed
+ except DeviceError:
+ logger.error("Failed to get data")
+ ```
+
+ Good pattern:
+
+ ```python
+ try:
+ data = await device.get_data() # Can throw
+ except DeviceError:
+ logger.error("Failed to get data")
+ return
+
+ # ✅ Process data outside try block
+ processed = data.get("value", 0) * 100
+ result = processed
+ ```
+
+### Async Programming
+
+- **External I/O**: All external I/O operations must be async
+- **Best Practices**:
+ - Avoid sleeping in loops - use `asyncio.sleep()` not `time.sleep()`
+ - Avoid awaiting in loops - use `asyncio.gather()` instead
+ - No blocking calls in async functions
+ - Use `asyncio.create_task()` for background operations
+- **Thread Safety**: Use proper synchronization for shared state
+
+### Documentation Standards
+
+- **Module Docstrings**: Concise descriptions at top of files
+ ```python
+ """Utilities for motion detection and analysis."""
+ ```
+- **Function Docstrings**: Required for public functions and methods
+
+ ```python
+ async def process_frame(frame: ndarray, config: Config) -> Detection:
+ """Process a video frame for object detection.
+
+ Args:
+ frame: The video frame as numpy array
+ config: Detection configuration
+
+ Returns:
+ Detection results with bounding boxes
+ """
+ ```
+
+- **Comment Style**:
+ - Explain the "why" not just the "what"
+ - Keep lines under 88 characters when possible
+ - Use clear, descriptive comments
+
+### File Organization
+
+- **API Endpoints**: `frigate/api/` - FastAPI route handlers
+- **Configuration**: `frigate/config/` - Configuration parsing and validation
+- **Detectors**: `frigate/detectors/` - Object detection backends
+- **Events**: `frigate/events/` - Event management and storage
+- **Utilities**: `frigate/util/` - Shared utility functions
+
+## Frontend (React/TypeScript) Standards
+
+### Internationalization (i18n)
+
+- **CRITICAL**: Never write user-facing strings directly in components
+- **Always use react-i18next**: Import and use the `t()` function
+
+ ```tsx
+ import { useTranslation } from "react-i18next";
+
+ function MyComponent() {
+ const { t } = useTranslation(["views/live"]);
+ return {t("camera_not_found")}
;
+ }
+ ```
+
+- **Translation Files**: Add English strings to the appropriate json files in `web/public/locales/en`
+- **Namespaces**: Organize translations by feature/view (e.g., `views/live`, `common`, `views/system`)
+
+### Code Quality
+
+- **Linting**: ESLint (see `web/.eslintrc.cjs`)
+- **Formatting**: Prettier with Tailwind CSS plugin
+- **Type Safety**: TypeScript strict mode enabled
+
+### Component Patterns
+
+- **UI Components**: Use Radix UI primitives (in `web/src/components/ui/`)
+- **Styling**: TailwindCSS with `cn()` utility for class merging
+- **State Management**: React hooks (useState, useEffect, useCallback, useMemo)
+- **Data Fetching**: Custom hooks with proper loading and error states
+
+### ESLint Rules
+
+Key rules enforced:
+
+- `react-hooks/rules-of-hooks`: error
+- `react-hooks/exhaustive-deps`: error
+- `no-console`: error (use proper logging or remove)
+- `@typescript-eslint/no-explicit-any`: warn (always use proper types instead of `any`)
+- Unused variables must be prefixed with `_`
+- Comma dangles required for multiline objects/arrays
+
+### File Organization
+
+- **Pages**: `web/src/pages/` - Route components
+- **Views**: `web/src/views/` - Complex view components
+- **Components**: `web/src/components/` - Reusable components
+- **Hooks**: `web/src/hooks/` - Custom React hooks
+- **API**: `web/src/api/` - API client functions
+- **Types**: `web/src/types/` - TypeScript type definitions
+
+## Testing Requirements
+
+### Backend Testing
+
+- **Framework**: Python unittest
+- **Run Command**: `python3 -u -m unittest`
+- **Location**: `frigate/test/`
+- **Coverage**: Aim for comprehensive test coverage of core functionality
+- **Pattern**: Use `TestCase` classes with descriptive test method names
+ ```python
+ class TestMotionDetection(unittest.TestCase):
+ def test_detects_motion_above_threshold(self):
+ # Test implementation
+ ```
+
+### Test Best Practices
+
+- Always have a way to test your work and confirm your changes
+- Write tests for bug fixes to prevent regressions
+- Test edge cases and error conditions
+- Mock external dependencies (cameras, APIs, hardware)
+- Use fixtures for test data
+
+## Development Commands
+
+### Python Backend
+
+```bash
+# Run all tests
+python3 -u -m unittest
+
+# Run specific test file
+python3 -u -m unittest frigate.test.test_ffmpeg_presets
+
+# Check formatting (Ruff)
+ruff format --check frigate/
+
+# Apply formatting
+ruff format frigate/
+
+# Run linter
+ruff check frigate/
+
+# Type check
+python3 -u -m mypy --config-file frigate/mypy.ini frigate
+
+# Regenerate the OpenAPI spec after adding, changing, or removing an API
+# endpoint or its auth dependency — outputs docs/static/frigate-api.yaml,
+# annotated with each endpoint's auth requirement (admin / any / camera /
+# public). NEVER edit that file by hand. CI runs the --check variant and fails
+# if it is out of date. (from repo root)
+python3 generate_api_auth_spec.py
+python3 generate_api_auth_spec.py --check
+```
+
+### Frontend (from web/ directory)
+
+```bash
+# Start dev server (AI agents should never run this directly unless asked)
+npm run dev
+
+# Build for production
+npm run build
+
+# Run linter
+npm run lint
+
+# Fix linting issues
+npm run lint:fix
+
+# Format code
+npm run prettier:write
+
+# E2E: first-time setup
+npm install
+npx playwright install chromium
+
+# E2E: build the app and run all tests
+npm run e2e:build && npm run e2e
+
+# E2E: interactive UI for debugging
+npm run e2e:ui
+
+# E2E: run a specific spec
+npx playwright test --config e2e/playwright.config.ts e2e/specs/live.spec.ts
+
+# E2E: filter by name, or run only desktop/mobile
+npx playwright test --config e2e/playwright.config.ts --grep="severity tab"
+npx playwright test --config e2e/playwright.config.ts --project=desktop
+
+# E2E: regenerate mock data after backend model changes (from repo root)
+PYTHONPATH=. python3 web/e2e/fixtures/mock-data/generate-mock-data.py
+
+# Regenerate config translations from Pydantic models — outputs to
+# web/public/locales/en/config/{global,cameras}.json. NEVER edit those
+# JSON files by hand; change the Pydantic field title/description and
+# re-run this script. (from repo root)
+python3 generate_config_translations.py
+
+# Extract i18n keys from source into the locale files after adding
+# new t() calls. Use the :ci variant to verify the locale files are
+# in sync with source (fails if extraction would change anything).
+npm run i18n:extract
+npm run i18n:extract:ci
+```
+
+### Docker Development
+
+AI agents should never run these commands directly unless instructed.
+
+```bash
+# Build local image
+make local
+
+# Build debug image
+make debug
+```
+
+## Common Patterns
+
+### API Endpoint Pattern
+
+```python
+from fastapi import APIRouter, Request
+from frigate.api.defs.tags import Tags
+
+router = APIRouter(tags=[Tags.Events])
+
+@router.get("/events")
+async def get_events(request: Request, limit: int = 100):
+ """Retrieve events from the database."""
+ # Implementation
+```
+
+After adding, changing, or removing an endpoint (or its auth dependency), regenerate the OpenAPI spec with `python3 generate_api_auth_spec.py` so `docs/static/frigate-api.yaml` stays in sync and the endpoint's auth requirement is documented. CI enforces this via the `--check` variant; never edit that file by hand.
+
+### Configuration Access
+
+```python
+# Access Frigate configuration
+config: FrigateConfig = request.app.frigate_config
+camera_config = config.cameras["front_door"]
+```
+
+### Database Queries
+
+```python
+from frigate.models import Event
+
+# Use Peewee ORM for database access
+events = (
+ Event.select()
+ .where(Event.camera == camera_name)
+ .order_by(Event.start_time.desc())
+ .limit(limit)
+)
+```
+
+## Common Anti-Patterns to Avoid
+
+### ❌ Avoid These
+
+```python
+# Blocking operations in async functions
+data = requests.get(url) # ❌ Use async HTTP client
+time.sleep(5) # ❌ Use asyncio.sleep()
+
+# Hardcoded strings in React components
+Camera not found
# ❌ Use t("camera_not_found")
+
+# Missing error handling
+data = await api.get_data() # ❌ No exception handling
+
+# Bare exceptions in regular code
+try:
+ value = await sensor.read()
+except Exception: # ❌ Too broad
+ logger.error("Failed")
+
+# Returning exceptions in JSON responses
+except ValueError as e:
+ return JSONResponse(
+ content={"success": False, "message": str(e)},
+ )
+```
+
+### ✅ Use These Instead
+
+```python
+# Async operations
+import aiohttp
+async with aiohttp.ClientSession() as session:
+ async with session.get(url) as response:
+ data = await response.json()
+
+await asyncio.sleep(5) # ✅ Non-blocking
+
+# Translatable strings in React
+const { t } = useTranslation();
+{t("camera_not_found")}
# ✅ Translatable
+
+# Proper error handling
+try:
+ data = await api.get_data()
+except ApiException as err:
+ logger.error("API error: %s", err)
+ raise
+
+# Specific exceptions
+try:
+ value = await sensor.read()
+except SensorException as err: # ✅ Specific
+ logger.exception("Failed to read sensor")
+
+# Safe error responses
+except ValueError:
+ logger.exception("Invalid parameters for API request")
+ return JSONResponse(
+ content={
+ "success": False,
+ "message": "Invalid request parameters",
+ },
+ )
+```
+
+## WebSocket Broadcasts
+
+Outbound WebSocket broadcasts go through a per-recipient classifier in `frigate/comms/ws.py` that enforces camera-level access. **The classifier is fail-closed: any topic it doesn't recognize is dropped for every client.** New outbound topics must be classified there or they'll silently disappear.
+
+## Project-Specific Conventions
+
+### Configuration Files
+
+- Main config: `config/config.yml`
+
+### Directory Structure
+
+- Backend code: `frigate/`
+- Frontend code: `web/`
+- Docker files: `docker/`
+- Documentation: `docs/`
+- Database migrations: `migrations/`
+
+### Code Style Conformance
+
+Always conform new and refactored code to the existing coding style in the project:
+
+- Follow established patterns in similar files
+- Match indentation and formatting of surrounding code
+- Use consistent naming conventions (snake_case for Python, camelCase for TypeScript)
+- Maintain the same level of verbosity in comments and docstrings
+
+## Additional Resources
+
+- Documentation: https://docs.frigate.video
+- Main Repository: https://github.com/blakeblackshear/frigate
+- Home Assistant Integration: https://github.com/blakeblackshear/frigate-hass-integration
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 120000
index 0000000000..47dc3e3d86
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1 @@
+AGENTS.md
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9cb575d37a..e25c20835d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -10,11 +10,14 @@ If you've found a bug and want to fix it, go for it. Link to the relevant issue
### New features
-Every new feature adds scope that the maintainers must test, maintain, and support long-term. Before writing code for a new feature:
+A pull request is more than just code — it's a request for the maintainers to review, integrate, and support the change long-term. We're selective about what we take on, and prioritize changes that align with the project's direction and can be responsibly maintained in the long term.
-1. **Check for existing discussion.** Search [feature requests](https://github.com/blakeblackshear/frigate/issues) and [discussions](https://github.com/blakeblackshear/frigate/discussions) to see if it's been proposed or discussed. Pinned feature requests are on our radar — we plan to get to them, but we don't maintain a public roadmap or timeline. Check in with us first if you have interest in contributing to one.
+**Large or highly-requested features** raise the bar even higher. Popularity signals demand, but it doesn't pre-approve any particular implementation. The bigger the change, the higher the long-term cost, and the more important it is that we're aligned on scope and approach before any code is written. A large PR that lands without prior discussion is unlikely to be merged as-is, no matter how well it's implemented.
+
+Before writing code for a new feature:
+
+1. **Check for existing discussion.** Search [feature requests](https://github.com/blakeblackshear/frigate/issues) and [discussions](https://github.com/blakeblackshear/frigate/discussions) to see if it's been proposed or discussed. Feature requests tagged with "planned" are on our radar — we plan to get to them, but we don't maintain a public roadmap or timeline. Check in with us first if you have interest in contributing to one.
2. **Start a discussion or feature request first.** This helps ensure your idea aligns with Frigate's direction before you invest time building it. Community interest in a feature request helps us gauge demand, though a great idea is a great idea even without a crowd behind it.
-3. **Be open to "no".** We try to be thoughtful about what we take on, and sometimes that means saying no to good code if the feature isn't the right fit for the project. These calls are sometimes subjective, and we won't always get them right. We're happy to discuss and reconsider.
## AI usage policy
@@ -39,6 +42,8 @@ We're not trying to gatekeep how you write code. Use whatever tools make you pro
Some honest context: when we review a PR, we're not just evaluating whether the code works today. We're evaluating whether we can maintain it, debug it, and extend it long-term — often without the original author's involvement. Code that the author doesn't deeply understand is code that nobody understands, and that's a liability.
+One more thing worth saying directly: most maintainers already have access to the same AI tools you do. A PR that's entirely AI-generated — where the author can't explain the design, debug issues independently, or engage substantively in design discussions — doesn't offer something we couldn't produce ourselves. What makes a contribution genuinely valuable is the human judgment and domain understanding behind it, as well as the engagement during review that shapes it into something we can confidently take on long-term.
+
## Pull request guidelines
### Before submitting
diff --git a/docker/main/Dockerfile b/docker/main/Dockerfile
index c512ceb848..3ba1772f41 100644
--- a/docker/main/Dockerfile
+++ b/docker/main/Dockerfile
@@ -63,7 +63,7 @@ RUN --mount=type=bind,source=docker/main/build_intel_media_driver.sh,target=/dep
FROM scratch AS go2rtc
ARG TARGETARCH
WORKDIR /rootfs/usr/local/go2rtc/bin
-ADD --link --chmod=755 "https://github.com/AlexxIT/go2rtc/releases/download/v1.9.13/go2rtc_linux_${TARGETARCH}" go2rtc
+ADD --link --chmod=755 "https://github.com/AlexxIT/go2rtc/releases/download/v1.9.14/go2rtc_linux_${TARGETARCH}" go2rtc
FROM wget AS tempio
ARG TARGETARCH
@@ -265,8 +265,8 @@ ENV PATH="/usr/local/go2rtc/bin:/usr/local/tempio/bin:/usr/local/nginx/sbin:${PA
RUN --mount=type=bind,source=docker/main/install_deps.sh,target=/deps/install_deps.sh \
/deps/install_deps.sh
-ENV DEFAULT_FFMPEG_VERSION="7.0"
-ENV INCLUDED_FFMPEG_VERSIONS="${DEFAULT_FFMPEG_VERSION}:5.0"
+ENV DEFAULT_FFMPEG_VERSION="8.0"
+ENV INCLUDED_FFMPEG_VERSIONS="${DEFAULT_FFMPEG_VERSION}:7.0:5.0"
RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \
&& sed -i 's/args.append("setuptools")/args.append("setuptools==77.0.3")/' get-pip.py \
diff --git a/docker/main/install_deps.sh b/docker/main/install_deps.sh
index d54a64c520..e197ce1b68 100755
--- a/docker/main/install_deps.sh
+++ b/docker/main/install_deps.sh
@@ -52,9 +52,13 @@ if [[ "${TARGETARCH}" == "amd64" ]]; then
tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/5.0 --strip-components 1 amd64/bin/ffmpeg amd64/bin/ffprobe
rm -rf ffmpeg.tar.xz
mkdir -p /usr/lib/ffmpeg/7.0
- wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2026-03-19-13-03/ffmpeg-n7.1.3-43-g5a1f107b4c-linux64-gpl-7.1.tar.xz"
+ wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2024-09-19-12-51/ffmpeg-n7.0.2-18-g3e6cec1286-linux64-gpl-7.0.tar.xz"
tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/7.0 --strip-components 1 amd64/bin/ffmpeg amd64/bin/ffprobe
rm -rf ffmpeg.tar.xz
+ mkdir -p /usr/lib/ffmpeg/8.0
+ wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2026-06-02-14-20/ffmpeg-n8.1.1-9-g58d4114d36-linux64-gpl-8.1.tar.xz"
+ tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/8.0 --strip-components 1 amd64/bin/ffmpeg amd64/bin/ffprobe
+ rm -rf ffmpeg.tar.xz
fi
# ffmpeg -> arm64
@@ -64,9 +68,13 @@ if [[ "${TARGETARCH}" == "arm64" ]]; then
tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/5.0 --strip-components 1 arm64/bin/ffmpeg arm64/bin/ffprobe
rm -f ffmpeg.tar.xz
mkdir -p /usr/lib/ffmpeg/7.0
- wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2026-03-19-13-03/ffmpeg-n7.1.3-43-g5a1f107b4c-linuxarm64-gpl-7.1.tar.xz"
+ wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2024-09-19-12-51/ffmpeg-n7.0.2-18-g3e6cec1286-linuxarm64-gpl-7.0.tar.xz"
tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/7.0 --strip-components 1 arm64/bin/ffmpeg arm64/bin/ffprobe
rm -f ffmpeg.tar.xz
+ mkdir -p /usr/lib/ffmpeg/8.0
+ wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2026-06-02-14-20/ffmpeg-n8.1.1-9-g58d4114d36-linuxarm64-gpl-8.1.tar.xz"
+ tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/8.0 --strip-components 1 arm64/bin/ffmpeg arm64/bin/ffprobe
+ rm -f ffmpeg.tar.xz
fi
# arch specific packages
@@ -87,40 +95,47 @@ if [[ "${TARGETARCH}" == "amd64" ]]; then
# intel packages use zst compression so we need to update dpkg
apt-get install -y dpkg
- # use intel apt intel packages
+ # use intel apt repo for libmfx1 (legacy QSV, pre-Gen12)
wget -qO - https://repositories.intel.com/gpu/intel-graphics.key | gpg --yes --dearmor --output /usr/share/keyrings/intel-graphics.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/gpu/ubuntu jammy client" | tee /etc/apt/sources.list.d/intel-gpu-jammy.list
apt-get -qq update
+
# intel-media-va-driver-non-free is built from source in the
# intel-media-driver Dockerfile stage for Battlemage (Xe2) support
apt-get -qq install --no-install-recommends --no-install-suggests -y \
- libmfx1 libmfxgen1 libvpl2
+ libmfx1
+ rm -f /usr/share/keyrings/intel-graphics.gpg
+ rm -f /etc/apt/sources.list.d/intel-gpu-jammy.list
+ # upgrade libva2, oneVPL runtime, and libvpl2 from trixie for Battlemage support
+ echo "deb http://deb.debian.org/debian trixie main" > /etc/apt/sources.list.d/trixie.list
+ apt-get -qq update
+ apt-get -qq install -y -t trixie libva2 libva-drm2 libzstd1
+ apt-get -qq install -y -t trixie libmfx-gen1.2 libvpl2
+ rm -f /etc/apt/sources.list.d/trixie.list
+ apt-get -qq update
apt-get -qq install -y ocl-icd-libopencl1
# install libtbb12 for NPU support
apt-get -qq install -y libtbb12
- rm -f /usr/share/keyrings/intel-graphics.gpg
- rm -f /etc/apt/sources.list.d/intel-gpu-jammy.list
-
- # install legacy and standard intel icd and level-zero-gpu
+ # install legacy and standard intel compute packages
# see https://github.com/intel/compute-runtime/blob/master/LEGACY_PLATFORMS.md for more info
# needed core package
- wget https://github.com/intel/compute-runtime/releases/download/25.13.33276.19/libigdgmm12_22.7.0_amd64.deb
- dpkg -i libigdgmm12_22.7.0_amd64.deb
- rm libigdgmm12_22.7.0_amd64.deb
+ wget https://github.com/intel/compute-runtime/releases/download/26.14.37833.4/libigdgmm12_22.9.0_amd64.deb
+ dpkg -i libigdgmm12_22.9.0_amd64.deb
+ rm libigdgmm12_22.9.0_amd64.deb
- # legacy packages
+ # legacy compute-runtime packages
wget https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-opencl-icd-legacy1_24.35.30872.36_amd64.deb
wget https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-level-zero-gpu-legacy1_1.5.30872.36_amd64.deb
wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-opencl_1.0.17537.24_amd64.deb
wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-core_1.0.17537.24_amd64.deb
- # standard packages
- wget https://github.com/intel/compute-runtime/releases/download/25.13.33276.19/intel-opencl-icd_25.13.33276.19_amd64.deb
- wget https://github.com/intel/compute-runtime/releases/download/25.13.33276.19/intel-level-zero-gpu_1.6.33276.19_amd64.deb
- wget https://github.com/intel/intel-graphics-compiler/releases/download/v2.10.10/intel-igc-opencl-2_2.10.10+18926_amd64.deb
- wget https://github.com/intel/intel-graphics-compiler/releases/download/v2.10.10/intel-igc-core-2_2.10.10+18926_amd64.deb
+ # standard compute-runtime packages
+ wget https://github.com/intel/compute-runtime/releases/download/26.14.37833.4/intel-opencl-icd_26.14.37833.4-0_amd64.deb
+ wget https://github.com/intel/compute-runtime/releases/download/26.14.37833.4/libze-intel-gpu1_26.14.37833.4-0_amd64.deb
+ wget https://github.com/intel/intel-graphics-compiler/releases/download/v2.32.7/intel-igc-opencl-2_2.32.7+21184_amd64.deb
+ wget https://github.com/intel/intel-graphics-compiler/releases/download/v2.32.7/intel-igc-core-2_2.32.7+21184_amd64.deb
# npu packages
wget https://github.com/oneapi-src/level-zero/releases/download/v1.28.2/level-zero_1.28.2+u22.04_amd64.deb
wget https://github.com/intel/linux-npu-driver/releases/download/v1.19.0/intel-driver-compiler-npu_1.19.0.20250707-16111289554_ubuntu22.04_amd64.deb
@@ -130,6 +145,10 @@ if [[ "${TARGETARCH}" == "amd64" ]]; then
dpkg -i *.deb
rm *.deb
apt-get -qq install -f -y
+
+ # Battlemage uses the xe kernel driver, but the VA-API driver is still iHD.
+ # The oneVPL runtime may look for a driver named after the kernel module.
+ ln -sf /usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so /usr/lib/x86_64-linux-gnu/dri/xe_drv_video.so
fi
if [[ "${TARGETARCH}" == "arm64" ]]; then
diff --git a/docker/main/requirements-dev.txt b/docker/main/requirements-dev.txt
index ac9d357583..df5818fe0d 100644
--- a/docker/main/requirements-dev.txt
+++ b/docker/main/requirements-dev.txt
@@ -1,4 +1,4 @@
-ruff
+ruff == 0.15.20
# types
types-peewee == 3.17.*
diff --git a/docker/main/requirements-wheels.txt b/docker/main/requirements-wheels.txt
index f81fefea47..7bd098454d 100644
--- a/docker/main/requirements-wheels.txt
+++ b/docker/main/requirements-wheels.txt
@@ -11,7 +11,7 @@ joserfc == 1.2.*
cryptography == 44.0.*
pathvalidate == 3.3.*
markupsafe == 3.0.*
-python-multipart == 0.0.20
+python-multipart == 0.0.26
# Classification Model Training
tensorflow == 2.19.* ; platform_machine == 'aarch64'
tensorflow-cpu == 2.19.* ; platform_machine == 'x86_64'
@@ -42,7 +42,7 @@ opencv-python-headless == 4.11.0.*
opencv-contrib-python == 4.11.0.*
scipy == 1.16.*
# OpenVino & ONNX
-openvino == 2025.3.*
+openvino == 2025.4.*
onnxruntime == 1.22.*
# Embeddings
transformers == 4.45.*
diff --git a/docker/main/rootfs/usr/local/ffmpeg/get_ffmpeg_path.py b/docker/main/rootfs/usr/local/ffmpeg/get_ffmpeg_path.py
index 0f492cc5c5..9f4d08f2e3 100644
--- a/docker/main/rootfs/usr/local/ffmpeg/get_ffmpeg_path.py
+++ b/docker/main/rootfs/usr/local/ffmpeg/get_ffmpeg_path.py
@@ -5,11 +5,7 @@ from typing import Any
from ruamel.yaml import YAML
sys.path.insert(0, "/opt/frigate")
-from frigate.const import (
- DEFAULT_FFMPEG_VERSION,
- INCLUDED_FFMPEG_VERSIONS,
-)
-from frigate.util.config import find_config_file
+from frigate.util.config import find_config_file, resolve_ffmpeg_path
sys.path.remove("/opt/frigate")
@@ -29,9 +25,4 @@ except FileNotFoundError:
config: dict[str, Any] = {}
path = config.get("ffmpeg", {}).get("path", "default")
-if path == "default":
- print(f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffmpeg")
-elif path in INCLUDED_FFMPEG_VERSIONS:
- print(f"/usr/lib/ffmpeg/{path}/bin/ffmpeg")
-else:
- print(f"{path}/bin/ffmpeg")
+print(resolve_ffmpeg_path(path, "ffmpeg"))
diff --git a/docker/main/rootfs/usr/local/go2rtc/create_config.py b/docker/main/rootfs/usr/local/go2rtc/create_config.py
index 71897be0a7..70cb744f13 100644
--- a/docker/main/rootfs/usr/local/go2rtc/create_config.py
+++ b/docker/main/rootfs/usr/local/go2rtc/create_config.py
@@ -12,42 +12,27 @@ sys.path.insert(0, "/opt/frigate")
from frigate.config.env import substitute_frigate_vars
from frigate.const import (
BIRDSEYE_PIPE,
- DEFAULT_FFMPEG_VERSION,
- INCLUDED_FFMPEG_VERSIONS,
LIBAVFORMAT_VERSION_MAJOR,
)
from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_encode
-from frigate.util.config import find_config_file
+from frigate.util.config import find_config_file, resolve_ffmpeg_path
+from frigate.util.services import (
+ is_go2rtc_arbitrary_exec_allowed,
+ is_restricted_go2rtc_source,
+)
sys.path.remove("/opt/frigate")
yaml = YAML()
-# Check if arbitrary exec sources are allowed (defaults to False for security)
-allow_arbitrary_exec = None
-if "GO2RTC_ALLOW_ARBITRARY_EXEC" in os.environ:
- allow_arbitrary_exec = os.environ.get("GO2RTC_ALLOW_ARBITRARY_EXEC")
-elif (
- os.path.isdir("/run/secrets")
- and os.access("/run/secrets", os.R_OK)
- and "GO2RTC_ALLOW_ARBITRARY_EXEC" in os.listdir("/run/secrets")
-):
- allow_arbitrary_exec = (
- Path(os.path.join("/run/secrets", "GO2RTC_ALLOW_ARBITRARY_EXEC"))
- .read_text()
- .strip()
- )
-# check for the add-on options file
-elif os.path.isfile("/data/options.json"):
- with open("/data/options.json") as f:
- raw_options = f.read()
- options = json.loads(raw_options)
- allow_arbitrary_exec = options.get("go2rtc_allow_arbitrary_exec")
-
-ALLOW_ARBITRARY_EXEC = allow_arbitrary_exec is not None and str(
- allow_arbitrary_exec
-).lower() in ("true", "1", "yes")
-
+FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")}
+# read docker secret files as env vars too
+if os.path.isdir("/run/secrets"):
+ for secret_file in os.listdir("/run/secrets"):
+ if secret_file.startswith("FRIGATE_"):
+ FRIGATE_ENV_VARS[secret_file] = (
+ Path(os.path.join("/run/secrets", secret_file)).read_text().strip()
+ )
config_file = find_config_file()
@@ -107,12 +92,7 @@ if go2rtc_config.get("rtsp", {}).get("password") is not None:
# ensure ffmpeg path is set correctly
path = config.get("ffmpeg", {}).get("path", "default")
-if path == "default":
- ffmpeg_path = f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffmpeg"
-elif path in INCLUDED_FFMPEG_VERSIONS:
- ffmpeg_path = f"/usr/lib/ffmpeg/{path}/bin/ffmpeg"
-else:
- ffmpeg_path = f"{path}/bin/ffmpeg"
+ffmpeg_path = resolve_ffmpeg_path(path, "ffmpeg")
if go2rtc_config.get("ffmpeg") is None:
go2rtc_config["ffmpeg"] = {"bin": ffmpeg_path}
@@ -128,18 +108,13 @@ if LIBAVFORMAT_VERSION_MAJOR < 59:
go2rtc_config["ffmpeg"]["rtsp"] = rtsp_args
-def is_restricted_source(stream_source: str) -> bool:
- """Check if a stream source is restricted (echo, expr, or exec)."""
- return stream_source.strip().startswith(("echo:", "expr:", "exec:"))
-
-
for name in list(go2rtc_config.get("streams", {})):
stream = go2rtc_config["streams"][name]
if isinstance(stream, str):
try:
- formatted_stream = substitute_frigate_vars(stream)
- if not ALLOW_ARBITRARY_EXEC and is_restricted_source(formatted_stream):
+ formatted_stream = stream.format(**FRIGATE_ENV_VARS)
+ if is_restricted_go2rtc_source(formatted_stream):
print(
f"[ERROR] Stream '{name}' uses a restricted source (echo/expr/exec) which is disabled by default for security. "
f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec sources."
@@ -157,8 +132,8 @@ for name in list(go2rtc_config.get("streams", {})):
filtered_streams = []
for i, stream_item in enumerate(stream):
try:
- formatted_stream = substitute_frigate_vars(stream_item)
- if not ALLOW_ARBITRARY_EXEC and is_restricted_source(formatted_stream):
+ formatted_stream = stream_item.format(**FRIGATE_ENV_VARS)
+ if is_restricted_go2rtc_source(formatted_stream):
print(
f"[ERROR] Stream '{name}' item {i + 1} uses a restricted source (echo/expr/exec) which is disabled by default for security. "
f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec sources."
@@ -181,6 +156,20 @@ for name in list(go2rtc_config.get("streams", {})):
)
del go2rtc_config["streams"][name]
+ elif isinstance(stream, dict):
+ # The map form ({"url": ...}) lets go2rtc resolve the source
+ # recursively, so it is effectively a dynamic way to generate the URL
+ # for a stream. That can only be backed by an exec source, so it cannot
+ # be allowed unless arbitrary exec is explicitly enabled. When it is
+ # enabled, leave the map untouched for go2rtc to resolve.
+ if not is_go2rtc_arbitrary_exec_allowed():
+ print(
+ f"[ERROR] Stream '{name}' uses a dynamic source format which is disabled by default for security. "
+ f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec sources."
+ )
+ del go2rtc_config["streams"][name]
+ continue
+
# add birdseye restream stream if enabled
if config.get("birdseye", {}).get("restream", False):
birdseye: dict[str, Any] = config.get("birdseye")
diff --git a/docker/main/rootfs/usr/local/nginx/conf/nginx.conf b/docker/main/rootfs/usr/local/nginx/conf/nginx.conf
index d954bdcd52..399dba5736 100644
--- a/docker/main/rootfs/usr/local/nginx/conf/nginx.conf
+++ b/docker/main/rootfs/usr/local/nginx/conf/nginx.conf
@@ -252,6 +252,7 @@ http {
include proxy.conf;
proxy_cache api_cache;
+ proxy_cache_key "$scheme$proxy_host$request_uri|$role|$groups|$user";
proxy_cache_lock on;
proxy_cache_use_stale updating;
proxy_cache_valid 200 5s;
@@ -273,6 +274,13 @@ http {
include proxy.conf;
}
+ location /api/logout {
+ auth_request off;
+ rewrite ^/api(/.*)$ $1 break;
+ proxy_pass http://frigate_api;
+ include proxy.conf;
+ }
+
# Allow unauthenticated access to the first_time_login endpoint
# so the login page can load help text before authentication.
location /api/auth/first_time_login {
diff --git a/docker/rockchip/conv2rknn.py b/docker/rockchip/conv2rknn.py
index 4880d98684..700f35689f 100644
--- a/docker/rockchip/conv2rknn.py
+++ b/docker/rockchip/conv2rknn.py
@@ -11,10 +11,10 @@ except FileNotFoundError:
pass
try:
- with open("/config/conv2rknn.yaml", "r") as config_file:
+ with open("/config/conv2rknn.yaml") as config_file:
configuration = yaml.safe_load(config_file)
except FileNotFoundError:
- raise Exception("Please place a config file at /config/conv2rknn.yaml")
+ raise Exception("Please place a config file at /config/conv2rknn.yaml") from None
if configuration["config"] != None:
rknn_config = configuration["config"]
@@ -31,7 +31,7 @@ if "soc" not in configuration:
with open("/proc/device-tree/compatible") as file:
soc = file.read().split(",")[-1].strip("\x00")
except FileNotFoundError:
- raise Exception("Make sure to run docker in privileged mode.")
+ raise Exception("Make sure to run docker in privileged mode.") from None
configuration["soc"] = [
soc,
diff --git a/docker/rocm/Dockerfile b/docker/rocm/Dockerfile
index 13bd357491..653ed1e4ee 100644
--- a/docker/rocm/Dockerfile
+++ b/docker/rocm/Dockerfile
@@ -13,7 +13,7 @@ ARG ROCM
RUN apt update -qq && \
apt install -y wget gpg && \
- wget -O rocm.deb https://repo.radeon.com/amdgpu-install/7.2/ubuntu/jammy/amdgpu-install_7.2.70200-1_all.deb && \
+ wget -O rocm.deb https://repo.radeon.com/amdgpu-install/7.2.3/ubuntu/jammy/amdgpu-install_7.2.3.70203-1_all.deb && \
apt install -y ./rocm.deb && \
apt update && \
apt install -qq -y rocm
@@ -32,11 +32,14 @@ RUN echo /opt/rocm/lib|tee /opt/rocm-dist/etc/ld.so.conf.d/rocm.conf
FROM deps AS deps-prelim
COPY docker/rocm/debian-backports.sources /etc/apt/sources.list.d/debian-backports.sources
-RUN apt-get update && \
+# install_deps.sh upgraded libstdc++6 from trixie for Battlemage; the matching
+# -dev package must also come from trixie or apt refuses to satisfy it.
+RUN echo "deb http://deb.debian.org/debian trixie main" > /etc/apt/sources.list.d/trixie.list && \
+ apt-get update && \
apt-get install -y libnuma1 && \
apt-get install -qq -y -t bookworm-backports mesa-va-drivers mesa-vulkan-drivers && \
- # Install C++ standard library headers for HIPRTC kernel compilation fallback
- apt-get install -qq -y libstdc++-12-dev && \
+ apt-get install -qq -y -t trixie libstdc++-14-dev && \
+ rm -f /etc/apt/sources.list.d/trixie.list && \
rm -rf /var/lib/apt/lists/*
WORKDIR /opt/frigate
@@ -75,6 +78,10 @@ ENV MIGRAPHX_DISABLE_MIOPEN_FUSION=1
ENV MIGRAPHX_DISABLE_SCHEDULE_PASS=1
ENV MIGRAPHX_DISABLE_REDUCE_FUSION=1
ENV MIGRAPHX_ENABLE_HIPRTC_WORKAROUNDS=1
+ENV MIOPEN_CUSTOM_CACHE_DIR=/config/model_cache/migraphx
+ENV MIOPEN_USER_DB_PATH=/config/model_cache/migraphx
+ENV AMD_COMGR_CACHE=1
+ENV AMD_COMGR_CACHE_DIR=/config/model_cache/migraphx
COPY --from=rocm-dist / /
diff --git a/docker/rocm/requirements-wheels-rocm.txt b/docker/rocm/requirements-wheels-rocm.txt
index da22f2ff6d..f60b550c3f 100644
--- a/docker/rocm/requirements-wheels-rocm.txt
+++ b/docker/rocm/requirements-wheels-rocm.txt
@@ -1 +1 @@
-onnxruntime-migraphx @ https://github.com/NickM-27/frigate-onnxruntime-rocm/releases/download/v7.2.0/onnxruntime_migraphx-1.23.1-cp311-cp311-linux_x86_64.whl
\ No newline at end of file
+onnxruntime-migraphx @ https://github.com/NickM-27/frigate-onnxruntime-rocm/releases/download/v7.2.3-1/onnxruntime_migraphx-1.24.4-cp311-cp311-linux_x86_64.whl
\ No newline at end of file
diff --git a/docker/rocm/rocm.hcl b/docker/rocm/rocm.hcl
index 710bfe9956..224118818e 100644
--- a/docker/rocm/rocm.hcl
+++ b/docker/rocm/rocm.hcl
@@ -1,5 +1,5 @@
variable "ROCM" {
- default = "7.2.0"
+ default = "7.2.3"
}
variable "HSA_OVERRIDE_GFX_VERSION" {
default = ""
diff --git a/docs/data/object_detectors_models.yaml b/docs/data/object_detectors_models.yaml
new file mode 100644
index 0000000000..a4458c3091
--- /dev/null
+++ b/docs/data/object_detectors_models.yaml
@@ -0,0 +1,1346 @@
+edgeTPU:
+ title: EdgeTPU
+ models:
+ - key: mobiledet
+ label: Mobiledet
+ recommended: true
+ download: A TensorFlow Lite model is provided in the container at `/edgetpu_model.tflite` and is used by this detector type by default. To provide your own model, bind mount the file into the container and provide the path with `model.path`.
+ ui: Navigate to **Settings > System > Detectors and model** and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `usb`.
+ yaml: |-
+ detectors:
+ coral:
+ type: edgetpu
+ device: usb
+ - key: yolov9
+ label: YOLOv9
+ recommended: false
+ download: "[Download the model](https://github.com/dbro/frigate-detector-edgetpu-yolo9/releases/download/v1.0/yolov9-s-relu6-best_320_int8_edgetpu.tflite), bind mount the file into the container, and provide the path with `model.path`. Note that the linked model requires a 17-label [labelmap file](https://raw.githubusercontent.com/dbro/frigate-detector-edgetpu-yolo9/refs/heads/main/labels-coco17.txt) that includes only 17 COCO classes."
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `usb`. Then on the same page, in the **Custom Model** tab, configure the model settings:
+
+ | Field | Value |
+ | ---------------------------------------- | ----------------------------------------------------------------- |
+ | **Custom object detector model path** | `/config/model_cache/yolov9-s-relu6-best_320_int8_edgetpu.tflite` |
+ | **Label map for custom object detector** | `/config/labels-coco17.txt` |
+ | **Object detection model input width** | `320` (should match the imgsize of the model) |
+ | **Object detection model input height** | `320` (should match the imgsize of the model) |
+ | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) |
+ | **Model Input Tensor Shape** | `nhwc` (Frigate's default value) |
+ | **Model Input D Type** | `int` (Frigate's default value) |
+ | **Object Detection Model Type** | `yolo-generic` |
+ yaml: |-
+ detectors:
+ coral:
+ type: edgetpu
+ device: usb
+
+ model:
+ model_type: yolo-generic
+ width: 320 # <--- should match the imgsize of the model, typically 320
+ height: 320 # <--- should match the imgsize of the model, typically 320
+ path: /config/model_cache/yolov9-s-relu6-best_320_int8_edgetpu.tflite
+ labelmap_path: /config/labels-coco17.txt
+hailo8l:
+ title: Hailo-8/Hailo-8L
+ models:
+ - key: yolo
+ label: YOLO
+ recommended: true
+ download: If no custom model path or URL is provided, the Hailo detector automatically downloads the default model (YOLOv6n) from the Hailo Model Zoo on first startup based on the detected hardware. Once cached under `/config/model_cache/hailo`, the model works fully offline.
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **Hailo-8/Hailo-8L** from the detector type dropdown and click **Add**, then set device to `PCIe`. Then on the same page, in the **Custom Model** tab, configure the model settings:
+
+ | Field | Value |
+ | ---------------------------------------- | ----------------------- |
+ | **Label map for custom object detector** | `/labelmap/coco-80.txt` |
+ | **Object detection model input width** | `320` |
+ | **Object detection model input height** | `320` |
+ | **Model Input Pixel Color Format** | `rgb` |
+ | **Model Input Tensor Shape** | `nhwc` |
+ | **Model Input D Type** | `int` |
+ | **Object Detection Model Type** | `yolo-generic` |
+
+ The detector automatically selects the default model based on your hardware. Optionally, specify a local model path or URL to override.
+ yaml: |-
+ detectors:
+ hailo:
+ type: hailo8l
+ device: PCIe
+
+ model:
+ width: 320
+ height: 320
+ input_tensor: nhwc
+ input_pixel_format: rgb
+ input_dtype: int
+ model_type: yolo-generic
+ labelmap_path: /labelmap/coco-80.txt
+
+ # The detector automatically selects the default model based on your hardware:
+ # - For Hailo-8 hardware: YOLOv6n (default: yolov6n.hef)
+ # - For Hailo-8L hardware: YOLOv6n (default: yolov6n.hef)
+ #
+ # Optionally, you can specify a local model path to override the default.
+ # If a local path is provided and the file exists, it will be used instead of downloading.
+ # Example:
+ # path: /config/model_cache/hailo/yolov6n.hef
+ #
+ # You can also override using a custom URL:
+ # path: https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8/yolov6n.hef
+ # just make sure to give it the write configuration based on the model
+ - key: ssd
+ label: SSD MobileNet v1
+ recommended: false
+ download: For SSD-based models, provide either a model path or URL to your compiled SSD model. The integration will first check the local path before downloading if necessary. The model file is cached under `/config/model_cache/hailo`.
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **Hailo-8/Hailo-8L** from the detector type dropdown and click **Add**, then set device to `PCIe`. Then on the same page, in the **Custom Model** tab, configure the model settings:
+
+ | Field | Value |
+ | --------------------------------------- | ------ |
+ | **Object detection model input width** | `300` |
+ | **Object detection model input height** | `300` |
+ | **Model Input Pixel Color Format** | `rgb` |
+ | **Model Input Tensor Shape** | `nhwc` |
+ | **Model Input D Type** | `int` (Frigate's default value) |
+ | **Object Detection Model Type** | `ssd` |
+
+ Specify the local model path or URL for SSD MobileNet v1.
+ yaml: |-
+ detectors:
+ hailo:
+ type: hailo8l
+ device: PCIe
+
+ model:
+ width: 300
+ height: 300
+ input_tensor: nhwc
+ input_pixel_format: rgb
+ model_type: ssd
+ # Specify the local model path (if available) or URL for SSD MobileNet v1.
+ # Example with a local path:
+ # path: /config/model_cache/h8l_cache/ssd_mobilenet_v1.hef
+ #
+ # Or override using a custom URL:
+ # path: https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8l/ssd_mobilenet_v1.hef
+openvino:
+ title: OpenVINO
+ models:
+ - key: yolov9
+ label: YOLOv9
+ recommended: true
+ download: |-
+ YOLOv9 model can be exported as ONNX using the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=t` and `IMG_SIZE=320` in the first line to the [model size](https://github.com/WongKinYiu/yolov9#performance) you would like to convert (available model sizes are `t`, `s`, `m`, `c`, and `e`, common image sizes are `320` and `640`).
+
+ ```sh
+ docker build . --build-arg MODEL_SIZE=t --build-arg IMG_SIZE=320 --output . -f- <<'EOF'
+ FROM python:3.11 AS build
+ RUN apt-get update && apt-get install --no-install-recommends -y cmake libgl1 && rm -rf /var/lib/apt/lists/*
+ COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/
+ WORKDIR /yolov9
+ ADD https://github.com/WongKinYiu/yolov9.git .
+ RUN uv pip install --system -r requirements.txt
+ RUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier==0.4.* onnxscript
+ ARG MODEL_SIZE
+ ARG IMG_SIZE
+ ADD https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-${MODEL_SIZE}-converted.pt yolov9-${MODEL_SIZE}.pt
+ RUN sed -i "s/ckpt = torch.load(attempt_download(w), map_location='cpu')/ckpt = torch.load(attempt_download(w), map_location='cpu', weights_only=False)/g" models/experimental.py
+ RUN python3 export.py --weights ./yolov9-${MODEL_SIZE}.pt --imgsz ${IMG_SIZE} --simplify --include onnx
+ FROM scratch
+ ARG MODEL_SIZE
+ ARG IMG_SIZE
+ COPY --from=build /yolov9/yolov9-${MODEL_SIZE}.onnx /yolov9-${MODEL_SIZE}-${IMG_SIZE}.onnx
+ EOF
+ ```
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU` (or `NPU`). Then on the same page, in the **Custom Model** tab, configure:
+
+ | Field | Value |
+ | ---------------------------------------- | -------------------------------------------------------- |
+ | **Custom object detector model path** | `/config/model_cache/yolo.onnx` (use the filename you generated above) |
+ | **Label map for custom object detector** | `/labelmap/coco-80.txt` |
+ | **Object detection model input width** | `320` (should match the imgsize set during model export) |
+ | **Object detection model input height** | `320` (should match the imgsize set during model export) |
+ | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) |
+ | **Model Input Tensor Shape** | `nchw` |
+ | **Model Input D Type** | `float` |
+ | **Object Detection Model Type** | `yolo-generic` |
+ yaml: |-
+ detectors:
+ ov:
+ type: openvino
+ device: GPU # or NPU
+
+ model:
+ model_type: yolo-generic
+ width: 320 # <--- should match the imgsize set during model export
+ height: 320 # <--- should match the imgsize set during model export
+ input_tensor: nchw
+ input_dtype: float
+ path: /config/model_cache/yolo.onnx # use the filename you generated above
+ labelmap_path: /labelmap/coco-80.txt
+ - key: ssd
+ label: SSDLite MobileNet v2
+ recommended: false
+ download: An OpenVINO model is provided in the container at `/openvino-model/ssdlite_mobilenet_v2.xml` and is used by this detector type by default. The model comes from Intel's Open Model Zoo [SSDLite MobileNet V2](https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/ssdlite_mobilenet_v2) and is converted to an FP16 precision IR model.
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU` (or `NPU`). Then on the same page, in the **Custom Model** tab, configure:
+
+ | Field | Value |
+ | ---------------------------------------- | ------------------------------------------ |
+ | **Custom object detector model path** | `/openvino-model/ssdlite_mobilenet_v2.xml` |
+ | **Label map for custom object detector** | `/openvino-model/coco_91cl_bkgr.txt` |
+ | **Object detection model input width** | `300` |
+ | **Object detection model input height** | `300` |
+ | **Model Input Pixel Color Format** | `bgr` |
+ | **Model Input Tensor Shape** | `nhwc` |
+ | **Model Input D Type** | `int` (Frigate's default value) |
+ | **Object Detection Model Type** | `ssd` (Frigate's default value) |
+ yaml: |-
+ detectors:
+ ov:
+ type: openvino
+ device: GPU # Or NPU
+
+ model:
+ width: 300
+ height: 300
+ input_tensor: nhwc
+ input_pixel_format: bgr
+ path: /openvino-model/ssdlite_mobilenet_v2.xml
+ labelmap_path: /openvino-model/coco_91cl_bkgr.txt
+ - key: yolo-legacy
+ label: YOLO (v3, v4, v7)
+ recommended: false
+ download: |-
+ To export as ONNX:
+
+ ```sh
+ git clone https://github.com/NateMeyer/tensorrt_demos
+ cd tensorrt_demos/yolo
+ ./download_yolo.sh
+ python3 yolo_to_onnx.py -m yolov7-320
+ ```
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU` (or `NPU`). Then on the same page, in the **Custom Model** tab, configure:
+
+ | Field | Value |
+ | ---------------------------------------- | -------------------------------------------------------- |
+ | **Custom object detector model path** | `/config/model_cache/yolo.onnx` (use the filename you generated above) |
+ | **Label map for custom object detector** | `/labelmap/coco-80.txt` |
+ | **Object detection model input width** | `320` (should match the imgsize set during model export) |
+ | **Object detection model input height** | `320` (should match the imgsize set during model export) |
+ | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) |
+ | **Model Input Tensor Shape** | `nchw` |
+ | **Model Input D Type** | `float` |
+ | **Object Detection Model Type** | `yolo-generic` |
+ yaml: |-
+ detectors:
+ ov:
+ type: openvino
+ device: GPU # or NPU
+
+ model:
+ model_type: yolo-generic
+ width: 320 # <--- should match the imgsize set during model export
+ height: 320 # <--- should match the imgsize set during model export
+ input_tensor: nchw
+ input_dtype: float
+ path: /config/model_cache/yolo.onnx # use the filename you generated above
+ labelmap_path: /labelmap/coco-80.txt
+ - key: yolonas
+ label: YOLO-NAS
+ recommended: false
+ download: |-
+ You can build and download a compatible model with pre-trained weights using [this notebook](https://github.com/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb) [](https://colab.research.google.com/github/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb) which can be run directly in [Google Colab](https://colab.research.google.com/github/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb).
+
+ :::warning
+
+ The pre-trained YOLO-NAS weights from DeciAI are subject to their license and can't be used commercially. For more information, see: https://docs.deci.ai/super-gradients/latest/LICENSE.YOLONAS.html
+
+ :::
+
+ The input image size in this notebook is set to 320x320. This results in lower CPU usage and faster inference times without impacting performance in most cases due to the way Frigate crops video frames to areas of interest before running detection. The notebook and config can be updated to 640x640 if desired.
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU`. Then on the same page, in the **Custom Model** tab, configure:
+
+ | Field | Value |
+ | ---------------------------------------- | ------------------------------------------------- |
+ | **Custom object detector model path** | `/config/yolo_nas_s.onnx` |
+ | **Label map for custom object detector** | `/labelmap/coco-80.txt` |
+ | **Object detection model input width** | `320` (should match whatever was set in notebook) |
+ | **Object detection model input height** | `320` (should match whatever was set in notebook) |
+ | **Model Input Pixel Color Format** | `bgr` |
+ | **Model Input Tensor Shape** | `nchw` |
+ | **Model Input D Type** | `int` (Frigate's default value) |
+ | **Object Detection Model Type** | `yolonas` |
+ yaml: |-
+ detectors:
+ ov:
+ type: openvino
+ device: GPU
+
+ model:
+ model_type: yolonas
+ width: 320 # <--- should match whatever was set in notebook
+ height: 320 # <--- should match whatever was set in notebook
+ input_tensor: nchw
+ input_pixel_format: bgr
+ path: /config/yolo_nas_s.onnx
+ labelmap_path: /labelmap/coco-80.txt
+ - key: yolox
+ label: YOLOX
+ recommended: false
+ download: YOLOx models can be downloaded [from the YOLOx repo](https://github.com/Megvii-BaseDetection/YOLOX/tree/main/demo/ONNXRuntime).
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU`. Then on the same page, in the **Custom Model** tab, configure:
+
+ | Field | Value |
+ | ------------------------------------- | -------------------------------- |
+ | **Custom object detector model path** | `/config/yolox.onnx` (use the filename you generated above) |
+ | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) |
+ | **Model Input Tensor Shape** | `nhwc` (Frigate's default value) |
+ | **Model Input D Type** | `int` (Frigate's default value) |
+ | **Object Detection Model Type** | `yolox` |
+ yaml: |-
+ detectors:
+ ov:
+ type: openvino
+ device: GPU
+
+ model:
+ model_type: yolox
+ path: /config/model_cache/yolox.onnx # use the filename you generated above
+ labelmap_path: /labelmap/coco-80.txt
+ - key: rfdetr
+ label: RF-DETR
+ recommended: false
+ download: |-
+ RF-DETR can be exported as ONNX by running the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=Nano` in the first line to `Nano`, `Small`, or `Medium` size.
+
+ ```sh
+ docker build . --build-arg MODEL_SIZE=Nano --rm --output . -f- <<'EOF'
+ FROM python:3.12 AS build
+ RUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/*
+ COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/
+ WORKDIR /rfdetr
+ RUN uv pip install --system rfdetr[onnxexport] torch==2.8.0 onnx==1.19.1 transformers==4.57.6 onnxscript
+ ARG MODEL_SIZE
+ RUN python3 -c "from rfdetr import RFDETR${MODEL_SIZE}; x = RFDETR${MODEL_SIZE}(resolution=320); x.export(simplify=True)"
+ FROM scratch
+ ARG MODEL_SIZE
+ COPY --from=build /rfdetr/output/inference_model.onnx /rfdetr-${MODEL_SIZE}.onnx
+ EOF
+ ```
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU`. Then on the same page, in the **Custom Model** tab, configure:
+
+ | Field | Value |
+ | --------------------------------------- | --------------------------------- |
+ | **Custom object detector model path** | `/config/model_cache/rfdetr.onnx` (use the filename you generated above) |
+ | **Object detection model input width** | `320` |
+ | **Object detection model input height** | `320` |
+ | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) |
+ | **Model Input Tensor Shape** | `nchw` |
+ | **Model Input D Type** | `float` |
+ | **Object Detection Model Type** | `rfdetr` |
+ yaml: |-
+ detectors:
+ ov:
+ type: openvino
+ device: GPU
+
+ model:
+ model_type: rfdetr
+ width: 320
+ height: 320
+ input_tensor: nchw
+ input_dtype: float
+ path: /config/model_cache/rfdetr.onnx # use the filename you generated above
+ - key: dfine
+ label: D-FINE / DEIMv2
+ recommended: false
+ download: |-
+ #### D-FINE
+
+ D-FINE can be exported as ONNX by running the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=s` in the first line to `s`, `m`, or `l` size.
+
+ ```sh
+ docker build . --build-arg MODEL_SIZE=s --output . -f- <<'EOF'
+ FROM python:3.11 AS build
+ RUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/*
+ COPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/
+ WORKDIR /dfine
+ RUN git clone https://github.com/Peterande/D-FINE.git .
+ RUN uv pip install --system -r requirements.txt
+ RUN uv pip install --system onnx onnxruntime onnxsim onnxscript
+ # Create output directory and download checkpoint
+ RUN mkdir -p output
+ ARG MODEL_SIZE
+ RUN wget https://github.com/Peterande/storage/releases/download/dfinev1.0/dfine_${MODEL_SIZE}_obj2coco.pth -O output/dfine_${MODEL_SIZE}_obj2coco.pth
+ # Modify line 58 of export_onnx.py to change batch size to 1
+ RUN sed -i '58s/data = torch.rand(.*)/data = torch.rand(1, 3, 640, 640)/' tools/deployment/export_onnx.py
+ RUN python3 tools/deployment/export_onnx.py -c configs/dfine/objects365/dfine_hgnetv2_${MODEL_SIZE}_obj2coco.yml -r output/dfine_${MODEL_SIZE}_obj2coco.pth
+ FROM scratch
+ ARG MODEL_SIZE
+ COPY --from=build /dfine/output/dfine_${MODEL_SIZE}_obj2coco.onnx /dfine-${MODEL_SIZE}.onnx
+ EOF
+ ```
+
+ #### DEIMv2
+
+ [DEIMv2](https://github.com/Intellindust-AI-Lab/DEIMv2) can be exported as ONNX by running the command below. Pretrained weights are available on Hugging Face for two backbone families:
+
+ - **HGNetv2** (smaller/faster): `atto`, `femto`, `pico`, `n`
+ - **DINOv3** (larger/more accurate): `s`, `m`, `l`, `x`
+
+ Set `BACKBONE` and `MODEL_SIZE` in the first line to match your desired variant. Hugging Face model names use uppercase (e.g. `HGNetv2_N`, `DINOv3_S`), while config files use lowercase (e.g. `hgnetv2_n`, `dinov3_s`).
+
+ ```sh
+ docker build . --rm --build-arg BACKBONE=hgnetv2 --build-arg MODEL_SIZE=n --output . -f- <<'EOF'
+ FROM python:3.11-slim AS build
+ RUN apt-get update && apt-get install --no-install-recommends -y git libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/*
+ COPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/
+ WORKDIR /deimv2
+ RUN git clone https://github.com/Intellindust-AI-Lab/DEIMv2.git .
+ # Install CPU-only PyTorch first to avoid pulling CUDA variant
+ RUN uv pip install --no-cache --system torch torchvision --index-url https://download.pytorch.org/whl/cpu
+ RUN uv pip install --no-cache --system -r requirements.txt
+ RUN uv pip install --no-cache --system onnx safetensors huggingface_hub
+ RUN mkdir -p output
+ ARG BACKBONE
+ ARG MODEL_SIZE
+ # Download from Hugging Face and convert safetensors to pth
+ RUN python3 -c "\
+ from huggingface_hub import hf_hub_download; \
+ from safetensors.torch import load_file; \
+ import torch; \
+ backbone = '${BACKBONE}'.replace('hgnetv2','HGNetv2').replace('dinov3','DINOv3'); \
+ size = '${MODEL_SIZE}'.upper(); \
+ st = load_file(hf_hub_download('Intellindust/DEIMv2_' + backbone + '_' + size + '_COCO', 'model.safetensors')); \
+ torch.save({'model': st}, 'output/deimv2.pth')"
+ RUN sed -i "s/data = torch.rand(2/data = torch.rand(1/" tools/deployment/export_onnx.py
+ # HuggingFace safetensors omits frozen constants that the model constructor initializes
+ RUN sed -i "s/cfg.model.load_state_dict(state)/cfg.model.load_state_dict(state, strict=False)/" tools/deployment/export_onnx.py
+ RUN python3 tools/deployment/export_onnx.py -c configs/deimv2/deimv2_${BACKBONE}_${MODEL_SIZE}_coco.yml -r output/deimv2.pth
+ FROM scratch
+ ARG BACKBONE
+ ARG MODEL_SIZE
+ COPY --from=build /deimv2/output/deimv2.onnx /deimv2_${BACKBONE}_${MODEL_SIZE}.onnx
+ EOF
+ ```
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `CPU`. Then on the same page, in the **Custom Model** tab, configure:
+
+ | Field | Value |
+ | ---------------------------------------- | ---------------------------------- |
+ | **Custom object detector model path** | `/config/model_cache/dfine-s.onnx` (use the filename you generated above) |
+ | **Label map for custom object detector** | `/labelmap/coco-80.txt` |
+ | **Object detection model input width** | `640` |
+ | **Object detection model input height** | `640` |
+ | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) |
+ | **Model Input Tensor Shape** | `nchw` |
+ | **Model Input D Type** | `float` |
+ | **Object Detection Model Type** | `dfine` |
+ yaml: |-
+ detectors:
+ ov:
+ type: openvino
+ device: CPU
+
+ model:
+ model_type: dfine
+ width: 640
+ height: 640
+ input_tensor: nchw
+ input_dtype: float
+ path: /config/model_cache/dfine-s.onnx # use the filename you generated above
+ labelmap_path: /labelmap/coco-80.txt
+appleSilicon:
+ title: Apple Silicon
+ models:
+ - key: yolov9
+ label: YOLOv9
+ recommended: true
+ download: |-
+ YOLOv9 model can be exported as ONNX using the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=t` and `IMG_SIZE=320` in the first line to the [model size](https://github.com/WongKinYiu/yolov9#performance) you would like to convert (available model sizes are `t`, `s`, `m`, `c`, and `e`, common image sizes are `320` and `640`).
+
+ ```sh
+ docker build . --build-arg MODEL_SIZE=t --build-arg IMG_SIZE=320 --output . -f- <<'EOF'
+ FROM python:3.11 AS build
+ RUN apt-get update && apt-get install --no-install-recommends -y cmake libgl1 && rm -rf /var/lib/apt/lists/*
+ COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/
+ WORKDIR /yolov9
+ ADD https://github.com/WongKinYiu/yolov9.git .
+ RUN uv pip install --system -r requirements.txt
+ RUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier==0.4.* onnxscript
+ ARG MODEL_SIZE
+ ARG IMG_SIZE
+ ADD https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-${MODEL_SIZE}-converted.pt yolov9-${MODEL_SIZE}.pt
+ RUN sed -i "s/ckpt = torch.load(attempt_download(w), map_location='cpu')/ckpt = torch.load(attempt_download(w), map_location='cpu', weights_only=False)/g" models/experimental.py
+ RUN python3 export.py --weights ./yolov9-${MODEL_SIZE}.pt --imgsz ${IMG_SIZE} --simplify --include onnx
+ FROM scratch
+ ARG MODEL_SIZE
+ ARG IMG_SIZE
+ COPY --from=build /yolov9/yolov9-${MODEL_SIZE}.onnx /yolov9-${MODEL_SIZE}-${IMG_SIZE}.onnx
+ EOF
+ ```
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **ZMQ IPC** from the detector type dropdown and click **Add**, then set the endpoint to `tcp://host.docker.internal:5555`. Then on the same page, in the **Custom Model** tab, configure:
+
+ | Field | Value |
+ | ---------------------------------------- | -------------------------------------------------------- |
+ | **Custom object detector model path** | `/config/model_cache/yolo.onnx` (use the filename you generated above) |
+ | **Label map for custom object detector** | `/labelmap/coco-80.txt` |
+ | **Object detection model input width** | `320` (should match the imgsize set during model export) |
+ | **Object detection model input height** | `320` (should match the imgsize set during model export) |
+ | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) |
+ | **Model Input Tensor Shape** | `nchw` |
+ | **Model Input D Type** | `float` |
+ | **Object Detection Model Type** | `yolo-generic` |
+ yaml: |-
+ detectors:
+ apple-silicon:
+ type: zmq
+ endpoint: tcp://host.docker.internal:5555
+
+ model:
+ model_type: yolo-generic
+ width: 320 # <--- should match the imgsize set during model export
+ height: 320 # <--- should match the imgsize set during model export
+ input_tensor: nchw
+ input_dtype: float
+ path: /config/model_cache/yolo.onnx # use the filename you generated above
+ labelmap_path: /labelmap/coco-80.txt
+ - key: yolo-legacy
+ label: YOLO (v3, v4, v7)
+ recommended: false
+ download: |-
+ To export as ONNX:
+
+ ```sh
+ git clone https://github.com/NateMeyer/tensorrt_demos
+ cd tensorrt_demos/yolo
+ ./download_yolo.sh
+ python3 yolo_to_onnx.py -m yolov7-320
+ ```
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **ZMQ IPC** from the detector type dropdown and click **Add**, then set the endpoint to `tcp://host.docker.internal:5555`. Then on the same page, in the **Custom Model** tab, configure:
+
+ | Field | Value |
+ | ---------------------------------------- | -------------------------------------------------------- |
+ | **Custom object detector model path** | `/config/model_cache/yolo.onnx` (use the filename you generated above) |
+ | **Label map for custom object detector** | `/labelmap/coco-80.txt` |
+ | **Object detection model input width** | `320` (should match the imgsize set during model export) |
+ | **Object detection model input height** | `320` (should match the imgsize set during model export) |
+ | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) |
+ | **Model Input Tensor Shape** | `nchw` |
+ | **Model Input D Type** | `float` |
+ | **Object Detection Model Type** | `yolo-generic` |
+ yaml: |-
+ detectors:
+ apple-silicon:
+ type: zmq
+ endpoint: tcp://host.docker.internal:5555
+
+ model:
+ model_type: yolo-generic
+ width: 320 # <--- should match the imgsize set during model export
+ height: 320 # <--- should match the imgsize set during model export
+ input_tensor: nchw
+ input_dtype: float
+ path: /config/model_cache/yolo.onnx # use the filename you generated above
+ labelmap_path: /labelmap/coco-80.txt
+onnx:
+ title: ONNX
+ models:
+ - key: yolov9
+ label: YOLOv9
+ recommended: true
+ download: |-
+ YOLOv9 model can be exported as ONNX using the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=t` and `IMG_SIZE=320` in the first line to the [model size](https://github.com/WongKinYiu/yolov9#performance) you would like to convert (available model sizes are `t`, `s`, `m`, `c`, and `e`, common image sizes are `320` and `640`).
+
+ ```sh
+ docker build . --build-arg MODEL_SIZE=t --build-arg IMG_SIZE=320 --output . -f- <<'EOF'
+ FROM python:3.11 AS build
+ RUN apt-get update && apt-get install --no-install-recommends -y cmake libgl1 && rm -rf /var/lib/apt/lists/*
+ COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/
+ WORKDIR /yolov9
+ ADD https://github.com/WongKinYiu/yolov9.git .
+ RUN uv pip install --system -r requirements.txt
+ RUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier==0.4.* onnxscript
+ ARG MODEL_SIZE
+ ARG IMG_SIZE
+ ADD https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-${MODEL_SIZE}-converted.pt yolov9-${MODEL_SIZE}.pt
+ RUN sed -i "s/ckpt = torch.load(attempt_download(w), map_location='cpu')/ckpt = torch.load(attempt_download(w), map_location='cpu', weights_only=False)/g" models/experimental.py
+ RUN python3 export.py --weights ./yolov9-${MODEL_SIZE}.pt --imgsz ${IMG_SIZE} --simplify --include onnx
+ FROM scratch
+ ARG MODEL_SIZE
+ ARG IMG_SIZE
+ COPY --from=build /yolov9/yolov9-${MODEL_SIZE}.onnx /yolov9-${MODEL_SIZE}-${IMG_SIZE}.onnx
+ EOF
+ ```
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:
+
+ | Field | Value |
+ | ---------------------------------------- | -------------------------------------------------------- |
+ | **Custom object detector model path** | `/config/model_cache/yolo.onnx` (use the filename you generated above) |
+ | **Label map for custom object detector** | `/labelmap/coco-80.txt` |
+ | **Object detection model input width** | `320` (should match the imgsize set during model export) |
+ | **Object detection model input height** | `320` (should match the imgsize set during model export) |
+ | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) |
+ | **Model Input Tensor Shape** | `nchw` |
+ | **Model Input D Type** | `float` |
+ | **Object Detection Model Type** | `yolo-generic` |
+ yaml: |-
+ detectors:
+ onnx:
+ type: onnx
+
+ model:
+ model_type: yolo-generic
+ width: 320 # <--- should match the imgsize set during model export
+ height: 320 # <--- should match the imgsize set during model export
+ input_tensor: nchw
+ input_dtype: float
+ path: /config/model_cache/yolo.onnx # use the filename you generated above
+ labelmap_path: /labelmap/coco-80.txt
+ - key: rfdetr
+ label: RF-DETR
+ recommended: false
+ download: |-
+ RF-DETR can be exported as ONNX by running the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=Nano` in the first line to `Nano`, `Small`, or `Medium` size.
+
+ ```sh
+ docker build . --build-arg MODEL_SIZE=Nano --rm --output . -f- <<'EOF'
+ FROM python:3.12 AS build
+ RUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/*
+ COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/
+ WORKDIR /rfdetr
+ RUN uv pip install --system rfdetr[onnxexport] torch==2.8.0 onnx==1.19.1 transformers==4.57.6 onnxscript
+ ARG MODEL_SIZE
+ RUN python3 -c "from rfdetr import RFDETR${MODEL_SIZE}; x = RFDETR${MODEL_SIZE}(resolution=320); x.export(simplify=True)"
+ FROM scratch
+ ARG MODEL_SIZE
+ COPY --from=build /rfdetr/output/inference_model.onnx /rfdetr-${MODEL_SIZE}.onnx
+ EOF
+ ```
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:
+
+ | Field | Value |
+ | --------------------------------------- | --------------------------------- |
+ | **Custom object detector model path** | `/config/model_cache/rfdetr.onnx` (use the filename you generated above) |
+ | **Object detection model input width** | `320` |
+ | **Object detection model input height** | `320` |
+ | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) |
+ | **Model Input Tensor Shape** | `nchw` |
+ | **Model Input D Type** | `float` |
+ | **Object Detection Model Type** | `rfdetr` |
+ yaml: |-
+ detectors:
+ onnx:
+ type: onnx
+
+ model:
+ model_type: rfdetr
+ width: 320
+ height: 320
+ input_tensor: nchw
+ input_dtype: float
+ path: /config/model_cache/rfdetr.onnx # use the filename you generated above
+ - key: yolonas
+ label: YOLO-NAS
+ recommended: false
+ download: |-
+ You can build and download a compatible model with pre-trained weights using [this notebook](https://github.com/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb) [](https://colab.research.google.com/github/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb) which can be run directly in [Google Colab](https://colab.research.google.com/github/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb).
+
+ :::warning
+
+ The pre-trained YOLO-NAS weights from DeciAI are subject to their license and can't be used commercially. For more information, see: https://docs.deci.ai/super-gradients/latest/LICENSE.YOLONAS.html
+
+ :::
+
+ The input image size in this notebook is set to 320x320. This results in lower CPU usage and faster inference times without impacting performance in most cases due to the way Frigate crops video frames to areas of interest before running detection. The notebook and config can be updated to 640x640 if desired.
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:
+
+ | Field | Value |
+ | ---------------------------------------- | ------------------------------------------------- |
+ | **Custom object detector model path** | `/config/yolo_nas_s.onnx` |
+ | **Label map for custom object detector** | `/labelmap/coco-80.txt` |
+ | **Object detection model input width** | `320` (should match whatever was set in notebook) |
+ | **Object detection model input height** | `320` (should match whatever was set in notebook) |
+ | **Model Input Pixel Color Format** | `bgr` |
+ | **Model Input Tensor Shape** | `nchw` |
+ | **Model Input D Type** | `int` (Frigate's default value) |
+ | **Object Detection Model Type** | `yolonas` |
+ yaml: |-
+ detectors:
+ onnx:
+ type: onnx
+
+ model:
+ model_type: yolonas
+ width: 320 # <--- should match whatever was set in notebook
+ height: 320 # <--- should match whatever was set in notebook
+ input_pixel_format: bgr
+ input_tensor: nchw
+ path: /config/yolo_nas_s.onnx
+ labelmap_path: /labelmap/coco-80.txt
+ - key: yolox
+ label: YOLOX
+ recommended: false
+ download: YOLOx models can be downloaded [from the YOLOx repo](https://github.com/Megvii-BaseDetection/YOLOX/tree/main/demo/ONNXRuntime).
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:
+
+ | Field | Value |
+ | ---------------------------------------- | -------------------------------------------------------- |
+ | **Custom object detector model path** | `/config/model_cache/yolox_tiny.onnx` (use the filename you generated above) |
+ | **Label map for custom object detector** | `/labelmap/coco-80.txt` |
+ | **Object detection model input width** | `416` (should match the imgsize set during model export) |
+ | **Object detection model input height** | `416` (should match the imgsize set during model export) |
+ | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) |
+ | **Model Input Tensor Shape** | `nchw` |
+ | **Model Input D Type** | `float_denorm` |
+ | **Object Detection Model Type** | `yolox` |
+ yaml: |-
+ detectors:
+ onnx:
+ type: onnx
+
+ model:
+ model_type: yolox
+ width: 416 # <--- should match the imgsize set during model export
+ height: 416 # <--- should match the imgsize set during model export
+ input_tensor: nchw
+ input_dtype: float_denorm
+ path: /config/model_cache/yolox_tiny.onnx # use the filename you generated above
+ labelmap_path: /labelmap/coco-80.txt
+ - key: dfine
+ label: D-FINE / DEIMv2
+ recommended: false
+ download: |-
+ #### Downloading D-FINE Model
+
+ D-FINE can be exported as ONNX by running the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=s` in the first line to `s`, `m`, or `l` size.
+
+ ```sh
+ docker build . --build-arg MODEL_SIZE=s --output . -f- <<'EOF'
+ FROM python:3.11 AS build
+ RUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/*
+ COPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/
+ WORKDIR /dfine
+ RUN git clone https://github.com/Peterande/D-FINE.git .
+ RUN uv pip install --system -r requirements.txt
+ RUN uv pip install --system onnx onnxruntime onnxsim onnxscript
+ # Create output directory and download checkpoint
+ RUN mkdir -p output
+ ARG MODEL_SIZE
+ RUN wget https://github.com/Peterande/storage/releases/download/dfinev1.0/dfine_${MODEL_SIZE}_obj2coco.pth -O output/dfine_${MODEL_SIZE}_obj2coco.pth
+ # Modify line 58 of export_onnx.py to change batch size to 1
+ RUN sed -i '58s/data = torch.rand(.*)/data = torch.rand(1, 3, 640, 640)/' tools/deployment/export_onnx.py
+ RUN python3 tools/deployment/export_onnx.py -c configs/dfine/objects365/dfine_hgnetv2_${MODEL_SIZE}_obj2coco.yml -r output/dfine_${MODEL_SIZE}_obj2coco.pth
+ FROM scratch
+ ARG MODEL_SIZE
+ COPY --from=build /dfine/output/dfine_${MODEL_SIZE}_obj2coco.onnx /dfine-${MODEL_SIZE}.onnx
+ EOF
+ ```
+
+ #### Downloading DEIMv2 Model
+
+ [DEIMv2](https://github.com/Intellindust-AI-Lab/DEIMv2) can be exported as ONNX by running the command below. Pretrained weights are available on Hugging Face for two backbone families:
+
+ - **HGNetv2** (smaller/faster): `atto`, `femto`, `pico`, `n`
+ - **DINOv3** (larger/more accurate): `s`, `m`, `l`, `x`
+
+ Set `BACKBONE` and `MODEL_SIZE` in the first line to match your desired variant. Hugging Face model names use uppercase (e.g. `HGNetv2_N`, `DINOv3_S`), while config files use lowercase (e.g. `hgnetv2_n`, `dinov3_s`).
+
+ ```sh
+ docker build . --rm --build-arg BACKBONE=hgnetv2 --build-arg MODEL_SIZE=n --output . -f- <<'EOF'
+ FROM python:3.11-slim AS build
+ RUN apt-get update && apt-get install --no-install-recommends -y git libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/*
+ COPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/
+ WORKDIR /deimv2
+ RUN git clone https://github.com/Intellindust-AI-Lab/DEIMv2.git .
+ # Install CPU-only PyTorch first to avoid pulling CUDA variant
+ RUN uv pip install --no-cache --system torch torchvision --index-url https://download.pytorch.org/whl/cpu
+ RUN uv pip install --no-cache --system -r requirements.txt
+ RUN uv pip install --no-cache --system onnx safetensors huggingface_hub
+ RUN mkdir -p output
+ ARG BACKBONE
+ ARG MODEL_SIZE
+ # Download from Hugging Face and convert safetensors to pth
+ RUN python3 -c "\
+ from huggingface_hub import hf_hub_download; \
+ from safetensors.torch import load_file; \
+ import torch; \
+ backbone = '${BACKBONE}'.replace('hgnetv2','HGNetv2').replace('dinov3','DINOv3'); \
+ size = '${MODEL_SIZE}'.upper(); \
+ st = load_file(hf_hub_download('Intellindust/DEIMv2_' + backbone + '_' + size + '_COCO', 'model.safetensors')); \
+ torch.save({'model': st}, 'output/deimv2.pth')"
+ RUN sed -i "s/data = torch.rand(2/data = torch.rand(1/" tools/deployment/export_onnx.py
+ # HuggingFace safetensors omits frozen constants that the model constructor initializes
+ RUN sed -i "s/cfg.model.load_state_dict(state)/cfg.model.load_state_dict(state, strict=False)/" tools/deployment/export_onnx.py
+ RUN python3 tools/deployment/export_onnx.py -c configs/deimv2/deimv2_${BACKBONE}_${MODEL_SIZE}_coco.yml -r output/deimv2.pth
+ FROM scratch
+ ARG BACKBONE
+ ARG MODEL_SIZE
+ COPY --from=build /deimv2/output/deimv2.onnx /deimv2_${BACKBONE}_${MODEL_SIZE}.onnx
+ EOF
+ ```
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:
+
+ | Field | Value |
+ | ---------------------------------------- | ------------------------------------------- |
+ | **Custom object detector model path** | `/config/model_cache/dfine_m_obj2coco.onnx` (use the filename you generated above) |
+ | **Label map for custom object detector** | `/labelmap/coco-80.txt` |
+ | **Object detection model input width** | `640` |
+ | **Object detection model input height** | `640` |
+ | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) |
+ | **Model Input Tensor Shape** | `nchw` |
+ | **Model Input D Type** | `float` |
+ | **Object Detection Model Type** | `dfine` |
+ yaml: |-
+ detectors:
+ onnx:
+ type: onnx
+
+ model:
+ model_type: dfine
+ width: 640
+ height: 640
+ input_tensor: nchw
+ input_dtype: float
+ path: /config/model_cache/dfine_m_obj2coco.onnx # use the filename you generated above
+ labelmap_path: /labelmap/coco-80.txt
+ - key: yolo-legacy
+ label: YOLO (v3, v4, v7)
+ recommended: false
+ download: |-
+ To export as ONNX:
+
+ ```sh
+ git clone https://github.com/NateMeyer/tensorrt_demos
+ cd tensorrt_demos/yolo
+ ./download_yolo.sh
+ python3 yolo_to_onnx.py -m yolov7-320
+ ```
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:
+
+ | Field | Value |
+ | ---------------------------------------- | -------------------------------------------------------- |
+ | **Custom object detector model path** | `/config/model_cache/yolo.onnx` (use the filename you generated above) |
+ | **Label map for custom object detector** | `/labelmap/coco-80.txt` |
+ | **Object detection model input width** | `320` (should match the imgsize set during model export) |
+ | **Object detection model input height** | `320` (should match the imgsize set during model export) |
+ | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) |
+ | **Model Input Tensor Shape** | `nchw` |
+ | **Model Input D Type** | `float` |
+ | **Object Detection Model Type** | `yolo-generic` |
+ yaml: |-
+ detectors:
+ onnx:
+ type: onnx
+
+ model:
+ model_type: yolo-generic
+ width: 320 # <--- should match the imgsize set during model export
+ height: 320 # <--- should match the imgsize set during model export
+ input_tensor: nchw
+ input_dtype: float
+ path: /config/model_cache/yolo.onnx # use the filename you generated above
+ labelmap_path: /labelmap/coco-80.txt
+cpu:
+ title: CPU
+ models:
+ - key: ssd
+ label: MobileNet v2
+ recommended: true
+ download: A TensorFlow Lite model is provided in the container at `/cpu_model.tflite` and is used by this detector type by default. To provide your own model, bind mount the file into the container and provide the path with `model.path`.
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **CPU** from the detector type dropdown and click **Add**. Configure the number of threads and click **Add** again to add additional CPU detectors as needed (one per camera is recommended).
+
+ | Field | Value |
+ | ----------------- | ----- |
+ | **Detector type** | `cpu` |
+ | **Num threads** | `3` |
+ yaml: |-
+ detectors:
+ cpu1:
+ type: cpu
+ num_threads: 3
+deepstack:
+ title: DeepStack / CodeProject.AI
+ models:
+ - key: yolo
+ label: YOLO
+ recommended: true
+ download: This detector runs object detection over the network against a CodeProject.AI or DeepStack server, so no model is downloaded into Frigate itself. Visit the [CodeProject.AI official website](https://www.codeproject.com/Articles/5322557/CodeProject-AI-Server-AI-the-easy-way) to download and install the AI server on your preferred device (e.g. Raspberry Pi, Nvidia Jetson, or other compatible hardware) before configuring the detector.
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **DeepStack** from the detector type dropdown and click **Add**. Set the API URL to point to your CodeProject.AI server (e.g., `http://:/v1/vision/detection`).
+
+ | Field | Value |
+ | ------------- | ---------------------------------------------------------------------- |
+ | **API URL** | `http://:/v1/vision/detection` |
+ | **API Timeout** | `0.1` (seconds) |
+ yaml: |-
+ detectors:
+ deepstack:
+ api_url: http://:/v1/vision/detection
+ type: deepstack
+ api_timeout: 0.1 # seconds
+memryx:
+ title: MemryX
+ models:
+ - key: yolonas
+ label: YOLO-NAS
+ recommended: true
+ download: |-
+ The [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) model included in this detector is downloaded automatically and compiled to DFP with [mx_nc](https://developer.memryx.com/2p1/tools/neural_compiler.html#usage).
+
+ **Note:** The default model for the MemryX detector is YOLO-NAS 320x320.
+
+ The input size for **YOLO-NAS** can be set to either **320x320** (default) or **640x640**.
+
+ - The default size of **320x320** is optimized for lower CPU usage and faster inference times.
+
+ MemryX `.dfp` models are automatically downloaded at runtime, if enabled, to the container at `/memryx_models/model_folder/`.
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then on the same page, in the **Custom Model** tab, configure:
+
+ | Field | Value |
+ | ---------------------------------------- | ------------------------------------------------- |
+ | **Label map for custom object detector** | `/labelmap/coco-80.txt` |
+ | **Object detection model input width** | `320` (can be set to `640` for higher resolution) |
+ | **Object detection model input height** | `320` (can be set to `640` for higher resolution) |
+ | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) |
+ | **Model Input Tensor Shape** | `nchw` |
+ | **Model Input D Type** | `float` |
+ | **Object Detection Model Type** | `yolonas` |
+ yaml: |-
+ detectors:
+ memx0:
+ type: memryx
+ device: PCIe:0
+
+ model:
+ model_type: yolonas
+ width: 320 # (Can be set to 640 for higher resolution)
+ height: 320 # (Can be set to 640 for higher resolution)
+ input_tensor: nchw
+ input_dtype: float
+ labelmap_path: /labelmap/coco-80.txt
+ # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model.
+ # path: /config/yolonas.zip
+ # The .zip file must contain:
+ # ├── yolonas.dfp (a file ending with .dfp)
+ # └── yolonas_post.onnx (optional; only if the model includes a cropped post-processing network)
+ - key: yolov9
+ label: YOLOv9
+ recommended: false
+ download: |-
+ The YOLOv9s model included in this detector is downloaded from [the original GitHub](https://github.com/WongKinYiu/yolov9) and compiled to DFP with [mx_nc](https://developer.memryx.com/2p1/tools/neural_compiler.html#usage).
+
+ MemryX `.dfp` models are automatically downloaded at runtime, if enabled, to the container at `/memryx_models/model_folder/`.
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then on the same page, in the **Custom Model** tab, configure:
+
+ | Field | Value |
+ | ---------------------------------------- | ------------------------------------------------- |
+ | **Label map for custom object detector** | `/labelmap/coco-80.txt` |
+ | **Object detection model input width** | `320` (can be set to `640` for higher resolution) |
+ | **Object detection model input height** | `320` (can be set to `640` for higher resolution) |
+ | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) |
+ | **Model Input Tensor Shape** | `nchw` |
+ | **Model Input D Type** | `float` |
+ | **Object Detection Model Type** | `yolo-generic` |
+ yaml: |-
+ detectors:
+ memx0:
+ type: memryx
+ device: PCIe:0
+
+ model:
+ model_type: yolo-generic
+ width: 320 # (Can be set to 640 for higher resolution)
+ height: 320 # (Can be set to 640 for higher resolution)
+ input_tensor: nchw
+ input_dtype: float
+ labelmap_path: /labelmap/coco-80.txt
+ # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model.
+ # path: /config/yolov9.zip
+ # The .zip file must contain:
+ # ├── yolov9.dfp (a file ending with .dfp)
+ - key: yolox
+ label: YOLOX
+ recommended: false
+ download: |-
+ The model is sourced from the [OpenCV Model Zoo](https://github.com/opencv/opencv_zoo) and precompiled to DFP.
+
+ MemryX `.dfp` models are automatically downloaded at runtime, if enabled, to the container at `/memryx_models/model_folder/`.
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then on the same page, in the **Custom Model** tab, configure:
+
+ | Field | Value |
+ | ---------------------------------------- | ----------------------- |
+ | **Label map for custom object detector** | `/labelmap/coco-80.txt` |
+ | **Object detection model input width** | `640` |
+ | **Object detection model input height** | `640` |
+ | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) |
+ | **Model Input Tensor Shape** | `nchw` |
+ | **Model Input D Type** | `float_denorm` |
+ | **Object Detection Model Type** | `yolox` |
+ yaml: |-
+ detectors:
+ memx0:
+ type: memryx
+ device: PCIe:0
+
+ model:
+ model_type: yolox
+ width: 640
+ height: 640
+ input_tensor: nchw
+ input_dtype: float_denorm
+ labelmap_path: /labelmap/coco-80.txt
+ # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model.
+ # path: /config/yolox.zip
+ # The .zip file must contain:
+ # ├── yolox.dfp (a file ending with .dfp)
+ - key: ssd
+ label: SSDLite MobileNet v2
+ recommended: false
+ download: |-
+ The model is sourced from the [OpenMMLab Model Zoo](https://mmdeploy-oss.openmmlab.com/model/mmdet-det/ssdlite-e8679f.onnx) and has been converted to DFP.
+
+ MemryX `.dfp` models are automatically downloaded at runtime, if enabled, to the container at `/memryx_models/model_folder/`.
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then on the same page, in the **Custom Model** tab, configure:
+
+ | Field | Value |
+ | ---------------------------------------- | ----------------------- |
+ | **Label map for custom object detector** | `/labelmap/coco-80.txt` |
+ | **Object detection model input width** | `320` |
+ | **Object detection model input height** | `320` |
+ | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) |
+ | **Model Input Tensor Shape** | `nchw` |
+ | **Model Input D Type** | `float` |
+ | **Object Detection Model Type** | `ssd` |
+ yaml: |-
+ detectors:
+ memx0:
+ type: memryx
+ device: PCIe:0
+
+ model:
+ model_type: ssd
+ width: 320
+ height: 320
+ input_tensor: nchw
+ input_dtype: float
+ labelmap_path: /labelmap/coco-80.txt
+ # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model.
+ # path: /config/ssdlite_mobilenet.zip
+ # The .zip file must contain:
+ # ├── ssdlite_mobilenet.dfp (a file ending with .dfp)
+ # └── ssdlite_mobilenet_post.onnx (optional; only if the model includes a cropped post-processing network)
+tensorrt:
+ title: TensorRT
+ models:
+ - key: yolo-legacy
+ label: YOLO (v3, v4, v7)
+ recommended: true
+ download: |-
+ The model used for TensorRT must be preprocessed on the same hardware platform that it will run on, so Frigate generates the `.trt` model file on-device at startup. Processed models are stored in the `/config/model_cache` folder.
+
+ By default no models are generated. Set the `YOLO_MODELS` environment variable in Docker to one or more comma-separated model names (from the available `yolov3`/`yolov4`/`yolov7` models) and each one will be generated on startup if the corresponding `{model}.trt` file is not already present in `model_cache` (delete it to force regeneration). On Jetson devices with DLAs (Xavier or Orin), append `-dla` to a model name to generate a DLA model. If your GPU does not support FP16 operations, pass `USE_FP16=False` to disable it.
+
+ An example `docker-compose.yml` fragment that converts the `yolov7-320` and `yolov7x-640` models:
+
+ ```yml
+ frigate:
+ environment:
+ - YOLO_MODELS=yolov7-320,yolov7x-640
+ - USE_FP16=false
+ ```
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **TensorRT** from the detector type dropdown and click **Add**, then set the device to `0` (the default GPU index). Then on the same page, in the **Custom Model** tab, configure:
+
+ | Field | Value |
+ | ---------------------------------------- | ------------------------------------------------------------ |
+ | **Custom object detector model path** | `/config/model_cache/tensorrt/yolov7-320.trt` (use the filename you generated above) |
+ | **Label map for custom object detector** | `/labelmap/coco-80.txt` |
+ | **Object detection model input width** | `320` (MUST match the chosen model, e.g., yolov7-320 -> 320) |
+ | **Object detection model input height** | `320` (MUST match the chosen model, e.g., yolov7-320 -> 320) |
+ | **Model Input Pixel Color Format** | `rgb` |
+ | **Model Input Tensor Shape** | `nchw` |
+ | **Model Input D Type** | `int` (Frigate's default value) |
+ | **Object Detection Model Type** | `ssd` (Frigate's default value) |
+ yaml: |-
+ detectors:
+ tensorrt:
+ type: tensorrt
+ device: 0 #This is the default, select the first GPU
+
+ model:
+ path: /config/model_cache/tensorrt/yolov7-320.trt # use the filename you generated above
+ labelmap_path: /labelmap/coco-80.txt
+ input_tensor: nchw
+ input_pixel_format: rgb
+ width: 320 # MUST match the chosen model i.e yolov7-320 -> 320, yolov4-416 -> 416
+ height: 320 # MUST match the chosen model i.e yolov7-320 -> 320 yolov4-416 -> 416
+synaptics:
+ title: Synaptics
+ models:
+ - key: ssd
+ label: SSD MobileNet
+ recommended: true
+ download: A synap model is provided in the container at `/mobilenet.synap` and is used by this detector type by default. The model comes from the [Synap-release Github](https://github.com/synaptics-astra/synap-release/tree/v1.5.0/models/dolphin/object_detection/coco/model/mobilenet224_full80).
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **Synaptics** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:
+
+ | Field | Value |
+ | ---------------------------------------- | ---------------------------- |
+ | **Custom object detector model path** | `/synaptics/mobilenet.synap` |
+ | **Label map for custom object detector** | `/labelmap/coco-80.txt` |
+ | **Object detection model input width** | `224` |
+ | **Object detection model input height** | `224` |
+ | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) |
+ | **Model Input Tensor Shape** | `nhwc` |
+ | **Model Input D Type** | `int` (Frigate's default value) |
+ | **Object Detection Model Type** | `ssd` (Frigate's default value) |
+ yaml: |-
+ detectors: # required
+ synap_npu: # required
+ type: synaptics # required
+
+ model: # required
+ path: /synaptics/mobilenet.synap # required
+ width: 224 # required
+ height: 224 # required
+ input_tensor: nhwc # default value (optional. If you change the model, it is required)
+ labelmap_path: /labelmap/coco-80.txt # required
+rknn:
+ title: RKNN
+ models:
+ - key: yolov9
+ label: YOLOv9
+ recommended: true
+ download: |-
+ If no custom model is provided, the RKNN detector downloads a default model from GitHub on first startup. Once cached, the model works fully offline. All models are automatically downloaded and stored in the folder `config/model_cache/rknn_cache`. After upgrading Frigate, you should remove older models to free up space.
+
+ You can also provide your own `.rknn` model. You should not save your own models in the `rknn_cache` folder, store them directly in the `model_cache` folder or another subfolder. To convert a model to `.rknn` format see the `rknn-toolkit2` (requires a x86 machine). Note, that there is only post-processing for the supported models.
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and, in the **Custom Model** tab, configure:
+
+ | Field | Value |
+ | ---------------------------------------- | -------------------------------------------------- |
+ | **Custom object detector model path** | `frigate-fp16-yolov9-t` (or other yolov9 variants) |
+ | **Label map for custom object detector** | `/labelmap/coco-80.txt` |
+ | **Object detection model input width** | `320` |
+ | **Object detection model input height** | `320` |
+ | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) |
+ | **Model Input Tensor Shape** | `nhwc` |
+ | **Model Input D Type** | `int` (Frigate's default value) |
+ | **Object Detection Model Type** | `yolo-generic` |
+ yaml: |-
+ model: # required
+ # name of model (will be automatically downloaded) or path to your own .rknn model file
+ # possible values are:
+ # - frigate-fp16-yolov9-t
+ # - frigate-fp16-yolov9-s
+ # - frigate-fp16-yolov9-m
+ # - frigate-fp16-yolov9-c
+ # - frigate-fp16-yolov9-e
+ # your yolo_model.rknn
+ path: frigate-fp16-yolov9-t
+ model_type: yolo-generic
+ width: 320
+ height: 320
+ input_tensor: nhwc
+ labelmap_path: /labelmap/coco-80.txt
+ - key: yolonas
+ label: YOLO-NAS
+ recommended: false
+ download: |-
+ If no custom model is provided, the RKNN detector downloads a default model from GitHub on first startup. Once cached, the model works fully offline. All models are automatically downloaded and stored in the folder `config/model_cache/rknn_cache`. After upgrading Frigate, you should remove older models to free up space.
+
+ You can also provide your own `.rknn` model. You should not save your own models in the `rknn_cache` folder, store them directly in the `model_cache` folder or another subfolder. To convert a model to `.rknn` format see the `rknn-toolkit2` (requires a x86 machine). Note, that there is only post-processing for the supported models.
+
+ **Note:** The pre-trained YOLO-NAS weights from DeciAI are subject to their license and can't be used commercially. For more information, see: https://docs.deci.ai/super-gradients/latest/LICENSE.YOLONAS.html
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and, in the **Custom Model** tab, configure:
+
+ | Field | Value |
+ | ---------------------------------------- | ----------------------------------------------------------------------- |
+ | **Custom object detector model path** | `deci-fp16-yolonas_s` (or `deci-fp16-yolonas_m`, `deci-fp16-yolonas_l`) |
+ | **Label map for custom object detector** | `/labelmap/coco-80.txt` |
+ | **Object detection model input width** | `320` |
+ | **Object detection model input height** | `320` |
+ | **Model Input Pixel Color Format** | `bgr` |
+ | **Model Input Tensor Shape** | `nhwc` |
+ | **Model Input D Type** | `int` (Frigate's default value) |
+ | **Object Detection Model Type** | `yolonas` |
+ yaml: |-
+ model: # required
+ # name of model (will be automatically downloaded) or path to your own .rknn model file
+ # possible values are:
+ # - deci-fp16-yolonas_s
+ # - deci-fp16-yolonas_m
+ # - deci-fp16-yolonas_l
+ # your yolonas_model.rknn
+ path: deci-fp16-yolonas_s
+ model_type: yolonas
+ width: 320
+ height: 320
+ input_pixel_format: bgr
+ input_tensor: nhwc
+ labelmap_path: /labelmap/coco-80.txt
+ - key: yolox
+ label: YOLOx
+ recommended: false
+ download: |-
+ If no custom model is provided, the RKNN detector downloads a default model from GitHub on first startup. Once cached, the model works fully offline. All models are automatically downloaded and stored in the folder `config/model_cache/rknn_cache`. After upgrading Frigate, you should remove older models to free up space.
+
+ You can also provide your own `.rknn` model. You should not save your own models in the `rknn_cache` folder, store them directly in the `model_cache` folder or another subfolder. To convert a model to `.rknn` format see the `rknn-toolkit2` (requires a x86 machine). Note, that there is only post-processing for the supported models.
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and, in the **Custom Model** tab, configure:
+
+ | Field | Value |
+ | ---------------------------------------- | ---------------------------------------------- |
+ | **Custom object detector model path** | `rock-i8-yolox_nano` (or other yolox variants) |
+ | **Label map for custom object detector** | `/labelmap/coco-80.txt` |
+ | **Object detection model input width** | `416` |
+ | **Object detection model input height** | `416` |
+ | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) |
+ | **Model Input Tensor Shape** | `nhwc` |
+ | **Model Input D Type** | `int` (Frigate's default value) |
+ | **Object Detection Model Type** | `yolox` |
+ yaml: |-
+ model: # required
+ # name of model (will be automatically downloaded) or path to your own .rknn model file
+ # possible values are:
+ # - rock-i8-yolox_nano
+ # - rock-i8-yolox_tiny
+ # - rock-fp16-yolox_nano
+ # - rock-fp16-yolox_tiny
+ # your yolox_model.rknn
+ path: rock-i8-yolox_nano
+ model_type: yolox
+ width: 416
+ height: 416
+ input_tensor: nhwc
+ labelmap_path: /labelmap/coco-80.txt
+axengine:
+ title: AXEngine
+ models:
+ - key: yolov9
+ label: YOLOv9
+ recommended: true
+ download: A yolov9 axmodel is provided in the container at `/axmodels` and is used by this detector type by default. The AXEngine detector downloads its default model from HuggingFace on first startup; once cached, the model works fully offline.
+ ui: |-
+ Navigate to **Settings > System > Detectors and model** and select **AXEngine NPU** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:
+
+ | Field | Value |
+ | ---------------------------------------- | ----------------------- |
+ | **Custom object detector model path** | `frigate-yolov9-tiny` |
+ | **Label map for custom object detector** | `/labelmap/coco-80.txt` |
+ | **Object detection model input width** | `320` |
+ | **Object detection model input height** | `320` |
+ | **Model Input Pixel Color Format** | `bgr` |
+ | **Model Input Tensor Shape** | `nhwc` (Frigate's default value) |
+ | **Model Input D Type** | `int` |
+ | **Object Detection Model Type** | `yolo-generic` |
+ yaml: |-
+ detectors:
+ axengine:
+ type: axengine
+
+ model:
+ path: frigate-yolov9-tiny
+ model_type: yolo-generic
+ width: 320
+ height: 320
+ input_dtype: int
+ input_pixel_format: bgr
+ labelmap_path: /labelmap/coco-80.txt
+degirumAiServer:
+ title: DeGirum AI Server
+ models:
+ - key: ai-server-inference
+ label: AI Server Inference
+ recommended: true
+ download: |-
+ Launch a DeGirum AI server as a Docker container, then point the detector at it. Add this to your `docker-compose.yml`:
+
+ ```yaml
+ degirum_detector:
+ container_name: degirum
+ image: degirum/aiserver:latest
+ privileged: true
+ ports:
+ - "8778:8778"
+ ```
+
+ Set `location` to the server's service name, container name, or `host:port`.
+ ui: |
+ Navigate to **Settings > System > Detectors and model** and select **DeGirum** from the detector type dropdown and click **Add**.
+
+ | Field | Value |
+ | --- | --- |
+ | **Location** | `degirum` |
+ | **Zoo** | `degirum/public` |
+ | **Token** | your AI Hub token (optional for the public zoo) |
+ yaml: |
+ degirum_detector:
+ type: degirum
+ location: degirum
+ zoo: degirum/public
+ token: dg_example_token
+degirumLocal:
+ title: DeGirum Local
+ models:
+ - key: local-inference
+ label: Local Inference
+ recommended: true
+ download: Run hardware directly inside the Frigate container with `@local`, removing the AI server hop. The matching device runtime (e.g. the Hailo runtime) must be installed in the container; confirm it with `degirum sys-info`.
+ ui: |
+ Navigate to **Settings > System > Detectors and model** and select **DeGirum** from the detector type dropdown and click **Add**.
+
+ | Field | Value |
+ | --- | --- |
+ | **Location** | `@local` |
+ | **Zoo** | `degirum/public` |
+ | **Token** | your AI Hub token (optional for the public zoo) |
+ yaml: |
+ degirum_detector:
+ type: degirum
+ location: @local
+ zoo: degirum/public
+ token: dg_example_token
+degirumCloud:
+ title: DeGirum AI Hub Cloud
+ models:
+ - key: ai-hub-cloud-inference
+ label: AI Hub Cloud Inference
+ recommended: true
+ download: Run inferences on DeGirum's [AI Hub](https://hub.degirum.com) cloud with `@cloud`. Sign up, create an access token, and set it as `token`. Network latency may require lowering your detection fps.
+ ui: |
+ Navigate to **Settings > System > Detectors and model** and select **DeGirum** from the detector type dropdown and click **Add**.
+
+ | Field | Value |
+ | --- | --- |
+ | **Location** | `@cloud` |
+ | **Zoo** | `degirum/public` |
+ | **Token** | your AI Hub token (optional for the public zoo) |
+ yaml: |
+ degirum_detector:
+ type: degirum
+ location: @cloud
+ zoo: degirum/public
+ token: dg_example_token
diff --git a/docs/docs/configuration/reference.md b/docs/docs/configuration/advanced/reference.md
similarity index 94%
rename from docs/docs/configuration/reference.md
rename to docs/docs/configuration/advanced/reference.md
index e5eb161386..896ab2642b 100644
--- a/docs/docs/configuration/reference.md
+++ b/docs/docs/configuration/advanced/reference.md
@@ -147,6 +147,13 @@ auth:
# NOTE: changing this value will not automatically update password hashes, you
# will need to change each user password for it to apply
hash_iterations: 600000
+ # Optional: Map roles to the list of cameras each role can access (default: none)
+ # NOTE: An empty list grants the role access to all cameras. Roles defined here can be
+ # referenced by proxy header role mapping or assigned to native users.
+ roles:
+ my_custom_role:
+ - front_door
+ - back_yard
# Optional: model modifications
# NOTE: The default values are for the EdgeTPU detector.
@@ -166,6 +173,9 @@ model:
# Required: Object detection model input tensor format
# Valid values are nhwc or nchw (default: shown below)
input_tensor: nhwc
+ # Optional: Data type of the model input tensor
+ # Valid values are float, float_denorm, or int (default: shown below)
+ input_dtype: int
# Required: Object detection model type, currently only used with the OpenVINO detector
# Valid values are ssd, yolox, yolonas (default: shown below)
model_type: ssd
@@ -196,11 +206,12 @@ audio:
# - 500 - medium sensitivity
# - 1000 - low sensitivity
min_volume: 500
+ # Optional: Number of threads to use for audio detection (default: shown below)
+ num_threads: 2
# Optional: Types of audio to listen for (default: shown below)
listen:
- bark
- fire_alarm
- - scream
- speech
- yell
# Optional: Filters to configure detection.
@@ -257,7 +268,7 @@ birdseye:
# More information about presets at https://docs.frigate.video/configuration/ffmpeg_presets
ffmpeg:
# Optional: ffmpeg binary path (default: shown below)
- # can also be set to `7.0` or `5.0` to specify one of the included versions
+ # can also be set to `8.0` or `5.0` to specify one of the included versions
# or can be set to any path that holds `bin/ffmpeg` & `bin/ffprobe`
path: "default"
# Optional: global ffmpeg args (default: shown below)
@@ -328,7 +339,7 @@ detect:
# especially when using separate streams for detect and record.
# Use this setting to make the timeline bounding boxes more closely align
# with the recording. The value can be positive or negative.
- # TIP: Imagine there is an tracked object clip with a person walking from left to right.
+ # TIP: Imagine there is a tracked object clip with a person walking from left to right.
# If the tracked object lifecycle bounding box is consistently to the left of the person
# then the value should be decreased. Similarly, if a person is walking from
# left to right and the bounding box is consistently ahead of the person
@@ -469,6 +480,8 @@ review:
- Animals in the garden
# Optional: Preferred response language (default: English)
preferred_language: English
+ # Optional: Save thumbnails sent to the GenAI provider for review/debugging purposes (default: shown below)
+ debug_save_thumbnails: False
# Optional: Motion configuration
# NOTE: Can be overridden at the camera level
@@ -500,6 +513,8 @@ motion:
# - 30 - medium sensitivity
# - 50 - low sensitivity
contour_area: 10
+ # Optional: Alpha blending factor used in frame differencing for motion calculation (default: shown below)
+ delta_alpha: 0.2
# Optional: Alpha value passed to cv2.accumulateWeighted when averaging frames to determine the background (default: shown below)
# Higher values mean the current frame impacts the average a lot, and a new object will be averaged into the background faster.
# Low values will cause things like moving shadows to be detected as motion for longer.
@@ -572,6 +587,8 @@ record:
timelapse_args: "-vf setpts=0.04*PTS -r 30"
# Optional: Global hardware acceleration settings for timelapse exports. (default: inherit)
hwaccel_args: auto
+ # Optional: Maximum number of export jobs to process at the same time (default: shown below)
+ max_concurrent: 3
# Optional: Recording Preview Settings
preview:
# Optional: Quality of recording preview (default: shown below).
@@ -714,28 +731,42 @@ lpr:
enhancement: 0
# Optional: Save plate images to /media/frigate/clips/lpr for debugging purposes (default: shown below)
debug_save_plates: False
- # Optional: List of regex replacement rules to normalize detected plates (default: shown below)
- replace_rules: {}
+ # Optional: List of regex replacement rules to normalize detected plates before matching (default: none)
+ replace_rules:
+ # Required: regex pattern to match in the detected plate
+ - pattern: "O"
+ # Required: string to replace the matched pattern with
+ replacement: "0"
-# Optional: Configuration for AI / LLM provider
+# Optional: Configuration for AI / LLM providers
# WARNING: Depending on the provider, this will send thumbnails over the internet
# to Google or OpenAI's LLMs to generate descriptions. GenAI features can be configured at
# the camera level to enhance privacy for indoor cameras.
+# NOTE: genai is a map of named providers. Each key is a name you choose for the provider,
+# and each role (chat, descriptions, embeddings) may be assigned to exactly one provider.
genai:
- # Required: Provider must be one of ollama, gemini, or openai
- provider: ollama
- # Required if provider is ollama. May also be used for an OpenAI API compatible backend with the openai provider.
- base_url: http://localhost::11434
- # Required if gemini or openai
- api_key: "{FRIGATE_GENAI_API_KEY}"
- # Required: The model to use with the provider.
- model: gemini-1.5-flash
- # Optional additional args to pass to the GenAI Provider (default: None)
- provider_options:
- keep_alive: -1
- # Optional: Options to pass during inference calls (default: {})
- runtime_options:
- temperature: 0.7
+ # Required: name of the provider (chosen by you, used to reference it elsewhere)
+ my_provider:
+ # Required: Provider must be one of ollama, openai, azure_openai, gemini, or llamacpp
+ provider: ollama
+ # Required if provider is ollama. May also be used for an OpenAI API compatible backend with the openai provider.
+ base_url: http://localhost::11434
+ # Required if gemini or openai
+ api_key: "{FRIGATE_GENAI_API_KEY}"
+ # Required: The model to use with the provider.
+ model: gemini-1.5-flash
+ # Optional: Roles this provider handles (default: shown below)
+ # Each role (chat, descriptions, embeddings) must be assigned to exactly one provider.
+ roles:
+ - chat
+ - descriptions
+ - embeddings
+ # Optional additional args to pass to the GenAI Provider (default: None)
+ provider_options:
+ keep_alive: -1
+ # Optional: Options to pass during inference calls (default: {})
+ runtime_options:
+ temperature: 0.7
# Optional: Configuration for audio transcription
# NOTE: only the enabled option can be overridden at the camera level
@@ -789,7 +820,7 @@ classification:
interval: None
# Optional: Restream configuration
-# Uses https://github.com/AlexxIT/go2rtc (v1.9.13)
+# Uses https://github.com/AlexxIT/go2rtc (v1.9.14)
# NOTE: The default go2rtc API port (1984) must be used,
# changing this port for the integrated go2rtc instance is not supported.
go2rtc:
@@ -840,8 +871,8 @@ cameras:
# Required: name of the camera
back:
# Optional: Enable/Disable the camera (default: shown below).
- # If disabled: config is used but no live stream and no capture etc.
- # Events/Recordings are still viewable.
+ # When False, ffmpeg is not started and the camera is hidden from the UI
+ # (except Camera Management). Re-enabling requires a Frigate restart.
enabled: True
# Optional: camera type used for some Frigate features (default: shown below)
# Options are "generic" and "lpr"
@@ -908,6 +939,9 @@ cameras:
inertia: 3
# Optional: Number of seconds that an object must loiter to be considered in the zone (default: shown below)
loitering_time: 0
+ # Optional: Minimum speed required for an object to be considered present in the zone (default: none)
+ # In real-world units if distances are set. Used for speed-based zone triggers.
+ speed_threshold: 2.5
# Optional: List of objects that can trigger this zone (default: all tracked objects)
objects:
- person
@@ -945,6 +979,9 @@ cameras:
order: 0
# Optional: Whether or not to show the camera in the Frigate UI (default: shown below)
dashboard: True
+ # Optional: Whether this camera is visible in review (the review page and its camera
+ # filter, motion review, and the history view) (default: shown below)
+ review: True
# Optional: connect to ONVIF camera
# to enable PTZ controls.
@@ -1083,22 +1120,6 @@ ui:
# Optional: Set the time format used.
# Options are browser, 12hour, or 24hour (default: shown below)
time_format: browser
- # Optional: Set the date style for a specified length.
- # Options are: full, long, medium, short
- # Examples:
- # short: 2/11/23
- # medium: Feb 11, 2023
- # full: Saturday, February 11, 2023
- # (default: shown below).
- date_style: short
- # Optional: Set the time style for a specified length.
- # Options are: full, long, medium, short
- # Examples:
- # short: 8:14 PM
- # medium: 8:15:22 PM
- # full: 8:15:22 PM Mountain Standard Time
- # (default: shown below).
- time_style: medium
# Optional: Set the unit system to either "imperial" or "metric" (default: metric)
# Used in the UI and in MQTT topics
unit_system: metric
diff --git a/docs/docs/configuration/advanced.md b/docs/docs/configuration/advanced/system.md
similarity index 87%
rename from docs/docs/configuration/advanced.md
rename to docs/docs/configuration/advanced/system.md
index e6de72593b..28291efff7 100644
--- a/docs/docs/configuration/advanced.md
+++ b/docs/docs/configuration/advanced/system.md
@@ -1,7 +1,6 @@
---
-id: advanced
-title: Advanced Options
-sidebar_label: Advanced Options
+id: system
+title: System
---
import ConfigTabs from "@site/src/components/ConfigTabs";
@@ -68,15 +67,21 @@ This section can be used to set environment variables for those unable to modify
Variables prefixed with `FRIGATE_` can be referenced in config fields that support environment variable substitution (such as MQTT host and credentials, camera stream URLs, and ONVIF host and credentials) using the `{FRIGATE_VARIABLE_NAME}` syntax.
+:::note
+
+The `go2rtc` section is an exception. go2rtc runs as a separate process, so its stream definitions can only be substituted with variables that exist in the container's environment (set via Docker `-e`, the `environment:` section of `docker-compose.yml`, or Docker secrets). Variables defined in the `environment_vars` block above are not available to go2rtc streams. Home Assistant app users, who cannot set container environment variables, must instead put credentials directly in their go2rtc stream URLs.
+
+:::
+
Navigate to to add or edit environment variables.
-| Field | Description |
-| --------- | --------------------------------------------------------- |
-| **Key** | The environment variable name (e.g., `FRIGATE_MQTT_USER`) |
-| **Value** | The value for the variable |
+| Field | Description |
+| ----------------- | --------------------------------------------------------- |
+| **Variable name** | The environment variable name (e.g., `FRIGATE_MQTT_USER`) |
+| **Value** | The value for the variable |
Variables defined here can be referenced elsewhere in your configuration using the `{FRIGATE_VARIABLE_NAME}` syntax.
@@ -172,7 +177,7 @@ Custom models may also require different input tensor formats. The colorspace co
-Navigate to to configure the model path, dimensions, and input format.
+Navigate to and open the **Custom Model** tab to configure the model path, dimensions, and input format.
| Field | Description |
| --------------------------------------------- | ------------------------------------ |
@@ -202,7 +207,7 @@ model:
:::warning
-If the labelmap is customized then the labels used for alerts will need to be adjusted as well. See [alert labels](../configuration/review.md#restricting-alerts-to-specific-labels) for more info.
+If the labelmap is customized then the labels used for alerts will need to be adjusted as well. See [alert labels](../review.md#restricting-alerts-to-specific-labels) for more info.
:::
@@ -234,26 +239,16 @@ Some labels have special handling and modifications can disable functionality.
## Network Configuration
-Changes to Frigate's internal network configuration can be made by bind mounting nginx.conf into the container. For example:
-
-```yaml
-services:
- frigate:
- container_name: frigate
- ...
- volumes:
- ...
- - /path/to/your/nginx.conf:/usr/local/nginx/conf/nginx.conf
-```
+Frigate exposes a few networking options. IPv6 and the listen ports are set in the `networking` configuration (or from the Settings UI); more advanced changes require [customizing the bundled Nginx configuration](#customizing-the-nginx-configuration).
### Enabling IPv6
-IPv6 is disabled by default. Enable it in the Frigate configuration.
+By default Frigate listens on IPv4 only. To also listen on IPv6 (on port `5000`, and on `8971` when TLS is configured), enable it in the `networking` configuration.
-Navigate to and expand **IPv6 configuration**, then enable **Enable IPv6**.
+Navigate to and enable **IPv6**.
@@ -261,7 +256,7 @@ Navigate to and expand **IPv6
```yaml
networking:
ipv6:
- enabled: True
+ enabled: true
```
@@ -300,6 +295,20 @@ This setting is for advanced users. For the majority of use cases it's recommend
:::
+### Customizing the Nginx configuration
+
+More advanced changes to Frigate's internal network configuration can be made by bind mounting your own `nginx.conf` into the container. For example:
+
+```yaml
+services:
+ frigate:
+ container_name: frigate
+ ...
+ volumes:
+ ...
+ - /path/to/your/nginx.conf:/usr/local/nginx/conf/nginx.conf
+```
+
## Base path
By default, Frigate runs at the root path (`/`). However some setups require to run Frigate under a custom path prefix (e.g. `/frigate`), especially when Frigate is located behind a reverse proxy that requires path-based routing.
@@ -351,7 +360,7 @@ To do this:
### Custom go2rtc version
-Frigate currently includes go2rtc v1.9.13, there may be certain cases where you want to run a different version of go2rtc.
+Frigate currently includes go2rtc v1.9.14, there may be certain cases where you want to run a different version of go2rtc.
To do this:
diff --git a/docs/docs/configuration/audio_detectors.md b/docs/docs/configuration/audio_detectors.md
index eba22ec184..03c68895bd 100644
--- a/docs/docs/configuration/audio_detectors.md
+++ b/docs/docs/configuration/audio_detectors.md
@@ -54,7 +54,7 @@ The ffmpeg process for capturing audio will be a separate connection to the came
-Navigate to and add an input with the `audio` role pointing to a stream that includes audio.
+Navigate to and add an input with the `audio` role pointing to a stream that includes audio.
@@ -78,7 +78,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. 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.
+The audio detector uses volume levels in the same way that motion in a camera feed is used for object detection. This means that Frigate will not run audio detection unless the audio volume is above the configured level in order to reduce resource usage. Audio levels can vary widely between camera models so it is important to run tests to see what volume levels are. The [Debug view](/usage/live#the-single-camera-view) in the Frigate UI has an Audio tab for cameras that have the `audio` role assigned where a graph and the current levels are displayed. The `min_volume` parameter should be set to the minimum the `RMS` level required to run audio detection.
:::tip
@@ -88,7 +88,7 @@ Volume is considered motion for recordings, this means when the `record -> retai
### Configuring Audio Events
-The included audio model has over [500 different types](https://github.com/blakeblackshear/frigate/blob/dev/audio-labelmap.txt) of audio that can be detected, many of which are not practical. By default `bark`, `fire_alarm`, `scream`, `speech`, and `yell` are enabled but these can be customized.
+The included audio model has over [500 different types](https://github.com/blakeblackshear/frigate/blob/dev/audio-labelmap.txt) of audio that can be detected, many of which are not practical. By default `bark`, `fire_alarm`, `speech`, and `yell` are enabled but these can be customized.
@@ -107,7 +107,6 @@ audio:
listen:
- bark
- fire_alarm
- - scream
- speech
- yell
```
@@ -115,9 +114,73 @@ audio:
+### Common Audio Labels
+
+The labelmap includes hundreds of sound types. The labels below are the ones most users may find practical, grouped by what they're typically used for. Use the exact label string from the left column in your `listen` config, or search for the label in the Frigate UI directly.
+
+Some labels cover several related sounds: `yell` is triggered by shouting, yelling, children shouting, and screaming; `crying` covers baby cries, sobbing, and whimpering; and `speech` covers ordinary talking and conversation.
+
+**Safety and security**
+
+| Label | Detects |
+| ---------------- | ---------------------------------- |
+| `yell` | Shouting, yelling, screaming |
+| `fire_alarm` | Fire and smoke alarm sirens |
+| `smoke_detector` | Smoke detector beeps |
+| `alarm` | General alarm sounds |
+| `car_alarm` | Car alarms |
+| `siren` | Emergency vehicle and civil sirens |
+| `glass` | Glass clinking |
+| `shatter` | Breaking glass |
+| `breaking` | Something breaking |
+| `gunshot` | Gunshots |
+| `explosion` | Explosions |
+
+**People and activity**
+
+| Label | Detects |
+| ----------- | ------------------------ |
+| `speech` | Talking and conversation |
+| `laughter` | Laughing |
+| `crying` | Baby crying and sobbing |
+| `cough` | Coughing |
+| `footsteps` | Footsteps and walking |
+| `knock` | Knocking on a door |
+| `doorbell` | Doorbell |
+| `ding-dong` | Doorbell chime |
+
+**Pets and animals**
+
+| Label | Detects |
+| ---------- | ---------------- |
+| `bark` | Dog barking |
+| `dog` | Other dog sounds |
+| `howl` | Howling |
+| `growling` | Growling |
+| `meow` | Cat meowing |
+| `cat` | Other cat sounds |
+| `hiss` | Hissing |
+
+**Vehicles and driveway**
+
+| Label | Detects |
+| ----------------- | -------------------- |
+| `car` | Passing cars |
+| `honk` | Car horns |
+| `truck` | Trucks |
+| `reversing_beeps` | Vehicle backup beeps |
+| `motorcycle` | Motorcycles |
+| `engine_starting` | Engines starting |
+
+:::tip
+
+Frequently-heard labels like `speech` can generate a lot of events, and each event could save a snapshot and recording based on your configuration, so start with a focused set and expand from there. The defaults (`bark`, `fire_alarm`, `speech`, `yell`) plus a few of the safety labels above cover most needs. See the [full audio labelmap](https://github.com/blakeblackshear/frigate/blob/dev/audio-labelmap.txt) or the Frigate UI for every available type.
+
+:::
+
### Audio Transcription
-Frigate supports fully local audio transcription using either `sherpa-onnx` or OpenAI's open-source Whisper models via `faster-whisper`. The goal of this feature is to support Semantic Search for `speech` audio events. Frigate is not intended to act as a continuous, fully-automatic speech transcription service — automatically transcribing all speech (or queuing many audio events for transcription) requires substantial CPU (or GPU) resources and is impractical on most systems. For this reason, transcriptions for events are initiated manually from the UI or the API rather than being run continuously in the background.
+Frigate supports fully local audio transcription using either `sherpa-onnx` or OpenAI's open-source Whisper models via `faster-whisper`. The goal of this feature is to support Semantic Search for `speech` audio events. Frigate is not intended to act as a continuous, fully-automatic speech transcription service. Automatically transcribing all speech (or queuing many audio events for transcription) requires substantial CPU (or GPU) resources and is impractical on most systems. For this reason, transcriptions for events are initiated manually from the UI or the API rather than being run continuously in the background.
:::info
diff --git a/docs/docs/configuration/authentication.md b/docs/docs/configuration/authentication.md
index 0d80d80ce2..a565398774 100644
--- a/docs/docs/configuration/authentication.md
+++ b/docs/docs/configuration/authentication.md
@@ -91,7 +91,7 @@ auth:
## Session Length
-The default session length for user authentication in Frigate is 24 hours. This setting determines how long a user's authenticated session remains active before a token refresh is required — otherwise, the user will need to log in again.
+The default session length for user authentication in Frigate is 24 hours. This setting determines how long a user's authenticated session remains active before a token refresh is required. Otherwise, the user will need to log in again.
While the default provides a balance of security and convenience, you can customize this duration to suit your specific security requirements and user experience preferences. The session length is configured in seconds.
@@ -141,7 +141,7 @@ Changing the secret will invalidate current tokens.
## Proxy configuration
-Frigate can be configured to leverage features of common upstream authentication proxies such as Authelia, Authentik, oauth2_proxy, or traefik-forward-auth.
+Frigate can be configured to leverage features of common upstream authentication proxies such as Authelia, Authentik, oauth2_proxy, or traefik-forward-auth. Frigate does not implement OIDC, SAML, or LDAP natively; as an NVR focused on recording and object detection, it relies on robust, battle-tested proxies to handle those protocols and passes the authenticated user and role through via headers (see below).
If you are leveraging the authentication of an upstream proxy, you likely want to disable Frigate's authentication as there is no correspondence between users in Frigate's database and users authenticated via the proxy. Optionally, if communication between the reverse proxy and Frigate is over an untrusted network, you should set an `auth_secret` in the `proxy` config and configure the proxy to send the secret value as a header named `X-Proxy-Secret`. Assuming this is an untrusted network, you will also want to [configure a real TLS certificate](tls.md) to ensure the traffic can't simply be sniffed to steal the secret.
diff --git a/docs/docs/configuration/autotracking.md b/docs/docs/configuration/autotracking.md
index 27312eaa92..f49026e11c 100644
--- a/docs/docs/configuration/autotracking.md
+++ b/docs/docs/configuration/autotracking.md
@@ -6,6 +6,7 @@ title: Camera Autotracking
import ConfigTabs from "@site/src/components/ConfigTabs";
import TabItem from "@theme/TabItem";
import NavPath from "@site/src/components/NavPath";
+import FaqItem from "@site/src/components/FaqItem";
An ONVIF-capable, PTZ (pan-tilt-zoom) camera that supports relative movement within the field of view (FOV) can be configured to automatically track moving objects and keep them in the center of the frame.
@@ -161,13 +162,13 @@ Every PTZ camera is different, so autotracking may not perform ideally in every
The object tracker in Frigate estimates the motion of the PTZ so that tracked objects are preserved when the camera moves. In most cases 5 fps is sufficient, but if you plan to track faster moving objects, you may want to increase this slightly. Higher frame rates (> 10fps) will only slow down Frigate and the motion estimator and may lead to dropped frames, especially if you are using experimental zooming.
-A fast [detector](object_detectors.md) is recommended. CPU detectors will not perform well or won't work at all. You can watch Frigate's debug viewer for your camera to see a thicker colored box around the object currently being autotracked.
+A fast [detector](object_detectors.md) is recommended. CPU detectors will not perform well or won't work at all. You can watch Frigate's [debug viewer](/usage/live#the-single-camera-view) for your camera to see a thicker colored box around the object currently being autotracked.

A full-frame zone in `required_zones` is not recommended, especially if you've calibrated your camera and there are `movement_weights` defined in the configuration file. Frigate will continue to autotrack an object that has entered one of the `required_zones`, even if it moves outside of that zone.
-Some users have found it helpful to adjust the zone `inertia` value. See the [configuration reference](index.md).
+Some users have found it helpful to adjust the zone `inertia` value. See the [configuration reference](advanced/reference.md).
## Zooming
@@ -187,30 +188,96 @@ In security and surveillance, it's common to use "spotter" cameras in combinatio
## Troubleshooting and FAQ
-### The autotracker loses track of my object. Why?
+### Camera Compatibility
+
+
+
+See the community-maintained list of [ONVIF PTZ camera recommendations](cameras.md#onvif-ptz-camera-recommendations) for cameras and brands reported to work (and not work) with autotracking. This is not an exhaustive list that is frequently updated, so other cameras not listed may also work well. Frigate's autotracking was developed with a Dahua SD1A404XB-GNR (now sold as the EmpireTech PTZ1A4M-4X-S2), and Dahua / EmpireTech PTZs are the most consistently reported as working well.
+
+When comparing models:
+
+- Verify ONVIF support first. See [Checking ONVIF camera support](#checking-onvif-camera-support) above.
+- Favor a camera with a fast PTZ motor. Cameras with slow motors may fail [calibration](#calibration) and will struggle to keep up with objects that move across the field of view quickly.
+
+
+
+
+
+No. Reolink cameras (including the TrackMix series) lack the ONVIF FOV RelativeMove firmware support that Frigate's autotracker requires, so autotracking will not work with any current Reolink PTZ. Their video streams and basic PTZ controls still work in Frigate. If you want object tracking on a Reolink PTZ, you will need to use the tracking feature built into the camera's firmware, which is proprietary and operates independently of Frigate.
+
+
+
+
+
+There are two possible known reasons for this (and perhaps others yet unknown): a slow PTZ motor or buggy camera firmware. Frigate uses an ONVIF parameter provided by the camera, `MoveStatus`, to determine when the PTZ's motor is moving or idle. According to some users, Hikvision PTZs (even with the latest firmware), are not updating this value after PTZ movement. Unfortunately there is no workaround to this bug in Hikvision firmware, so autotracking will not function correctly and should be disabled in your config. This may also be the case with other non-Hikvision cameras utilizing Hikvision firmware, such as some Annke models. In rare cases the vendor may provide fixed firmware on request; for example, Annke has supplied firmware that resolves this for the CZ504 (see the [camera recommendations list](cameras.md#onvif-ptz-camera-recommendations)).
+
+
+
+
+
+Some cameras have firmware that reports that FOV RelativeMove, the ONVIF command that Frigate uses for autotracking, is supported. However, if the camera does not pan or tilt when an object comes into the required zone, your camera's firmware does not actually support FOV RelativeMove. One such camera is the Uniview IPC672LR-AX4DUPK. It actually moves its zoom motor instead of panning and tilting and does not follow the ONVIF standard whatsoever.
+
+
+
+### Calibration Issues
+
+
+
+This is often caused by the same reason as the "MOVING" status error above - the `MoveStatus` ONVIF parameter is not changing due to a bug in your camera's firmware. Also, see the note above: Frigate's web UI and all other cameras will be unresponsive while calibration is in progress. This is expected and normal. But if you don't see log entries every few seconds for calibration progress, your camera is not compatible with autotracking.
+
+
+
+
+
+Calibration measures the amount of time it takes for Frigate to make a series of movements with your PTZ. This error message is recorded in the log if these values are too high for Frigate to support calibrated autotracking. This is often the case when your camera's motor or network connection is too slow or your camera's firmware doesn't report the motor status in a timely manner.
+
+Some things to try:
+
+- If your camera's firmware has a PTZ or motor speed setting, set it to the fastest available speed and calibrate again.
+- Run without calibration: remove the `movement_weights` line from your config, set `calibrate_on_startup` to `False`, and restart.
+
+If calibration consistently fails, this often means your camera's motor is too slow and autotracking will behave unpredictably or won't be able to keep up with moving objects.
+
+
+
+
+
+Frigate uses the `movement_weights` measured during calibration to predict how far the camera needs to move to keep an object centered, so inaccurate values produce movements that don't seem to make sense: overshooting, moving the opposite direction, or zooming in on an object's last known position and losing it entirely. This is almost always a calibration issue.
+
+- Remove the `movement_weights` entry from your config and restart Frigate to run without calibration. If tracking improves, try recalibrating.
+- Recalibrate several times. The `movement_weights` values should be close to each other after each run. If they vary significantly between runs, your camera may not be reporting its motor status reliably, and you may get better results without calibration.
+- If you are using zooming, a high `zoom_factor` can cause the camera to zoom in too far and lose the object. Try a lower value.
+
+Remember to recalibrate whenever you change your `return_preset`, change your camera's detect `fps`, or enable zooming after calibrating with it disabled.
+
+
+
+### Tracking Behavior
+
+
There are many reasons this could be the case. If you are using experimental zooming, your `zoom_factor` value might be too high, the object might be traveling too quickly, the scene might be too dark, there are not enough details in the scene (for example, a PTZ looking down on a driveway or other monotone background without a sufficient number of hard edges or corners), or the scene is otherwise less than optimal for Frigate to maintain tracking.
Your camera's shutter speed may also be set too low so that blurring occurs with motion. Check your camera's firmware to see if you can increase the shutter speed.
-Watching Frigate's debug view can help to determine a possible cause. The autotracked object will have a thicker colored box around it.
+Watching Frigate's debug view can help to determine a possible cause. The autotracked object will have a thicker colored box around it. If the camera consistently zooms in on the object and then loses it, see [Autotracking is erratic, moves the camera in the wrong direction, or zooms past my object. Why?](#autotracking-is-erratic-or-moves-the-camera-in-the-wrong-direction) above.
-### I'm seeing an error in the logs that my camera "is still in ONVIF 'MOVING' status." What does this mean?
+
-There are two possible known reasons for this (and perhaps others yet unknown): a slow PTZ motor or buggy camera firmware. Frigate uses an ONVIF parameter provided by the camera, `MoveStatus`, to determine when the PTZ's motor is moving or idle. According to some users, Hikvision PTZs (even with the latest firmware), are not updating this value after PTZ movement. Unfortunately there is no workaround to this bug in Hikvision firmware, so autotracking will not function correctly and should be disabled in your config. This may also be the case with other non-Hikvision cameras utilizing Hikvision firmware.
-
-### I tried calibrating my camera, but the logs show that it is stuck at 0% and Frigate is not starting up.
-
-This is often caused by the same reason as above - the `MoveStatus` ONVIF parameter is not changing due to a bug in your camera's firmware. Also, see the note above: Frigate's web UI and all other cameras will be unresponsive while calibration is in progress. This is expected and normal. But if you don't see log entries every few seconds for calibration progress, your camera is not compatible with autotracking.
-
-### I'm seeing this error in the logs: "Autotracker: motion estimator couldn't get transformations". What does this mean?
+
To maintain object tracking during PTZ moves, Frigate tracks the motion of your camera based on the details of the frame. If you are seeing this message, it could mean that your `zoom_factor` may be set too high, the scene around your detected object does not have enough details (like hard edges or color variations), or your camera's shutter speed is too slow and motion blur is occurring. Try reducing `zoom_factor`, finding a way to alter the scene around your object, or changing your camera's shutter speed.
-### Calibration seems to have completed, but the camera is not actually moving to track my object. Why?
+
-Some cameras have firmware that reports that FOV RelativeMove, the ONVIF command that Frigate uses for autotracking, is supported. However, if the camera does not pan or tilt when an object comes into the required zone, your camera's firmware does not actually support FOV RelativeMove. One such camera is the Uniview IPC672LR-AX4DUPK. It actually moves its zoom motor instead of panning and tilting and does not follow the ONVIF standard whatsoever.
+
-### Frigate reports an error saying that calibration has failed. Why?
+When the PTZ moves, the entire frame changes at once. Frigate's motion detection treats sudden scene-wide changes (like a lightning flash, an infrared mode switch, or a camera move) specially and pauses detection momentarily until the scene stabilizes. This is expected and normal, and detection resumes shortly after the camera stops moving. If detection does not resume once the camera is stationary, use the [debug view](/usage/live#the-single-camera-view) to see what is happening.
-Calibration measures the amount of time it takes for Frigate to make a series of movements with your PTZ. This error message is recorded in the log if these values are too high for Frigate to support calibrated autotracking. This is often the case when your camera's motor or network connection is too slow or your camera's firmware doesn't report the motor status in a timely manner. You can try running without calibration (just remove the `movement_weights` line from your config and restart), but if calibration fails, this often means that autotracking will behave unpredictably.
+
+
+
+
+Yes. Autotracking can be toggled per camera at runtime over MQTT with the [`frigate//ptz_autotracker/set`](../integrations/mqtt.md#frigatecamera_nameptz_autotrackerset) topic, and the [Home Assistant integration](../integrations/home-assistant.md) exposes a switch for it. This pairs well with the "spotter" camera automations described in [Usage applications](#usage-applications) above, for example only enabling autotracking at night or when nobody is home.
+
+
diff --git a/docs/docs/configuration/birdseye.md b/docs/docs/configuration/birdseye.md
index 8104494787..26a2386d89 100644
--- a/docs/docs/configuration/birdseye.md
+++ b/docs/docs/configuration/birdseye.md
@@ -6,10 +6,16 @@ import NavPath from "@site/src/components/NavPath";
In addition to Frigate's Live camera dashboard, Birdseye allows a portable heads-up view of your cameras to see what is going on around your property / space without having to watch all cameras that may have nothing happening. Birdseye allows specific modes that intelligently show and disappear based on what you care about.
-Birdseye can be viewed by adding the "Birdseye" camera to a Camera Group in the Web UI. Add a Camera Group by pressing the "+" icon on the Live page, and choose "Birdseye" as one of the cameras.
+Birdseye can be viewed by adding the "Birdseye" camera to a Camera Group in the Web UI. Add a Camera Group by pressing the pencil icon in the sidebar on the Live page, and choose "Birdseye" as one of the cameras.
Birdseye can also be used in Home Assistant dashboards, cast to media devices, etc.
+:::note
+
+Each camera tile in Birdseye is composed from the frames of the stream assigned the `detect` role, so a camera's image quality in Birdseye matches its detect stream resolution rather than a higher-resolution recording stream. If a camera looks low quality in Birdseye, increasing the detect width and height (or assigning the `detect` role to a higher-resolution stream) is what affects it. See [setting up camera inputs](./cameras.md#setting-up-camera-inputs) for how roles are assigned.
+
+:::
+
## Birdseye Behavior
### Birdseye Modes
@@ -35,10 +41,10 @@ To include a camera in Birdseye view only for specific circumstances, or exclude
**Per-camera overrides:** Navigate to to override the mode or disable Birdseye for a specific camera.
-| Field | Description |
-|-------|-------------|
-| **Enable Birdseye** | Whether this camera appears in Birdseye view |
-| **Tracking mode** | When to show the camera: `continuous`, `motion`, or `objects` |
+| Field | Description |
+| ------------------- | ------------------------------------------------------------- |
+| **Enable Birdseye** | Whether this camera appears in Birdseye view |
+| **Tracking mode** | When to show the camera: `continuous`, `motion`, or `objects` |
@@ -72,8 +78,8 @@ By default birdseye shows all cameras that have had the configured activity in t
Navigate to .
-| Field | Description |
-|-------|-------------|
+| Field | Description |
+| ------------------------ | --------------------------------------------------------------------------- |
| **Inactivity threshold** | Seconds of inactivity before a camera is hidden from Birdseye (default: 30) |
@@ -100,9 +106,9 @@ The resolution and aspect ratio of birdseye can be configured. Resolution will i
Navigate to .
-| Field | Description |
-|-------|-------------|
-| **Width** | Birdseye output width in pixels (default: 1280) |
+| Field | Description |
+| ---------- | ----------------------------------------------- |
+| **Width** | Birdseye output width in pixels (default: 1280) |
| **Height** | Birdseye output height in pixels (default: 720) |
@@ -120,12 +126,12 @@ birdseye:
### Sorting cameras in the Birdseye view
-It is possible to override the order of cameras that are being shown in the Birdseye view. The order is set at the camera level.
+It is possible to override the order of cameras that are being shown in the Birdseye view. The order is set at the camera level (when using YAML).
-Navigate to for each camera and set the **Position** field to control the display order.
+Navigate to and in the **Camera order** field, use the drag handle next to each camera name to control the display order.
@@ -161,8 +167,8 @@ It is possible to limit the number of cameras shown on birdseye at one time. Whe
Navigate to .
-| Field | Description |
-|-------|-------------|
+| Field | Description |
+| ------------------------ | ----------------------------------------------------------------------------------- |
| **Layout > Max cameras** | Maximum number of cameras shown at once (e.g., `1` for only the most active camera) |
@@ -187,8 +193,8 @@ By default birdseye tries to fit 2 cameras in each row and then double in size u
Navigate to .
-| Field | Description |
-|-------|-------------|
+| Field | Description |
+| --------------------------- | -------------------------------------------------------- |
| **Layout > Scaling factor** | Camera scaling factor between 1.0 and 5.0 (default: 2.0) |
diff --git a/docs/docs/configuration/camera_specific.md b/docs/docs/configuration/camera_specific.md
index 9d39ef2519..27eb729bfa 100644
--- a/docs/docs/configuration/camera_specific.md
+++ b/docs/docs/configuration/camera_specific.md
@@ -3,6 +3,8 @@ id: camera_specific
title: Camera Specific Configurations
---
+import NavPath from "@site/src/components/NavPath";
+
:::note
This page makes use of presets of FFmpeg args. For more information on presets, see the [FFmpeg Presets](/configuration/ffmpeg_presets) page.
@@ -148,19 +150,34 @@ WEB Digest Algorithm - MD5
Reolink has many different camera models with inconsistently supported features and behavior. The below table shows a summary of various features and recommendations.
-| Camera Resolution | Camera Generation | Recommended Stream Type | Additional Notes |
-| ----------------- | ------------------------- | --------------------------------- | ----------------------------------------------------------------------- |
-| 5MP or lower | All | http-flv | Stream is h264 |
-| 6MP or higher | Latest (ex: Duo3, CX-8##) | http-flv with ffmpeg 8.0, or rtsp | This uses the new http-flv-enhanced over H265 which requires ffmpeg 8.0 |
-| 6MP or higher | Older (ex: RLC-8##) | rtsp | |
+| Camera Resolution | Camera Generation | Recommended Stream Type | Additional Notes |
+| ----------------- | ------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------- |
+| 5MP or lower | All | http-flv | Stream is h264 |
+| 6MP or higher | Latest (ex: Duo3, CX-8##) | http-flv with ffmpeg 8.0, or rtsp | This uses the new http-flv-enhanced over H265 which requires ffmpeg 8.0 (Frigate's default) |
+| 6MP or higher | Older (ex: RLC-8##) | rtsp | |
-Frigate works much better with newer reolink cameras that are setup with the below options:
+Frigate works much better with newer Reolink cameras that are setup with the below options:
If available, recommended settings are:
- `On, fluency first` this sets the camera to CBR (constant bit rate)
- `Interframe Space 1x` this sets the iframe interval to the same as the frame rate
+#### Setup via the Add Camera Wizard
+
+The Add Camera Wizard is the recommended way to add a standard Reolink camera. Before starting, make sure [HTTP is enabled](https://support.reolink.com/articles/360003452893-How-to-Access-Reolink-Cameras-NVRs-Home-Hub-Locally-via-Web-Browsers/) in the camera's advanced network settings. The wizard uses the camera's HTTP API to determine its resolution and choose the recommended stream type from the table above.
+
+1. Click **Add Camera** in .
+2. Choose **Manual selection** as the stream detection method and select **Reolink** as the camera brand.
+3. The wizard queries the camera and automatically uses an http-flv stream for cameras 5MP and lower, or an RTSP stream for higher resolution cameras.
+4. In the validation step, enable **Use stream compatibility mode** for http-flv streams when the wizard recommends it.
+
+If you use the **Probe camera** method instead, the discovered stream URLs will be RTSP. For Reolink cameras where http-flv is recommended, the wizard will show a warning in the validation step.
+
+The wizard covers standard single-camera setups. For two way talk, cameras connected through a Reolink NVR, or audio transcoding for WebRTC live view, configure the camera manually as shown below.
+
+#### Manual configuration
+
According to [this discussion](https://github.com/blakeblackshear/frigate/issues/3235#issuecomment-1135876973), the http video streams seem to be the most reliable for Reolink.
Cameras connected via a Reolink NVR can be connected with the http stream, use `channel[0..15]` in the stream url for the additional channels.
@@ -175,7 +192,7 @@ Reolink's latest cameras support two way audio via go2rtc and other applications
NOTE: The RTSP stream can not be prefixed with `ffmpeg:`, as go2rtc needs to handle the stream to support two way audio.
-Ensure HTTP is enabled in the camera's advanced network settings. To use two way talk with Frigate, see the [Live view documentation](/configuration/live#two-way-talk).
+Ensure [HTTP is enabled](https://support.reolink.com/articles/360003452893-How-to-Access-Reolink-Cameras-NVRs-Home-Hub-Locally-via-Web-Browsers/) in the camera's advanced network settings. To use two way talk with Frigate, see the [Live view documentation](/configuration/live#two-way-talk).
:::
@@ -187,7 +204,7 @@ go2rtc:
- "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=username&password=password#video=copy#audio=copy#audio=opus"
your_reolink_camera_sub:
- "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=username&password=password"
- # example for connectin to a Reolink camera that supports two way talk
+ # example for connecting to a Reolink camera that supports two way talk
your_reolink_camera_twt:
- "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=username&password=password#video=copy#audio=copy#audio=opus"
- "rtsp://username:password@reolink_ip/Preview_01_sub"
@@ -225,13 +242,14 @@ cameras:
roles:
- detect
```
+
### Unifi Protect Cameras
-:::note
+:::note
-Unifi G5s cameras and newer need a Unifi Protect server to enable rtsps stream, it's not posible to enable it in standalone mode.
+Unifi G5s cameras and newer need a Unifi Protect server to enable rtsps stream, it's not possible to enable it in standalone mode.
:::
@@ -246,7 +264,7 @@ go2rtc:
- rtspx://192.168.1.1:7441/abcdefghijk
```
-[See the go2rtc docs for more information](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#source-rtsp)
+[See the go2rtc docs for more information](https://github.com/AlexxIT/go2rtc/tree/v1.9.14#source-rtsp)
In the Unifi 2.0 update Unifi Protect Cameras had a change in audio sample rate which causes issues for ffmpeg. The input rate needs to be set for record if used directly with unifi protect.
@@ -269,7 +287,6 @@ Some community members have found better performance on Wyze cameras by using an
To use a USB camera (webcam) with Frigate, the recommendation is to use go2rtc's [FFmpeg Device](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#source-ffmpeg-device) support:
- Preparation outside of Frigate:
-
- Get USB camera path. Run `v4l2-ctl --list-devices` to get a listing of locally-connected cameras available. (You may need to install `v4l-utils` in a way appropriate for your Linux distribution). In the sample configuration below, we use `video=0` to correlate with a detected device path of `/dev/video0`
- Get USB camera formats & resolutions. Run `ffmpeg -f v4l2 -list_formats all -i /dev/video0` to get an idea of what formats and resolutions the USB Camera supports. In the sample configuration below, we use a width of 1024 and height of 576 in the stream and detection settings based on what was reported back.
- If using Frigate in a container (e.g. Docker on TrueNAS), ensure you have USB Passthrough support enabled, along with a specific Host Device (`/dev/video0`) + Container Device (`/dev/video0`) listed.
diff --git a/docs/docs/configuration/cameras.md b/docs/docs/configuration/cameras.md
index 8094c9f1c7..dc93f30ce1 100644
--- a/docs/docs/configuration/cameras.md
+++ b/docs/docs/configuration/cameras.md
@@ -24,12 +24,14 @@ Each role can only be assigned to one input per camera. The options for roles ar
-Navigate to .
+Navigate to .
| Field | Description |
| ----------------- | ------------------------------------------------------------------- |
| **Camera inputs** | List of input stream definitions (paths and roles) for this camera. |
+For each input you can choose its source: select **Restream (go2rtc)** to pick an existing [go2rtc stream](restream.md) from a dropdown (Frigate uses the `rtsp://127.0.0.1:8554/` path and `preset-rtsp-restream` input args for that input automatically), or **Manual input path** to type the stream URL directly.
+
Navigate to .
| Field | Description |
@@ -67,7 +69,7 @@ Additional cameras are simply added under the camera configuration section.
-Navigate to and use the add camera button to configure each additional camera.
+Navigate to and use the add camera button to configure each additional camera.
@@ -143,6 +145,11 @@ If your ONVIF camera does not require authentication credentials, you may still
:::
+If a camera connects but fails to authenticate, two optional fields can help:
+
+- `tls_insecure`: Skips TLS certificate verification and sends the ONVIF password as plaintext (`PasswordText`) instead of a hashed digest (`PasswordDigest`). Some cameras reject the digest token and only accept plaintext. This weakens connection security, so only enable it on a trusted local network.
+- `ignore_time_mismatch`: ONVIF authentication tokens include a timestamp, and a camera will reject the token if its clock differs too much from Frigate's. Enabling this makes Frigate compensate for the time offset so authentication can still succeed. Running NTP on both the camera and the Frigate host is the recommended fix; only use this in a "safe" environment, as it slightly weakens token validation.
+
If your camera has multiple ONVIF profiles, you can specify which one to use for PTZ control with the `profile` option, matched by token or name. When not set, Frigate selects the first profile with a valid PTZ configuration. Check the Frigate debug logs (`frigate.ptz.onvif: debug`) to see available profile names and tokens for your camera.
An ONVIF-capable camera that supports relative movement within the field of view (FOV) can also be configured to automatically track moving objects and keep them in the center of the frame. For autotracking setup, see the [autotracking](autotracking.md) docs.
@@ -174,7 +181,7 @@ The FeatureList on the [ONVIF Conformant Products Database](https://www.onvif.or
| Hikvision DS-2DE3A404IWG-E/W | ✅ | ✅ | |
| Reolink | ✅ | ❌ | |
| Speco O8P32X | ✅ | ❌ | |
-| Sunba 405-D20X | ✅ | ❌ | Incomplete ONVIF support reported on original, and 4k models. All models are suspected incompatable. |
+| Sunba 405-D20X | ✅ | ❌ | Incomplete ONVIF support reported on original, and 4k models. All models are suspected incompatible. |
| Tapo | ✅ | ❌ | Many models supported, ONVIF Service Port: 2020 |
| Uniview IPC672LR-AX4DUPK | ✅ | ❌ | Firmware says FOV relative movement is supported, but camera doesn't actually move when sending ONVIF commands |
| Uniview IPC6612SR-X33-VG | ✅ | ✅ | Leave `calibrate_on_startup` as `False`. A user has reported that zooming with `absolute` is working. |
@@ -187,7 +194,7 @@ Camera groups let you organize cameras together with a shared name and icon, mak
-On the Live dashboard, press the **+** icon in the main navigation to add a new camera group. Configure the group name, select which cameras to include, choose an icon, and set the display order.
+On the Live dashboard, press the **pencil icon** in the main navigation to add a new camera group. Configure the group name, select which cameras to include, choose an icon, and set the display order.
diff --git a/docs/docs/configuration/index.md b/docs/docs/configuration/config.md
similarity index 65%
rename from docs/docs/configuration/index.md
rename to docs/docs/configuration/config.md
index 84f9780784..edb6d0cd74 100644
--- a/docs/docs/configuration/index.md
+++ b/docs/docs/configuration/config.md
@@ -1,5 +1,5 @@
---
-id: index
+id: config
title: Frigate Configuration
---
@@ -7,15 +7,58 @@ import ConfigTabs from "@site/src/components/ConfigTabs";
import TabItem from "@theme/TabItem";
import NavPath from "@site/src/components/NavPath";
-Frigate can be configured through the **Settings UI** or by editing the YAML configuration file directly. The Settings UI is the recommended approach — it provides validation and a guided experience for all configuration options.
+Frigate can be configured through the **Settings UI** or by editing the YAML configuration file directly. The Settings UI is the recommended approach. It provides validation and a guided experience for all configuration options.
-It is recommended to start with a minimal configuration and add to it as described in [the getting started guide](../guides/getting_started.md).
+## Using the Settings UI
+
+The Settings UI groups every configuration option into sections that are listed in the left-hand menu. Each section presents a guided form with validation, so you don't need to remember the structure of the YAML or look up option names by hand.
+
+### Global vs. camera-level configuration
+
+Settings are organized into two scopes:
+
+- **Global configuration**: values under apply to every camera by default. This is where you set the baseline behavior for object detection, recording, snapshots, motion, and so on.
+- **Camera configuration**: values under apply to a single camera. Use the camera selector button at the top of these pages to choose which camera you are editing.
+
+When a camera-level section is left untouched, the camera simply inherits the global values. Changing a value on a camera page **overrides** the global value for that camera only: the global setting and every other camera are unaffected. This mirrors how the YAML works, where a value set under `cameras.` takes precedence over the same value set at the top level.
+
+To undo an override and go back to inheriting from the parent scope, use the reset button at the bottom of the section:
+
+- On a camera section, the button is labeled **Reset to Global** and restores the camera to the global value.
+- On a global section, the button is labeled **Reset to Default** and restores Frigate's built-in default.
+
+Resetting asks for confirmation and cannot be undone once applied.
+
+### Saving changes and the Save All button
+
+Edits are not applied until you save them. As soon as you change a value, the UI tracks it as a pending change:
+
+- The edited section shows a **Modified** badge, and the changed fields are highlighted.
+- A **You have unsaved changes** notice appears above the section's **Save** and **Undo** buttons. **Save** commits just that section; **Undo** discards its pending edits.
+
+Because pending changes can span multiple sections (and multiple cameras), the header provides a **Save All** button that writes every pending change at once. Next to it, **Review pending changes** opens a summary that lists each pending edit with its scope (Global or a specific camera), the affected field, and the new value, so you can confirm exactly what will be written before committing. **Undo All** discards every pending change across all sections.
+
+### Restart-required indicators
+
+Most settings take effect immediately, but some require Frigate to restart before they apply. Fields that require a restart are marked with a small restart icon and a **Restart required** tooltip next to the field label.
+
+When you save a change that touches one of these fields, Frigate confirms the save and reminds you that a restart is needed (for example, _"Settings saved successfully. Restart Frigate to apply your changes."_). The notification includes a one-click **Restart Frigate** action so you can apply the change right away, or you can continue editing and restart later.
+
+### The colored dots in the camera configuration menu
+
+When you are working under , small colored dots can appear next to a section's name in the menu. They give you an at-a-glance summary of that section's state for the selected camera:
+
+- **Blue dot**: this section **overrides the global configuration**. One or more values in the section have been set specifically for this camera and differ from the global defaults.
+- **Profile-colored dot**: when you are viewing a [camera profile](./profiles.md), a dot in that profile's assigned color indicates the section is **overridden by that profile**. Each profile is given its own distinct color so you can tell at a glance which sections it changes.
+- **Amber dot**: this section has **unsaved changes**. It appears alongside the **Modified** badge whenever you have pending edits in the section that haven't been saved yet.
+
+Hover over any dot to see a tooltip describing what it means. Open a section to see exactly which fields are overridden: the section header indicates how many fields differ from the global (or base) configuration.
## Configuration File Location
-For users who prefer to edit the YAML configuration file directly:
+For users who prefer to edit the YAML configuration file directly, it is recommended to start with a minimal configuration and add to it as described in [the getting started guide](../guides/getting_started.md).
-- **Home Assistant App:** `/addon_configs//config.yml` — see [directory list](#accessing-app-config-dir)
+- **Home Assistant App:** `/addon_configs//config.yml` (see [directory list](#accessing-app-config-dir))
- **All other installations:** Map to `/config/config.yml` inside the container
It can be named `config.yml` or `config.yaml`, but if both files exist `config.yml` will be preferred and `config.yaml` will be ignored.
@@ -57,7 +100,7 @@ VS Code supports JSON schemas for automatically validating configuration files.
## Environment Variable Substitution
-Frigate supports the use of environment variables starting with `FRIGATE_` **only** where specifically indicated in the [reference config](./reference.md). For example, the following values can be replaced at runtime by using environment variables:
+Frigate supports the use of environment variables starting with `FRIGATE_` **only** where specifically indicated in the [reference config](./advanced/reference.md). For example, the following values can be replaced at runtime by using environment variables:
```yaml
mqtt:
@@ -92,7 +135,7 @@ genai:
## Common configuration examples
-Here are some common starter configuration examples. These can be configured through the Settings UI or via YAML. Refer to the [reference config](./reference.md) for detailed information about all config values.
+Here are some common starter configuration examples. These can be configured through the Settings UI or via YAML. Refer to the [reference config](./advanced/reference.md) for detailed information about all config values.
### Raspberry Pi Home Assistant App with USB Coral
@@ -110,10 +153,10 @@ Here are some common starter configuration examples. These can be configured thr
1. Navigate to and configure the MQTT connection to your Home Assistant Mosquitto broker
2. Navigate to and set **Hardware acceleration arguments** to `Raspberry Pi (H.264)`
-3. Navigate to and add a detector with **Type** `EdgeTPU` and **Device** `usb`
+3. Navigate to and add a detector with **Type** `EdgeTPU` and **Device** `usb`
4. Navigate to and set **Enable recording** to on, **Motion retention > Retention days** to `7`, **Alert retention > Event retention > Retention days** to `30`, **Alert retention > Event retention > Retention mode** to `motion`, **Detection retention > Event retention > Retention days** to `30`, **Detection retention > Event retention > Retention mode** to `motion`
5. Navigate to and set **Enable snapshots** to on, **Snapshot retention > Default retention** to `30`
-6. Navigate to and add your camera with the appropriate RTSP stream URL
+6. Navigate to and add your camera with the appropriate RTSP stream URL
7. Navigate to to add a motion mask for the camera timestamp
@@ -189,10 +232,10 @@ cameras:
1. Navigate to and set **Enable MQTT** to off
2. Navigate to and set **Hardware acceleration arguments** to `VAAPI (Intel/AMD GPU)`
-3. Navigate to and add a detector with **Type** `EdgeTPU` and **Device** `usb`
+3. Navigate to and add a detector with **Type** `EdgeTPU` and **Device** `usb`
4. Navigate to and set **Enable recording** to on, **Motion retention > Retention days** to `7`, **Alert retention > Event retention > Retention days** to `30`, **Alert retention > Event retention > Retention mode** to `motion`, **Detection retention > Event retention > Retention days** to `30`, **Detection retention > Event retention > Retention mode** to `motion`
5. Navigate to and set **Enable snapshots** to on, **Snapshot retention > Default retention** to `30`
-6. Navigate to and add your camera with the appropriate RTSP stream URL
+6. Navigate to and add your camera with the appropriate RTSP stream URL
7. Navigate to to add a motion mask for the camera timestamp
@@ -266,11 +309,11 @@ cameras:
1. Navigate to and configure the connection to your MQTT broker
2. Navigate to and set **Hardware acceleration arguments** to `VAAPI (Intel/AMD GPU)`
-3. Navigate to and add a detector with **Type** `openvino` and **Device** `AUTO`
-4. Navigate to and configure the OpenVINO model path and settings
+3. Navigate to and add a detector with **Type** `openvino` and **Device** `AUTO`
+4. On the same page, in the **Custom Model** tab, configure the OpenVINO model path and settings
5. Navigate to and set **Enable recording** to on, **Motion retention > Retention days** to `7`, **Alert retention > Event retention > Retention days** to `30`, **Alert retention > Event retention > Retention mode** to `motion`, **Detection retention > Event retention > Retention days** to `30`, **Detection retention > Event retention > Retention mode** to `motion`
6. Navigate to and set **Enable snapshots** to on, **Snapshot retention > Default retention** to `30`
-7. Navigate to and add your camera with the appropriate RTSP stream URL
+7. Navigate to and add your camera with the appropriate RTSP stream URL
8. Navigate to to add a motion mask for the camera timestamp
diff --git a/docs/docs/configuration/custom_classification/object_classification.md b/docs/docs/configuration/custom_classification/object_classification.md
index 6368190705..2bfe2c31cc 100644
--- a/docs/docs/configuration/custom_classification/object_classification.md
+++ b/docs/docs/configuration/custom_classification/object_classification.md
@@ -137,7 +137,7 @@ If examples for some of your classes do not appear in the grid, you can continue
:::tip Diversity matters far more than volume
-Selecting dozens of nearly identical images is one of the fastest ways to degrade model performance. MobileNetV2 can overfit quickly when trained on homogeneous data — the model learns what _that exact moment_ looked like rather than what actually defines the class. **This is why Frigate does not implement bulk training in the UI.**
+Selecting dozens of nearly identical images is one of the fastest ways to degrade model performance. MobileNetV2 can overfit quickly when trained on homogeneous data. The model learns what _that exact moment_ looked like rather than what actually defines the class. **This is why Frigate does not implement bulk training in the UI.**
For more detail, see [Frigate Tip: Best Practices for Training Face and Custom Classification Models](https://github.com/blakeblackshear/frigate/discussions/21374).
@@ -149,9 +149,16 @@ For more detail, see [Frigate Tip: Best Practices for Training Face and Custom C
- **The wizard is just the starting point**: You don't need to find and label every class upfront. Missing classes will naturally appear in Recent Classifications, and those images tend to be more valuable because they represent new conditions and edge cases.
- **Problem framing**: Keep classes visually distinct and relevant to the chosen object types.
- **Preprocessing**: Ensure examples reflect object crops similar to Frigate's boxes; keep the subject centered.
-- **Labels**: Keep label names short and consistent; include a `none` class if you plan to ignore uncertain predictions for sub labels.
+- **Crop size**: Aim for crops of at least 100×100 pixels (a 10,000 pixel area). Crops smaller than ~80×80 get stretched 3-7× by the model's 224×224 input resize and tend to collapse into a generic "blob" region of feature space where identity becomes unreliable. If most of your detections are small because the camera is far from the subject, consider repositioning the camera for closer crops.
+- **Class balance**: Aim to keep your largest class within ~3× the count of your smallest. Beyond that, the model becomes biased toward the dominant class and tends to default borderline predictions to it (the "everything looks like Buddy" failure mode).
- **Threshold**: Tune `threshold` per model to reduce false assignments. Start at `0.8` and adjust based on validation.
+:::tip `none` works differently from named classes
+
+Named classes work best with visually uniform examples. Every Buddy photo should look like Buddy. The `none` class needs the opposite: visual diversity across sizes, framings, and qualities, because at inference it has to absorb everything that isn't one of your named classes. Don't apply the same "only keep large, well-framed images" rule to `none` that you would to a named class. Mix in small crops, partial views, and false positives deliberately - otherwise the model has no signal for "small/ambiguous thing = not one of my known classes" and will force those crops into a named class by default.
+
+:::
+
## Debugging Classification Models
To troubleshoot issues with object classification models, enable debug logging to see detailed information about classification attempts, scores, and consensus calculations.
diff --git a/docs/docs/configuration/custom_classification/state_classification.md b/docs/docs/configuration/custom_classification/state_classification.md
index 8b32857d0e..7d479946bf 100644
--- a/docs/docs/configuration/custom_classification/state_classification.md
+++ b/docs/docs/configuration/custom_classification/state_classification.md
@@ -103,7 +103,7 @@ Once some images are assigned, training will begin automatically.
:::tip Diversity matters far more than volume
-Selecting dozens of nearly identical images is one of the fastest ways to degrade model performance. MobileNetV2 can overfit quickly when trained on homogeneous data — the model learns what _that exact moment_ looked like rather than what actually defines the state. This often leads to models that work perfectly under the original conditions but become unstable when day turns to night, weather changes, or seasonal lighting shifts. **This is why Frigate does not implement bulk training in the UI.**
+Selecting dozens of nearly identical images is one of the fastest ways to degrade model performance. MobileNetV2 can overfit quickly when trained on homogeneous data. The model learns what _that exact moment_ looked like rather than what actually defines the state. This often leads to models that work perfectly under the original conditions but become unstable when day turns to night, weather changes, or seasonal lighting shifts. **This is why Frigate does not implement bulk training in the UI.**
For more detail, see [Frigate Tip: Best Practices for Training Face and Custom Classification Models](https://github.com/blakeblackshear/frigate/discussions/21374).
diff --git a/docs/docs/configuration/face_recognition.md b/docs/docs/configuration/face_recognition.md
index 035e4f4e80..96a29c0a9e 100644
--- a/docs/docs/configuration/face_recognition.md
+++ b/docs/docs/configuration/face_recognition.md
@@ -6,6 +6,7 @@ title: Face Recognition
import ConfigTabs from "@site/src/components/ConfigTabs";
import TabItem from "@theme/TabItem";
import NavPath from "@site/src/components/NavPath";
+import FaqItem from "@site/src/components/FaqItem";
Face recognition identifies known individuals by matching detected faces with previously learned facial data. When a known `person` is recognized, their name will be added as a `sub_label`. This information is included in the UI, filters, as well as in notifications.
@@ -19,7 +20,7 @@ Face recognition requires a one-time internet connection to download detection a
### Face Detection
-When running a Frigate+ model (or any custom model that natively detects faces) should ensure that `face` is added to the [list of objects to track](../plus/#available-label-types) either globally or for a specific camera. This will allow face detection to run at the same time as object detection and be more efficient.
+When running a Frigate+ model (or any custom model that natively detects faces) should ensure that `face` is added to the [list of objects to track](../plus/index.md#available-label-types) either globally or for a specific camera. This will allow face detection to run at the same time as object detection and be more efficient.
When running a default COCO model or another model that does not include `face` as a detectable label, face detection will run via CV2 using a lightweight DNN model that runs on the CPU. In this case, you should _not_ define `face` in your list of objects to track.
@@ -86,7 +87,7 @@ Navigate to .
- **Detection threshold**: Face detection confidence score required before recognition runs. This field only applies to the standalone face detection model; `min_score` should be used to filter for models that have face detection built in.
- Default: `0.7`
- **Minimum face area**: Minimum size (in pixels) a face must be before recognition runs. Depending on the resolution of your camera's `detect` stream, you can increase this value to ignore small or distant faces.
- - Default: `500` pixels
+ - Default: `750` pixels
@@ -95,7 +96,7 @@ Navigate to .
face_recognition:
enabled: true
detection_threshold: 0.7
- min_area: 500
+ min_area: 750
```
@@ -151,6 +152,14 @@ Follow these steps to begin:
## Creating a Robust Training Set
+:::tip
+
+**The short version:** Start with a few clear, front-facing photos of each person. As faces are detected in the Recent Recognitions tab, train clear images that scored lower, adding variety (different angles, lighting, and expressions) slowly. Diversity matters far more than volume, and low-quality images hurt recognition more than they help.
+
+For a step-by-step narrative of these best practices (and the same principles applied to state and object classification), see the [Frigate Tips: Best Practices for Training](https://github.com/blakeblackshear/frigate/discussions/21374) discussion.
+
+:::
+
The number of images needed for a sufficient training set for face recognition varies depending on several factors:
- Diversity of the dataset: A dataset with diverse images, including variations in lighting, pose, and facial expressions, will require fewer images per person than a less diverse dataset.
@@ -171,7 +180,7 @@ When choosing images to include in the face training set it is recommended to al
- If it is difficult to make out details in a persons face it will not be helpful in training.
- Avoid images with extreme under/over-exposure.
- Avoid blurry / pixelated images.
-- Avoid training on infrared (gray-scale). The models are trained on color images and will be able to extract features from gray-scale images.
+- Avoid training on infrared (gray-scale). The models are trained on color images and will not be able to extract features from gray-scale images.
- Using images of people wearing hats / sunglasses may confuse the model.
- Do not upload too many similar images at the same time, it is recommended to train no more than 4-6 similar images for each person to avoid over-fitting.
@@ -181,9 +190,27 @@ When choosing images to include in the face training set it is recommended to al
The Recent Recognitions tab in the face library displays recent face recognition attempts. Detected face images are grouped according to the person they were identified as potentially matching.
-Each face image is labeled with a name (or `Unknown`) along with the confidence score of the recognition attempt. While each image can be used to train the system for a specific person, not all images are suitable for training.
+Each face image is labeled with a name (or `Unknown`) along with the confidence score of that recognition attempt. Images are grouped by the person they were matched against, not by who they actually are, so a group labeled with a person's name can contain a crop that is really someone else but happened to score as a partial match. The name and score shown on each individual crop describe that single attempt.
-Refer to the guidelines below for best practices on selecting images for training.
+While each image can be used to train the system for a specific person, not all images are suitable for training. Refer to the guidelines below for best practices on selecting images for training.
+
+### How Frigate Decides Who a Person Is
+
+Recognition does not happen one frame at a time. While a `person` is in view, Frigate runs face recognition on many frames, not just a single frame. The final `sub_label` is decided from all of those attempts together, weighted by the area of each face (larger, closer faces count more), not from any single frame.
+
+This has a few practical consequences:
+
+- A handful of wrong guesses on blurry or distant frames usually do not change the result. If Frigate sees a person as "Tom, Tom, Sam, Tom, Tom," it will still conclude the person was Tom.
+- The goal is not for every individual face crop to be correct. The goal is for each person to be recognized correctly overall, across all the faces captured while they were present.
+- A single very high confidence match will not by itself assign a sub label. Recognition must be consistent. See [I see scores above the threshold in the Recent Recognitions tab, but a sub label wasn't assigned?](#i-see-scores-above-the-threshold-in-the-recent-recognitions-tab-but-a-sub-label-wasnt-assigned) below.
+
+### Which Faces Are Worth Training?
+
+Whether a face is worth training has little to do with what it was recognized as. A crop is a good training candidate when all of these are true:
+
+- It did not already score high and correctly. Faces that are already recognized confidently add little and increase the risk of over-fitting.
+- It is clear enough to be useful: not blurry, not heavily off-axis, not infrared (gray-scale). If it is hard for you to make out the face, it will not help the model.
+- It adds something new: a different angle, lighting, expression, or distance than what you already have.
### Step 1 - Building a Strong Foundation
@@ -199,14 +226,16 @@ Once front-facing images are performing well, start choosing slightly off-angle
## FAQ
-### How do I debug Face Recognition issues?
+### Getting Recognition Working
+
+
Start with the [Usage](#usage) section and re-read the [Model Requirements](#model-requirements) above.
1. Ensure `person` is being _detected_. A `person` will automatically be scanned by Frigate for a face. Any detected faces will appear in the Recent Recognitions tab in the Frigate UI's Face Library.
If you are using a Frigate+ or `face` detecting model:
- - Watch the debug view (Settings --> Debug) to ensure that `face` is being detected along with `person`.
+ - Watch the [debug view](/usage/live#the-single-camera-view) to ensure that `face` is being detected along with `person`.
- You may need to adjust the `min_score` for the `face` object if faces are not being detected.
If you are **not** using a Frigate+ or `face` detecting model:
@@ -217,21 +246,47 @@ Start with the [Usage](#usage) section and re-read the [Model Requirements](#mod
- Make sure you have trained at least one face per the recommendations above.
- Adjust `recognition_threshold` settings per the suggestions [above](#advanced-configuration).
-### Detection does not work well with blurry images?
+
-Accuracy is definitely a going to be improved with higher quality cameras / streams. It is important to look at the DORI (Detection Observation Recognition Identification) range of your camera, if that specification is posted. This specification explains the distance from the camera that a person can be detected, observed, recognized, and identified. The identification range is the most relevant here, and the distance listed by the camera is the furthest that face recognition will realistically work.
+
+
+Face recognition does not run on the recording stream, this would be suboptimal for many reasons:
+
+1. The latency of accessing the recordings means the notifications would not include the names of recognized people because recognition would not complete until after.
+2. The embedding models used run on a set image size, so larger images will be scaled down to match this anyway.
+3. Motion clarity is much more important than extra pixels, over-compression and motion blur are much more detrimental to results than resolution.
+
+
+
+### Improving Accuracy and Training
+
+
+
+Accuracy is definitely going to be improved with higher quality cameras / streams. It is important to look at the DORI (Detection Observation Recognition Identification) range of your camera, if that specification is posted. This specification explains the distance from the camera that a person can be detected, observed, recognized, and identified. The identification range is the most relevant here, and the distance listed by the camera is the furthest that face recognition will realistically work.
Some users have also noted that setting the stream in camera firmware to a constant bit rate (CBR) leads to better image clarity than with a variable bit rate (VBR).
-### Why can't I bulk upload photos?
+
+
+
+
+The embedding models are trained on color images, so gray-scale and infrared (IR) faces sit in a different feature distribution and are more easily confused with other people. Prefer color images, and avoid mixing gray-scale samples in early while you are building a foundation. If someone only ever appears at night, gray-scale training is acceptable, but keep those samples limited and as clear as possible, and add them only once color recognition is stable for your other people.
+
+
+
+
It is important to methodically add photos to the library, bulk importing photos (especially from a general photo library) will lead to over-fitting in that particular scenario and hurt recognition performance.
-### Why can't I bulk reprocess faces?
+
+
+
Face embedding models work by breaking apart faces into different features. This means that when reprocessing an image, only images from a similar angle will have its score affected.
-### Why do unknown people score similarly to known people?
+
+
+
This can happen for a few different reasons, but this is usually an indicator that the training set needs to be improved. This is often related to over-fitting:
@@ -241,33 +296,54 @@ This can happen for a few different reasons, but this is usually an indicator th
Review your face collections and remove most of the unclear or low-quality images. Then, use the **Reprocess** button on each face in the **Train** tab to evaluate how the changes affect recognition scores.
-Avoid training on images that already score highly, as this can lead to over-fitting. Instead, focus on relatively clear images that score lower - ideally with different lighting, angles, and conditions—to help the model generalize more effectively.
+Avoid training on images that already score highly, as this can lead to over-fitting. Instead, focus on relatively clear images that score lower (ideally with different lighting, angles, and conditions) to help the model generalize more effectively.
-### Frigate misidentified a face. Can I tell it that a face is "not" a specific person?
+
+
+
+
+Only if it is a good image. Reassigning a face does add it to that person's training set, but two things are true at once:
+
+- Reassigning a single misclassified frame has a small effect. The image is weighted against every other sample for that person, so correcting 1 frame out of 20 will not move recognition much. Occasional wrong guesses on poor frames are normal and do not need to be fixed.
+- Reassigning a poor image (blurry, off-angle, low-resolution, gray-scale) can hurt more than the misidentification did, because low-quality samples degrade recognition for that whole person.
+
+So the decision is about image quality, not about the wrong label. If the crop is clear, well-lit, and reasonably front-facing, and it scored low or was wrong, assigning it to the correct person is useful. If you can barely make out the face yourself, ignore it; do not train it just to correct the label.
+
+If a person is repeatedly misidentified, do not keep reassigning the same frame. Instead, remove low-quality or misleading images and add a few high-quality samples to the correct person. See [Why do unknown people score similarly to known people?](#why-do-unknown-people-score-similarly-to-known-people) above.
+
+
+
+
No, face recognition does not support negative training (i.e., explicitly telling it who someone is _not_). Instead, the best approach is to improve the training data by using a more diverse and representative set of images for each person.
For more guidance, refer to the section above on improving recognition accuracy.
-### I see scores above the threshold in the Recent Recognitions tab, but a sub label wasn't assigned?
+This also applies to a stranger who is repeatedly matched to a known person (for example, a delivery driver recognized as you). Do not create a profile for them and do not reassign their faces to yourself, as this pollutes your training set and makes recognition worse. Leave the detection as unknown and improve the known person's training set instead. Face recognition learns who someone is, not who they are not.
-The Frigate considers the recognition scores across all recognition attempts for each person object. The scores are continually weighted based on the area of the face, and a sub label will only be assigned to person if a person is confidently recognized consistently. This avoids cases where a single high confidence recognition would throw off the results.
+
-### Can I use other face recognition software like DoubleTake at the same time as the built in face recognition?
+
+
+Frigate considers the recognition scores across all recognition attempts for each person object. The scores are continually weighted based on the area of the face, and a sub label will only be assigned to person if a person is confidently recognized consistently. This avoids cases where a single high confidence recognition would throw off the results.
+
+
+
+### Compatibility and Maintenance
+
+
No, using another face recognition service will interfere with Frigate's built in face recognition. When using double-take the sub_label feature must be disabled if the built in face recognition is also desired.
-### Does face recognition run on the recording stream?
+
-Face recognition does not run on the recording stream, this would be suboptimal for many reasons:
-
-1. The latency of accessing the recordings means the notifications would not include the names of recognized people because recognition would not complete until after.
-2. The embedding models used run on a set image size, so larger images will be scaled down to match this anyway.
-3. Motion clarity is much more important than extra pixels, over-compression and motion blur are much more detrimental to results than resolution.
-
-### I get an unknown error when taking a photo directly with my iPhone
+
By default iOS devices will use HEIC (High Efficiency Image Container) for images, but this format is not supported for uploads. Choosing `large` as the format instead of `original` will use JPG which will work correctly.
-### How can I delete the face database and start over?
+
+
+
Frigate does not store anything in its database related to face recognition. You can simply delete all of your faces through the Frigate UI or remove the contents of the `/media/frigate/clips/faces` directory.
+
+
diff --git a/docs/docs/configuration/ffmpeg_presets.md b/docs/docs/configuration/ffmpeg_presets.md
index 3333882801..5c1d0fc3b1 100644
--- a/docs/docs/configuration/ffmpeg_presets.md
+++ b/docs/docs/configuration/ffmpeg_presets.md
@@ -7,33 +7,33 @@ import ConfigTabs from "@site/src/components/ConfigTabs";
import TabItem from "@theme/TabItem";
import NavPath from "@site/src/components/NavPath";
-Some presets of FFmpeg args are provided by default to make the configuration easier. All presets can be seen in [this file](https://github.com/blakeblackshear/frigate/blob/master/frigate/ffmpeg_presets.py).
+Frigate ships with a set of FFmpeg presets to keep your configuration short and readable. Each preset expands to a longer list of FFmpeg arguments at runtime. You can see exactly what every preset expands to in [this file](https://github.com/blakeblackshear/frigate/blob/master/frigate/ffmpeg_presets.py).
-### Hwaccel Presets
+In the config file you reference a preset by its name (for example, `preset-vaapi`). In the UI, the same preset is shown with a friendly label (for example, **VAAPI (Intel/AMD GPU)**). Both refer to the same thing: the tables below list the config name alongside the label you'll see in the UI.
-It is highly recommended to use hwaccel presets in the config. These presets not only replace the longer args, but they also give Frigate hints of what hardware is available and allows Frigate to make other optimizations using the GPU such as when encoding the birdseye restream or when scaling a stream that has a size different than the native stream size.
+### Hwaccel (Hardware Acceleration) Presets {#hwaccel-presets}
-See [the hwaccel docs](/configuration/hardware_acceleration_video.md) for more info on how to setup hwaccel for your GPU / iGPU.
+Hardware acceleration arguments tell FFmpeg to decode your camera's video stream on a GPU or integrated graphics chip instead of the CPU, which dramatically lowers CPU usage. Using a preset is highly recommended. Beyond replacing a long list of arguments, each preset also tells Frigate what hardware is available so it can offload additional work to the GPU, for example, encoding the Birdseye restream or scaling a stream whose resolution differs from the camera's native size.
-| Preset | Usage | Other Notes |
-| --------------------- | ------------------------------ | ----------------------------------------------------- |
-| preset-rpi-64-h264 | 64 bit Rpi with h264 stream | |
-| preset-rpi-64-h265 | 64 bit Rpi with h265 stream | |
-| preset-vaapi | Intel & AMD VAAPI | Check hwaccel docs to ensure correct driver is chosen |
-| preset-intel-qsv-h264 | Intel QSV with h264 stream | If issues occur recommend using vaapi preset instead |
-| preset-intel-qsv-h265 | Intel QSV with h265 stream | If issues occur recommend using vaapi preset instead |
-| preset-nvidia | Nvidia GPU | |
-| preset-jetson-h264 | Nvidia Jetson with h264 stream | |
-| preset-jetson-h265 | Nvidia Jetson with h265 stream | |
-| preset-rkmpp | Rockchip MPP | Use image with \*-rk suffix and privileged mode |
+See [the hardware acceleration docs](/configuration/hardware_acceleration_video.md) for details on setting up hardware acceleration for your GPU / iGPU, then select the preset that matches your hardware.
-Select the appropriate hwaccel preset for your hardware.
+| Preset (YAML config) | UI Label | Usage | Notes |
+| --------------------- | ----------------------- | --------------------------------- | --------------------------------------------------------------- |
+| preset-rpi-64-h264 | Raspberry Pi (H.264) | 64-bit Raspberry Pi, H.264 stream | |
+| preset-rpi-64-h265 | Raspberry Pi (H.265) | 64-bit Raspberry Pi, H.265 stream | |
+| preset-vaapi | VAAPI (Intel/AMD GPU) | Intel or AMD GPU via VAAPI | Check the hwaccel docs to ensure the correct driver is selected |
+| preset-intel-qsv-h264 | Intel QuickSync (H.264) | Intel QuickSync, H.264 stream | If you have issues, use the VAAPI preset instead |
+| preset-intel-qsv-h265 | Intel QuickSync (H.265) | Intel QuickSync, H.265 stream | If you have issues, use the VAAPI preset instead |
+| preset-nvidia | NVIDIA GPU | NVIDIA GPU | |
+| preset-jetson-h264 | NVIDIA Jetson (H.264) | NVIDIA Jetson, H.264 stream | |
+| preset-jetson-h265 | NVIDIA Jetson (H.265) | NVIDIA Jetson, H.265 stream | |
+| preset-rkmpp | Rockchip RKMPP | Rockchip MPP | Use an image with the `-rk` suffix and run in privileged mode |
1. Navigate to and set **Hardware acceleration arguments** to the appropriate preset for your hardware.
-2. To override for a specific camera, navigate to and set **Hardware acceleration arguments** for that camera.
+2. To override for a specific camera, navigate to and set **Hardware acceleration arguments** for that camera.
@@ -53,25 +53,25 @@ cameras:
### Input Args Presets
-Input args presets help make the config more readable and handle use cases for different types of streams to ensure maximum compatibility.
+Input arguments are passed to FFmpeg before your camera source and control how Frigate connects to and reads the stream: the transport protocol, timeouts, reconnection behavior, and how the stream is probed. The right input args ensure a reliable connection and maximum compatibility for each type of stream.
-See [the camera specific docs](/configuration/camera_specific.md) for more info on non-standard cameras and recommendations for using them in Frigate.
+See [the camera-specific docs](/configuration/camera_specific.md) for more on non-standard cameras and recommendations for using them in Frigate.
-| Preset | Usage | Other Notes |
-| -------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------ |
-| preset-http-jpeg-generic | HTTP Live Jpeg | Recommend restreaming live jpeg instead |
-| preset-http-mjpeg-generic | HTTP Mjpeg Stream | Recommend restreaming mjpeg stream instead |
-| preset-http-reolink | Reolink HTTP-FLV Stream | Only for reolink http, not when restreaming as rtsp |
-| preset-rtmp-generic | RTMP Stream | |
-| preset-rtsp-generic | RTSP Stream | This is the default when nothing is specified |
-| preset-rtsp-restream | RTSP Stream from restream | Use for rtsp restream as source for frigate |
-| preset-rtsp-restream-low-latency | RTSP Stream from restream | Use for rtsp restream as source for frigate to lower latency, may cause issues with some cameras |
-| preset-rtsp-udp | RTSP Stream via UDP | Use when camera is UDP only |
-| preset-rtsp-blue-iris | Blue Iris RTSP Stream | Use when consuming a stream from Blue Iris |
+| Preset (config) | UI Label | Usage | Notes |
+| -------------------------------- | ----------------------------------------- | --------------------------- | ------------------------------------------------------------------------------- |
+| preset-http-jpeg-generic | HTTP JPEG (Generic) | HTTP live JPEG | Restreaming the live JPEG is recommended instead |
+| preset-http-mjpeg-generic | HTTP MJPEG (Generic) | HTTP MJPEG stream | Restreaming the MJPEG stream is recommended instead |
+| preset-http-reolink | HTTP - Reolink Cameras | Reolink HTTP-FLV stream | Only for Reolink HTTP, not when restreaming as RTSP |
+| preset-rtmp-generic | RTMP (Generic) | RTMP stream | |
+| preset-rtsp-generic | RTSP (Generic) | RTSP stream | The default when no input args are specified |
+| preset-rtsp-restream | RTSP - Restream from go2rtc | RTSP stream from a restream | Use when a go2rtc restream is the source for Frigate |
+| preset-rtsp-restream-low-latency | RTSP - Restream from go2rtc (Low Latency) | RTSP stream from a restream | Lowers latency for a go2rtc restream source; may cause issues with some cameras |
+| preset-rtsp-udp | RTSP - UDP | RTSP stream over UDP | Use when the camera only supports UDP |
+| preset-rtsp-blue-iris | RTSP - Blue Iris | Blue Iris RTSP stream | Use when consuming a stream from Blue Iris |
:::warning
-It is important to be mindful of input args when using restream because you can have a mix of protocols. `http` and `rtmp` presets cannot be used with `rtsp` streams. For example, when using a reolink cam with the rtsp restream as a source for record the preset-http-reolink will cause a crash. In this case presets will need to be set at the stream level. See the example below.
+Be mindful of input arguments when restreaming, because you can end up with a mix of protocols. The `http` and `rtmp` presets cannot be used with `rtsp` streams. For example, using a Reolink camera with an RTSP restream as the recording source while `preset-http-reolink` is applied will cause a crash. In cases like this, set the preset at the stream level instead. See the example below.
:::
@@ -96,13 +96,13 @@ cameras:
### Output Args Presets
-Output args presets help make the config more readable and handle use cases for different types of streams to ensure consistent recordings.
+Output arguments are passed to FFmpeg after your camera source and control how recordings are written: which codecs are used and whether audio and video are copied as-is or re-encoded. The right output args ensure consistent, playable recordings for each type of stream.
-| Preset | Usage | Other Notes |
-| -------------------------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| preset-record-generic | Record WITHOUT audio | If your camera doesn't have audio, or if you don't want to record audio, use this option |
-| preset-record-generic-audio-copy | Record WITH original audio | Use this to enable audio in recordings |
-| preset-record-generic-audio-aac | Record WITH transcoded aac audio | This is the default when no option is specified. Use it to transcode audio to AAC. If the source is already in AAC format, use preset-record-generic-audio-copy instead to avoid unnecessary re-encoding |
-| preset-record-mjpeg | Record an mjpeg stream | Recommend restreaming mjpeg stream instead |
-| preset-record-jpeg | Record live jpeg | Recommend restreaming live jpeg instead |
-| preset-record-ubiquiti | Record ubiquiti stream with audio | Recordings with ubiquiti non-standard audio |
+| Preset (config) | UI Label | Usage | Notes |
+| -------------------------------- | ------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| preset-record-generic | Record (Generic, no audio) | Record without audio | Use this if your camera has no audio, or if you don't want to record audio |
+| preset-record-generic-audio-copy | Record (Generic + Copy Audio) | Record with the original audio | Use this to keep the camera's audio in recordings without re-encoding |
+| preset-record-generic-audio-aac | Record (Generic + Audio to AAC) | Record with audio transcoded to AAC | The default when no output args are specified. Transcodes audio to AAC. If the source is already AAC, use `preset-record-generic-audio-copy` to avoid re-encoding |
+| preset-record-mjpeg | Record - MJPEG Cameras | Record an MJPEG stream | Restreaming the MJPEG stream is recommended instead |
+| preset-record-jpeg | Record - JPEG Cameras | Record a live JPEG | Restreaming the live JPEG is recommended instead |
+| preset-record-ubiquiti | Record - Ubiquiti Cameras | Record a Ubiquiti stream with audio | Handles Ubiquiti's non-standard audio format |
diff --git a/docs/docs/configuration/genai/config.md b/docs/docs/configuration/genai/config.md
index a02a313bab..738eb5db3f 100644
--- a/docs/docs/configuration/genai/config.md
+++ b/docs/docs/configuration/genai/config.md
@@ -27,13 +27,12 @@ Running Generative AI models on CPU is not recommended, as high inference times
You must use a vision-capable model with Frigate. The following models are recommended for local deployment:
-| Model | Notes |
-| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `qwen3-vl` | Strong visual and situational understanding, enhanced ability to identify smaller objects and interactions with object. |
-| `qwen3.5` | Strong situational understanding, but missing DeepStack from qwen3-vl leading to worse performance for identifying objects in people's hand and other small details. |
-| `gemma4` | Strong situational understanding, sometimes resorts to more vague terms like 'interacts' instead of assigning a specific action. |
-| `Intern3.5VL` | Relatively fast with good vision comprehension |
-| `gemma3` | Slower model with good vision and temporal understanding |
+| Model | Notes |
+| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `qwen3-vl` | Strong visual and situational understanding, enhanced ability to identify smaller objects and interactions with object. |
+| `qwen3.5` | Strong situational understanding, but missing DeepStack from qwen3-vl leading to worse performance for identifying objects in people's hand and other small details. |
+| `qwen3.6` | Strong situational understanding, similar to qwen3-vl |
+| `gemma4` | Strong situational understanding, sometimes resorts to more vague terms like 'interacts' instead of assigning a specific action. |
:::info
@@ -49,15 +48,14 @@ You should have at least 8 GB of RAM available (or VRAM if running on GPU) to ru
### Model Types: Instruct vs Thinking
-Most vision-language models are available as **instruct** models, which are fine-tuned to follow instructions and respond concisely to prompts. However, some models (such as certain Qwen-VL or minigpt variants) offer both **instruct** and **thinking** versions.
+Vision-language models come in **instruct** variants (fine-tuned to follow instructions and respond concisely), **thinking** variants (fine-tuned for free-form, speculative reasoning), and **hybrid** variants that support both modes per request. Most modern vision-language models are hybrid.
-- **Instruct models** are always recommended for use with Frigate. These models generate direct, relevant, actionable descriptions that best fit Frigate's object and event summary use case.
-- **Reasoning / Thinking models** are fine-tuned for more free-form, open-ended, and speculative outputs, which are typically not concise and may not provide the practical summaries Frigate expects. For this reason, Frigate does **not** recommend or support using thinking models.
+Frigate manages reasoning per task automatically:
-Some models are labeled as **hybrid** (capable of both thinking and instruct tasks). In these cases, it is recommended to disable reasoning / thinking, which is generally model specific (see your models documentation).
+- **Description tasks** (object descriptions, review descriptions, review summaries) are synthesis-only and benefit from concise, direct output, so Frigate disables thinking for these calls when the model exposes a per-request toggle.
+- **Chat** lets you toggle thinking on or off from the composer when the configured model supports it.
-**Recommendation:**
-Always select the `-instruct` or documented instruct/tagged variant of any model you use in your Frigate configuration. If in doubt, refer to your model provider's documentation or model library for guidance on the correct model variant to use.
+You can use a pure instruct, hybrid, or thinking-capable model with Frigate. No extra configuration is required to disable thinking for descriptions.
### llama.cpp
@@ -201,7 +199,7 @@ Cloud Generative AI providers require an active internet connection to send imag
### Ollama Cloud
-Ollama also supports [cloud models](https://ollama.com/cloud), where your local Ollama instance handles requests from Frigate, but model inference is performed in the cloud. Set up Ollama locally, sign in with your Ollama account, and specify the cloud model name in your Frigate config. For more details, see the Ollama cloud model [docs](https://docs.ollama.com/cloud).
+Ollama also supports [cloud models](https://ollama.com/cloud), where model inference is performed in the cloud. You can connect directly to Ollama Cloud by setting `base_url` to `https://ollama.com` and providing an API key. Alternatively, you can run Ollama locally and use a cloud model name so your local instance forwards requests to the cloud. For more details, see the Ollama cloud model [docs](https://docs.ollama.com/cloud).
#### Configuration
@@ -210,7 +208,8 @@ Ollama also supports [cloud models](https://ollama.com/cloud), where your local
1. Navigate to .
- Set **Provider** to `ollama`
- - Set **Base URL** to your local Ollama address (e.g., `http://localhost:11434`)
+ - Set **Base URL** to your local Ollama address (e.g., `http://localhost:11434`) or `https://ollama.com` for direct cloud inference
+ - Set **API key** if required by your endpoint (e.g., when using `https://ollama.com`)
- Set **Model** to the cloud model name
@@ -223,6 +222,16 @@ genai:
model: cloud-model-name
```
+or when using Ollama Cloud directly
+
+```yaml
+genai:
+ provider: ollama
+ base_url: https://ollama.com
+ model: cloud-model-name
+ api_key: your-api-key
+```
+
@@ -284,7 +293,7 @@ Other HTTP options are available, see the [python-genai documentation](https://g
### OpenAI
-OpenAI does not have a free tier for their API. With the release of gpt-4o, pricing has been reduced and each generation should cost fractions of a cent if you choose to go this route.
+OpenAI does not have a free tier for their API.
#### Supported Models
diff --git a/docs/docs/configuration/go2rtc.md b/docs/docs/configuration/go2rtc.md
new file mode 100644
index 0000000000..c7e5b083bb
--- /dev/null
+++ b/docs/docs/configuration/go2rtc.md
@@ -0,0 +1,70 @@
+---
+id: go2rtc
+title: go2rtc
+---
+
+import ConfigTabs from "@site/src/components/ConfigTabs";
+import TabItem from "@theme/TabItem";
+import NavPath from "@site/src/components/NavPath";
+
+Frigate uses the bundled go2rtc to power a number of key features:
+
+- WebRTC or MSE for live viewing with audio, higher resolutions and frame rates than the jsmpeg stream which is limited to the detect stream and does not support audio
+- Live stream support for cameras in Home Assistant Integration
+- RTSP relay for use with other consumers to reduce the number of connections to your camera streams
+
+:::tip[Most users no longer need to configure go2rtc by hand]
+
+The **camera setup wizard** is the recommended way to add cameras. Click **Add Camera** in , and the wizard probes your camera and writes its configuration for you, including the go2rtc restream and the live stream mapping, so go2rtc is set up automatically.
+
+This guide is mainly useful if you are **upgrading from an older version and have existing cameras that don't yet use go2rtc**, or if you want to fine-tune a stream by hand (for example, to transcode a codec your browser can't play). The [go2rtc troubleshooting guide](/troubleshooting/go2rtc) applies regardless of how your cameras were added.
+
+:::
+
+## Adding a go2rtc stream manually
+
+If you added your cameras with the wizard, go2rtc is already configured. You can skip straight to [troubleshooting](/troubleshooting/go2rtc). The steps below are for upgrading users with existing cameras that aren't using go2rtc yet, or for anyone who prefers to configure a stream by hand.
+
+Configure go2rtc to connect to your camera by adding the stream you want to use for live view. Avoid changing any other parts of your config at this step. Note that go2rtc supports [many different stream types](https://github.com/AlexxIT/go2rtc/tree/v1.9.14#module-streams), not just rtsp.
+
+:::tip
+
+For the best experience, set the stream name under `go2rtc` to match the name of your camera so that Frigate will automatically map it and be able to use better live view options for the camera.
+
+See [the live view docs](/configuration/live#setting-streams-for-live-ui) for more information.
+
+:::
+
+
+
+
+Navigate to and click **Add stream**. Give the stream a name (use the camera's name so Frigate can auto-map it - for example, if your camera's name is `back`, use `back` as the go2rtc stream name), then paste the camera's stream URL into the **Source** field. Save the section.
+
+
+
+
+```yaml
+go2rtc:
+ streams:
+ back:
+ - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
+```
+
+
+
+
+After adding this to the config, restart Frigate and try to watch the live stream for a single camera by clicking on it from the dashboard. It should look much clearer and more fluent than the original jsmpeg stream.
+
+### Next steps
+
+1. If the stream you added to go2rtc is also used by Frigate for the `record` or `detect` role, you can migrate your config to pull from the RTSP restream to reduce the number of connections to your camera as shown [here](/configuration/restream#reduce-connections-to-camera).
+2. You can [set up WebRTC](/configuration/live#webrtc-extra-configuration) if your camera supports two-way talk. Note that WebRTC only supports specific audio formats and may require opening ports on your router.
+3. If your camera supports two-way talk, you must configure your stream with `#backchannel=0` to prevent go2rtc from blocking other applications from accessing the camera's audio output. See [preventing go2rtc from blocking two-way audio](/configuration/restream#two-way-talk-restream) in the restream documentation.
+
+## Troubleshooting
+
+If your stream won't play, has no audio, uses excessive CPU, or otherwise misbehaves, see the dedicated [go2rtc troubleshooting guide](/troubleshooting/go2rtc). It walks through how to isolate where the problem is and covers the most common issues: unsupported codecs, H.265/HEVC, audio, WebRTC and two-way talk, hardware-accelerated transcoding with FFmpeg 8, and camera-specific quirks.
+
+## Homekit Configuration
+
+To add camera streams to Homekit Frigate must be configured in docker to use `host` networking mode. Once that is done, you can use the go2rtc WebUI (accessed via port 1984, which is disabled by default) to export a camera to Homekit. Any changes made will automatically be saved to `/config/go2rtc_homekit.yml`.
diff --git a/docs/docs/configuration/hardware_acceleration_video.md b/docs/docs/configuration/hardware_acceleration_video.md
index 7aeecfda95..b9312c327f 100644
--- a/docs/docs/configuration/hardware_acceleration_video.md
+++ b/docs/docs/configuration/hardware_acceleration_video.md
@@ -17,8 +17,6 @@ Some types of hardware acceleration are detected and used automatically, but you
- Check the logs: A message will either say that hardware acceleration was automatically detected, or there will be a warning that no hardware acceleration was automatically detected
- If hardware acceleration is specified in the config, verification can be done by ensuring the logs are free from errors. There is no CPU fallback for hardware acceleration.
-:::info
-
Frigate supports presets for optimal hardware accelerated video decoding:
**AMD**
@@ -49,14 +47,10 @@ Frigate supports presets for optimal hardware accelerated video decoding:
Depending on your system, these presets may not be compatible, and you may need to use manual hwaccel args to take advantage of your hardware. More information on hardware accelerated decoding for ffmpeg can be found here: https://trac.ffmpeg.org/wiki/HWAccelIntro
-:::
-
## Intel-based CPUs
Frigate can utilize most Intel integrated GPUs and Arc GPUs to accelerate video decoding.
-:::info
-
**Recommended hwaccel Preset**
| CPU Generation | Intel Driver | Recommended Preset | Notes |
@@ -68,11 +62,9 @@ Frigate can utilize most Intel integrated GPUs and Arc GPUs to accelerate video
| Intel Arc A-series | iHD / Xe | preset-intel-qsv-\* | |
| Intel Arc B-series | iHD / Xe | preset-intel-qsv-\* | Requires host kernel 6.12+ |
-:::
-
:::note
-The default driver is `iHD`. You may need to change the driver to `i965` by adding the following environment variable `LIBVA_DRIVER_NAME=i965` to your docker-compose file or [in the `config.yml` for HA App users](advanced.md#environment_vars).
+The default driver is `iHD`. You may need to change the driver to `i965` by adding the following environment variable `LIBVA_DRIVER_NAME=i965` to your docker-compose file or [in the `config.yml` for HA App users](advanced/system.md#environment_vars).
See [The Intel Docs](https://www.intel.com/content/www/us/en/support/articles/000005505/processors.html) to figure out what generation your CPU is.
@@ -85,7 +77,7 @@ VAAPI supports automatic profile selection so it will work automatically with bo
-Navigate to and set **Hardware acceleration arguments** to `VAAPI (Intel/AMD GPU)`. For per-camera overrides, navigate to .
+Navigate to and set **Hardware acceleration arguments** to `VAAPI (Intel/AMD GPU)`. For per-camera overrides, navigate to .
@@ -105,7 +97,7 @@ ffmpeg:
-Navigate to and set **Hardware acceleration arguments** to `Intel QuickSync (H.264)`. For per-camera overrides, navigate to .
+Navigate to and set **Hardware acceleration arguments** to `Intel QuickSync (H.264)`. For per-camera overrides, navigate to .
@@ -123,7 +115,7 @@ ffmpeg:
-Navigate to and set **Hardware acceleration arguments** to `Intel QuickSync (H.265)`. For per-camera overrides, navigate to .
+Navigate to and set **Hardware acceleration arguments** to `Intel QuickSync (H.265)`. For per-camera overrides, navigate to .
@@ -136,90 +128,32 @@ ffmpeg:
-### Configuring Intel GPU Stats in Docker
+### Configuring Intel GPU Stats
-Additional configuration is needed for the Docker container to be able to access the `intel_gpu_top` command for GPU stats. There are two options:
+Frigate reads Intel GPU utilization directly from the kernel's per-client DRM usage counters exposed at `/proc//fdinfo/`. This requires:
-1. Run the container as privileged.
-2. Add the `CAP_PERFMON` capability (note: you might need to set the `perf_event_paranoid` low enough to allow access to the performance event system.)
+- Linux kernel **5.19 or newer** for the `i915` driver, or any release of the `xe` driver.
+- Frigate running with permission to read other processes' fdinfo. Running as root inside the container (the default) satisfies this; non-root setups may need `CAP_SYS_PTRACE`.
-#### Run as privileged
+No `intel_gpu_top` binary, `CAP_PERFMON`, privileged mode, or `perf_event_paranoid` tuning is required.
-This method works, but it gives more permissions to the container than are actually needed.
+#### Stats for SR-IOV or specific devices
-##### Docker Compose - Privileged
-
-```yaml
-services:
- frigate:
- ...
- image: ghcr.io/blakeblackshear/frigate:stable
- # highlight-next-line
- privileged: true
-```
-
-##### Docker Run CLI - Privileged
-
-```bash {4}
-docker run -d \
- --name frigate \
- ...
- --privileged \
- ghcr.io/blakeblackshear/frigate:stable
-```
-
-#### CAP_PERFMON
-
-Only recent versions of Docker support the `CAP_PERFMON` capability. You can test to see if yours supports it by running: `docker run --cap-add=CAP_PERFMON hello-world`
-
-##### Docker Compose - CAP_PERFMON
-
-```yaml {5,6}
-services:
- frigate:
- ...
- image: ghcr.io/blakeblackshear/frigate:stable
- cap_add:
- - CAP_PERFMON
-```
-
-##### Docker Run CLI - CAP_PERFMON
-
-```bash {4}
-docker run -d \
- --name frigate \
- ...
- --cap-add=CAP_PERFMON \
- ghcr.io/blakeblackshear/frigate:stable
-```
-
-#### perf_event_paranoid
-
-_Note: This setting must be changed for the entire system._
-
-For more information on the various values across different distributions, see https://askubuntu.com/questions/1400874/what-does-perf-paranoia-level-four-do.
-
-Depending on your OS and kernel configuration, you may need to change the `/proc/sys/kernel/perf_event_paranoid` kernel tunable. You can test the change by running `sudo sh -c 'echo 2 >/proc/sys/kernel/perf_event_paranoid'` which will persist until a reboot. Make it permanent by running `sudo sh -c 'echo kernel.perf_event_paranoid=2 >> /etc/sysctl.d/local.conf'`
-
-#### Stats for SR-IOV or other devices
-
-When using virtualized GPUs via SR-IOV, you need to specify the device path to use to gather stats from `intel_gpu_top`. This example may work for some systems using SR-IOV:
+If the host has more than one Intel GPU (e.g. an iGPU plus a discrete GPU, or SR-IOV virtual functions), pin stats collection to a specific device by setting `intel_gpu_device` to either its PCI bus address or a DRM card/render-node path:
```yaml
telemetry:
stats:
- intel_gpu_device: "sriov"
+ intel_gpu_device: "0000:00:02.0"
```
-For other virtualized GPUs, try specifying the direct path to the device instead:
-
```yaml
telemetry:
stats:
- intel_gpu_device: "drm:/dev/dri/card0"
+ intel_gpu_device: "/dev/dri/card1"
```
-If you are passing in a device path, make sure you've passed the device through to the container.
+When passing a device path, make sure the device is also passed through to the container.
## AMD-based CPUs
@@ -227,7 +161,7 @@ Frigate can utilize modern AMD integrated GPUs and AMD GPUs to accelerate video
### Configuring Radeon Driver
-You need to change the driver to `radeonsi` by adding the following environment variable `LIBVA_DRIVER_NAME=radeonsi` to your docker-compose file or [in the `config.yml` for HA App users](advanced.md#environment_vars).
+You need to change the driver to `radeonsi` by adding the following environment variable `LIBVA_DRIVER_NAME=radeonsi` to your docker-compose file or [in the `config.yml` for HA App users](advanced/system.md#environment_vars).
### Via VAAPI
@@ -236,7 +170,7 @@ VAAPI supports automatic profile selection so it will work automatically with bo
-Navigate to and set **Hardware acceleration arguments** to `VAAPI (Intel/AMD GPU)`. For per-camera overrides, navigate to .
+Navigate to and set **Hardware acceleration arguments** to `VAAPI (Intel/AMD GPU)`. For per-camera overrides, navigate to .
@@ -251,7 +185,7 @@ ffmpeg:
## NVIDIA GPUs
-While older GPUs may work, it is recommended to use modern, supported GPUs. NVIDIA provides a [matrix of supported GPUs and features](https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new). If your card is on the list and supports CUVID/NVDEC, it will most likely work with Frigate for decoding. However, you must also use [a driver version that will work with FFmpeg](https://github.com/FFmpeg/nv-codec-headers/blob/master/README). Older driver versions may be missing symbols and fail to work, and older cards are not supported by newer driver versions. The only way around this is to [provide your own FFmpeg](/configuration/advanced#custom-ffmpeg-build) that will work with your driver version, but this is unsupported and may not work well if at all.
+While older GPUs may work, it is recommended to use modern, supported GPUs. NVIDIA provides a [matrix of supported GPUs and features](https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new). If your card is on the list and supports CUVID/NVDEC, it will most likely work with Frigate for decoding. However, you must also use [a driver version that will work with FFmpeg](https://github.com/FFmpeg/nv-codec-headers/blob/master/README). Older driver versions may be missing symbols and fail to work, and older cards are not supported by newer driver versions. The only way around this is to [provide your own FFmpeg](/configuration/advanced/system#custom-ffmpeg-build) that will work with your driver version, but this is unsupported and may not work well if at all.
A more complete list of cards and their compatible drivers is available in the [driver release readme](https://download.nvidia.com/XFree86/Linux-x86_64/525.85.05/README/supportedchips.html).
@@ -295,7 +229,7 @@ Using `preset-nvidia` ffmpeg will automatically select the necessary profile for
-Navigate to and set **Hardware acceleration arguments** to `NVIDIA GPU`. For per-camera overrides, navigate to .
+Navigate to and set **Hardware acceleration arguments** to `NVIDIA GPU`. For per-camera overrides, navigate to .
@@ -358,7 +292,7 @@ If you are using the HA App, you may need to use the full access variant and tur
-Navigate to and set **Hardware acceleration arguments** to `Raspberry Pi (H.264)` (for H.264 streams) or `Raspberry Pi (H.265)` (for H.265/HEVC streams). For per-camera overrides, navigate to .
+Navigate to and set **Hardware acceleration arguments** to `Raspberry Pi (H.264)` (for H.264 streams) or `Raspberry Pi (H.265)` (for H.265/HEVC streams). For per-camera overrides, navigate to .
@@ -478,7 +412,7 @@ For example, for H264 video, you'll select `preset-jetson-h264`.
-Navigate to and set **Hardware acceleration arguments** to `NVIDIA Jetson (H.264)` (or `NVIDIA Jetson (H.265)` for HEVC streams). For per-camera overrides, navigate to .
+Navigate to and set **Hardware acceleration arguments** to `NVIDIA Jetson (H.264)` (or `NVIDIA Jetson (H.265)` for HEVC streams). For per-camera overrides, navigate to .
@@ -510,7 +444,7 @@ Set the FFmpeg hwaccel preset to enable hardware video processing.
-Navigate to and set **Hardware acceleration arguments** to `Rockchip RKMPP`. For per-camera overrides, navigate to .
+Navigate to and set **Hardware acceleration arguments** to `Rockchip RKMPP`. For per-camera overrides, navigate to .
@@ -543,7 +477,7 @@ Error marking filters as finished
Restarting ffmpeg...
```
-you should try to uprade to FFmpeg 7. This can be done using this config option:
+you should try to upgrade to FFmpeg 7. This can be done using this config option:
```yaml
ffmpeg:
@@ -577,7 +511,7 @@ Set the FFmpeg hwaccel args to enable hardware video processing.
-Navigate to and configure the hardware acceleration args and input args manually for Synaptics hardware. For per-camera overrides, navigate to .
+Navigate to and configure the hardware acceleration args and input args manually for Synaptics hardware. For per-camera overrides, navigate to .
diff --git a/docs/docs/configuration/license_plate_recognition.md b/docs/docs/configuration/license_plate_recognition.md
index c60618fd43..eae92a9815 100644
--- a/docs/docs/configuration/license_plate_recognition.md
+++ b/docs/docs/configuration/license_plate_recognition.md
@@ -6,6 +6,7 @@ title: License Plate Recognition (LPR)
import ConfigTabs from "@site/src/components/ConfigTabs";
import TabItem from "@theme/TabItem";
import NavPath from "@site/src/components/NavPath";
+import FaqItem from "@site/src/components/FaqItem";
Frigate can recognize license plates on vehicles and automatically add the detected characters to the `recognized_license_plate` field or a [known](#matching) name as a `sub_label` to tracked objects of type `car` or `motorcycle`. A common use case may be to read the license plates of cars pulling into a driveway or cars passing by on a street.
@@ -283,8 +284,8 @@ Navigate to Wife's Car** | `ABC-1234`, `ABC-I234` (accounts for potential confusion between the number one and capital letter I) |
| **Known plates > Johnny** | `J*N-*234` (matches JHN-1234 and JMN-I234; `*` matches any number of characters) |
| **Known plates > Sally** | `[S5]LL 1234` (matches both SLL 1234 and 5LL 1234) |
@@ -363,7 +364,7 @@ An example configuration for a dedicated LPR camera using a `license_plate`-dete
Navigate to and set **Enable LPR** to on. Set **Device** to `CPU` (can also be `GPU` if available).
-Navigate to and add your camera streams.
+Navigate to and add your camera streams.
Navigate to .
@@ -473,15 +474,15 @@ Navigate to Camera configuration > FFmpeg" /> and add your camera streams.
+Navigate to and add your camera streams.
Navigate to .
| Field | Description |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
-| **Enable object detection** | Set to off — disables Frigate's standard object detection pipeline |
+| **Enable object detection** | Set to off to disable Frigate's standard object detection pipeline |
| **Detect FPS** | Set to `5`. Increase if necessary, though high values may slow down Frigate's enrichments pipeline and use considerable CPU. |
| **Detect width** | Set to `1920` (recommended value, but depends on your camera) |
| **Detect height** | Set to `1080` (recommended value, but depends on your camera) |
@@ -490,7 +491,7 @@ Navigate to .
| Field | Description |
| -------------------- | -------------------------------------------------------------------------------------- |
-| **Objects to track** | Set to an empty list — required when not using a Frigate+ model for dedicated LPR mode |
+| **Objects to track** | Set to an empty list, required when not using a Frigate+ model for dedicated LPR mode |
Navigate to .
@@ -591,7 +592,9 @@ By selecting the appropriate configuration, users can optimize their dedicated L
## FAQ
-### Why isn't my license plate being detected and recognized?
+### Detection and Recognition
+
+
Ensure that:
@@ -606,29 +609,43 @@ Recognized plates will show as object labels in the debug view and will appear i
If you are still having issues detecting plates, start with a basic configuration and see the debugging tips below.
-### Can I run LPR without detecting `car` or `motorcycle` objects?
+
+
+Can I run LPR without detecting car or motorcycle objects?>}>
In normal LPR mode, Frigate requires a `car` or `motorcycle` to be detected first before recognizing a license plate. If you have a dedicated LPR camera, you can change the camera `type` to `"lpr"` to use the Dedicated LPR Camera algorithm. This comes with important caveats, though. See the [Dedicated LPR Cameras](#dedicated-lpr-cameras) section above.
-### How can I improve detection accuracy?
+
+
+
- Use high-quality cameras with good resolution.
- Adjust `detection_threshold` and `recognition_threshold` values.
- Define a `format` regex to filter out invalid detections.
-### Does LPR work at night?
+
+
+
Yes, but performance depends on camera quality, lighting, and infrared capabilities. Make sure your camera can capture clear images of plates at night.
-### Can I limit LPR to specific zones?
+
+
+
LPR, like other Frigate enrichments, runs at the camera level rather than the zone level. While you can't restrict LPR to specific zones directly, you can control when recognition runs by setting a `min_area` value to filter out smaller detections.
-### How can I match known plates with minor variations?
+
+
+
Use `match_distance` to allow small character mismatches. Alternatively, define multiple variations in `known_plates`.
-### How do I debug LPR issues?
+
+
+### Performance and Troubleshooting
+
+
Start with ["Why isn't my license plate being detected and recognized?"](#why-isnt-my-license-plate-being-detected-and-recognized). If you are still having issues, work through these steps.
@@ -671,7 +688,7 @@ lpr:
3. Ensure your plates are being _detected_.
If you are using a Frigate+ or `license_plate` detecting model:
- - Watch the debug view (Settings --> Debug) to ensure that `license_plate` is being detected.
+ - Watch the [Debug view](/usage/live#the-single-camera-view) to ensure that `license_plate` is being detected.
- View MQTT messages for `frigate/events` to verify detected plates.
- You may need to adjust your `min_score` and/or `threshold` for the `license_plate` object if your plates are not being detected.
@@ -680,21 +697,28 @@ lpr:
- You may need to adjust your `detection_threshold` if your plates are not being detected.
4. Ensure the characters on detected plates are being _recognized_.
+ - Check the **Plate recognition** inference time in Enrichment metrics ( ). High inference times (> 100ms) could lead to poor recognition results, especially for dedicated LPR cameras where the plate crosses the frame quickly.
- Enable `debug_save_plates` to save images of detected text on plates to the clips directory (`/media/frigate/clips/lpr`). Ensure these images are readable and the text is clear.
- Watch the debug view to see plates recognized in real-time. For non-dedicated LPR cameras, the `car` or `motorcycle` label will change to the recognized plate when LPR is enabled and working.
- Adjust `recognition_threshold` settings per the suggestions [above](#advanced-configuration).
-### Will LPR slow down my system?
+
+
+
LPR's performance impact depends on your hardware. Ensure you have at least 4GB RAM and a capable CPU or GPU for optimal results. If you are running the Dedicated LPR Camera mode, resource usage will be higher compared to users who run a model that natively detects license plates. Tune your motion detection settings for your dedicated LPR camera so that the license plate detection model runs only when necessary.
-### I am seeing a YOLOv9 plate detection metric in Enrichment Metrics, but I have a Frigate+ or custom model that detects `license_plate`. Why is the YOLOv9 model running?
+
+
+I am seeing a YOLOv9 plate detection metric in Enrichment Metrics, but I have a Frigate+ or custom model that detects license_plate. Why is the YOLOv9 model running?>}>
The YOLOv9 license plate detector model will run (and the metric will appear) if you've enabled LPR but haven't defined `license_plate` as an object to track, either at the global or camera level.
If you are detecting `car` or `motorcycle` on cameras where you don't want to run LPR, make sure you disable LPR it at the camera level. And if you do want to run LPR on those cameras, make sure you define `license_plate` as an object to track.
-### It looks like Frigate picked up my camera's timestamp or overlay text as the license plate. How can I prevent this?
+
+
+
This could happen if cars or motorcycles travel close to your camera's timestamp or overlay text. You could either move the text through your camera's firmware, or apply a mask to it in Frigate.
@@ -702,6 +726,10 @@ If you are using a model that natively detects `license_plate`, add an _object m
If you are not using a model that natively detects `license_plate` or you are using dedicated LPR camera mode, only a _motion mask_ over your text is required.
-### I see "Error running ... model" in my logs, or my inference time is very high. How can I fix this?
+
+
+
This usually happens when your GPU is unable to compile or use one of the LPR models. Set your `device` to `CPU` and try again. GPU acceleration only provides a slight performance increase, and the models are lightweight enough to run without issue on most CPUs.
+
+
diff --git a/docs/docs/configuration/live.md b/docs/docs/configuration/live.md
index 5749379c63..5df70844c9 100644
--- a/docs/docs/configuration/live.md
+++ b/docs/docs/configuration/live.md
@@ -6,12 +6,13 @@ title: Live View
import ConfigTabs from "@site/src/components/ConfigTabs";
import TabItem from "@theme/TabItem";
import NavPath from "@site/src/components/NavPath";
+import FaqItem from "@site/src/components/FaqItem";
Frigate intelligently displays your camera streams on the Live view dashboard. By default, Frigate employs "smart streaming" where camera images update once per minute when no detectable activity is occurring to conserve bandwidth and resources. As soon as any motion or active objects are detected, cameras seamlessly switch to a live stream.
### Live View technologies
-Frigate intelligently uses three different streaming technologies to display your camera streams on the dashboard and the single camera view, switching between available modes based on network bandwidth, player errors, or required features like two-way talk. The highest quality and fluency of the Live view requires the bundled `go2rtc` to be configured as shown in the [step by step guide](/guides/configuring_go2rtc).
+Frigate intelligently uses three different streaming technologies to display your camera streams on the dashboard and the single camera view, switching between available modes based on network bandwidth, player errors, or required features like two-way talk. The highest quality and fluency of the Live view requires the bundled `go2rtc` to be [configured](/configuration/go2rtc).
The jsmpeg live view will use more browser and client GPU resources. Using go2rtc is highly recommended and will provide a superior experience.
@@ -88,8 +89,18 @@ Configure a "friendly name" for your stream followed by the go2rtc stream name.
-1. Navigate to , then select your camera.
- - Under **Live stream names**, add entries mapping a friendly name to each go2rtc stream name (e.g., `Main Stream` mapped to `test_cam`, `Sub Stream` mapped to `test_cam_sub`).
+1. Navigate to and select your camera.
+2. Under **Live stream names**, click **Add stream** to add a new entry.
+3. In the **Stream name** field, enter a friendly name that will appear in the Live UI's stream dropdown (e.g., `Main Stream`).
+4. In the **go2rtc stream** field, open the dropdown and select the go2rtc stream this name should map to (e.g., `test_cam`). The dropdown lists every stream configured under `go2rtc.streams`. If the go2rtc stream hasn't been created yet, you can type the name and choose **Use "..."** to save a custom value.
+5. Repeat for each additional stream you want to expose (e.g., `Sub Stream` → `test_cam_sub`).
+6. Use the trash icon on a row to remove a stream, then **Save** the section.
+
+:::tip
+
+Configure your go2rtc streams first under so the dropdown is populated with valid options.
+
+:::
@@ -257,19 +268,47 @@ cameras:
-### Disabling cameras
+### Camera state
-Cameras can be temporarily disabled through the Frigate UI and through [MQTT](/integrations/mqtt#frigatecamera_nameenabledset) to conserve system resources. When disabled, Frigate's ffmpeg processes are terminated — recording stops, object detection is paused, and the Live dashboard displays a blank image with a disabled message. Review items, tracked objects, and historical footage for disabled cameras can still be accessed via the UI.
+Each camera has three possible states, surfaced as a status selector in **Settings → Global configuration → Camera management**:
-:::note
+- **On**: streams are processed normally. Object detection, recording, and Live view are active.
+- **Off**: Frigate's ffmpeg processes are paused. Recording stops, object detection is paused, and the Live dashboard displays a blank image with a "Camera is off" message. The camera is still visible in the Live dashboard and its past review items, tracked objects, and historical footage remain accessible via the UI. The Off state persists across Frigate restarts via a `.runtime_state.json` file alongside `config.yml` (see [Runtime toggle persistence](#runtime-toggle-persistence)).
+- **Disabled**: the change is saved to your configuration file (`enabled: False`). The camera stops immediately, Frigate stops ffmpeg processes, and all live and historical UI elements for the camera are no longer visible but remains retained on disk. The camera is still listed in **Settings → Global configuration → Camera management** so it can be re-enabled. **A restart of Frigate is required to bring a disabled camera back to On.**
-Disabling a camera via the Frigate UI or MQTT is temporary and does not persist through restarts of Frigate.
+#### Turning a camera on or off
-:::
+Turning a camera off is temporary and does not require a restart. The available controls are:
-For restreamed cameras, go2rtc remains active but does not use system resources for decoding or processing unless there are active external consumers (such as the Advanced Camera Card in Home Assistant using a go2rtc source).
+- The power button in the single-camera Live view header
+- The right-click context menu on a camera tile on the Live dashboard
+- The Camera management settings pane (status set to **Off**)
+- The mobile settings drawer on the single-camera Live view (admin users only)
+- The [MQTT topic](/integrations/mqtt#frigatecamera_nameenabledset) `frigate//enabled/set` with payload `ON` or `OFF`
+- The Home Assistant integration via the [`camera.turn_on` / `camera.turn_off` actions](/integrations/home-assistant#camera-api)
-Note that disabling a camera through the config file (`enabled: False`) removes all related UI elements, including historical footage access. To retain access while disabling the camera, keep it enabled in the config and use the UI or MQTT to disable it temporarily.
+#### Disabling a camera
+
+Disabling a camera saves the change to your configuration file. Navigate to **Settings → Global configuration → Camera management** and set the camera's status to **Disabled**. Runtime processing stops immediately; the change persists across restarts.
+
+Re-enabling a disabled camera requires a restart of Frigate so that the ffmpeg processes and other camera-scoped resources can be initialized. The UI will prompt you to restart when you switch a disabled camera back to On.
+
+#### Restream behavior
+
+For both Off and Disabled cameras, go2rtc remains active but does not use system resources for decoding or processing unless there are active external consumers (such as the Advanced Camera Card in Home Assistant using a go2rtc source).
+
+#### Choosing Off versus Disabled
+
+If you want a camera's historical data (review items, tracked objects, footage) to stay accessible in the UI while you stop processing, set the camera to **Off**. If you want the camera fully removed from the Live dashboard, review filters, and other UI surfaces, set it to **Disabled**. The Disabled state still keeps the camera in Camera management so it can be re-enabled later; if you want to remove all traces of a camera including its configuration, delete it via Camera management instead.
+
+#### Runtime toggle persistence
+
+The Live view toggles for **camera on/off**, **detect**, **recordings**, **snapshots**, and **audio detection** (along with the equivalent MQTT `/set` topics) write the new state to `.runtime_state.json` next to your `config.yml`. The file is replayed on Frigate startup so your last-known toggle states survive a restart. Two interactions worth knowing:
+
+- **Settings UI saves win.** When you save a field through **Settings → Global configuration**, the matching entry is cleared from `.runtime_state.json` so the new value in your config file is the durable source.
+- **Switching profiles clears all runtime overrides.** Activating or deactivating a [profile](/configuration/profiles) is treated as a deliberate state change, so the file is wiped to avoid stale overrides replaying on top of the new profile.
+
+If you hand-edit `config.yml` while runtime overrides exist, the overrides will still replay on restart. Delete `.runtime_state.json` to reset to the YAML-defined defaults.
### Live player error messages
@@ -295,7 +334,7 @@ When your browser runs into problems playing back your camera streams, it will l
- **stalled**
- What it means: Playback has stalled because the player has fallen too far behind live (extended buffering or no data arriving).
- - What to try: This is usually indicative of the browser struggling to decode too many high-resolution streams at once. Try selecting a lower-bandwidth stream (substream), reduce the number of live streams open, improve the network connection, or lower the camera resolution. Also check your camera's keyframe (I-frame) interval — shorter intervals make playback start and recover faster. You can also try increasing the timeout value in the UI pane of Frigate's settings.
+ - What to try: This is usually indicative of the browser struggling to decode too many high-resolution streams at once. Try selecting a lower-bandwidth stream (substream), reduce the number of live streams open, improve the network connection, or lower the camera resolution. Also check your camera's keyframe (I-frame) interval: shorter intervals make playback start and recover faster. You can also try increasing the timeout value in the UI pane of Frigate's settings.
- Possible console messages from the player code:
- `Buffer time (10 seconds) exceeded, browser may not be playing media correctly.`
@@ -303,94 +342,155 @@ When your browser runs into problems playing back your camera streams, it will l
## Live view FAQ
-1. **Why don't I have audio in my Live view?**
+### Getting Live View Working
- You must use go2rtc to hear audio in your live streams. If you have go2rtc already configured, you need to ensure your camera is sending PCMA/PCMU or AAC audio. If you can't change your camera's audio codec, you need to [transcode the audio](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#source-ffmpeg) using go2rtc.
+
- Note that the low bandwidth mode player is a video-only stream. You should not expect to hear audio when in low bandwidth mode, even if you've set up go2rtc.
+You must use go2rtc to hear audio in your live streams. If you have go2rtc already configured, you need to ensure your camera is sending PCMA/PCMU or AAC audio. If you can't change your camera's audio codec, you need to [transcode the audio](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#source-ffmpeg) using go2rtc.
-2. **Frigate shows that my live stream is in "low bandwidth mode". What does this mean?**
+If the audio controls don't appear in the UI at all, verify that the Live view is actually using your go2rtc stream. If your go2rtc stream names don't match your Frigate camera name, you must map them with the `live -> streams` config (see [Setting Streams For Live UI](#setting-streams-for-live-ui) above); otherwise the UI falls back to the video-only jsmpeg player.
- Frigate intelligently selects the live streaming technology based on a number of factors (user-selected modes like two-way talk, camera settings, browser capabilities, available bandwidth) and prioritizes showing an actual up-to-date live view of your camera's stream as quickly as possible.
+Note that the low bandwidth mode player is a video-only stream. You should not expect to hear audio when in low bandwidth mode, even if you've set up go2rtc.
- When you have go2rtc configured, Live view initially attempts to load and play back your stream with a clearer, fluent stream technology (MSE). An initial timeout, a low bandwidth condition that would cause buffering of the stream, or decoding errors in the stream will cause Frigate to switch to the stream defined by the `detect` role, using the jsmpeg format. This is what the UI labels as "low bandwidth mode". On Live dashboards, the mode will automatically reset when smart streaming is configured and activity stops. Continuous streaming mode does not have an automatic reset mechanism, but you can use the _Reset_ option to force a reload of your stream.
+
- If you are using continuous streaming or you are loading more than a few high resolution streams at once on the dashboard, your browser may struggle to begin playback of your streams before the timeout. Frigate always prioritizes showing a live stream as quickly as possible, even if it is a lower quality jsmpeg stream. You can use the "Reset" link/button to try loading your high resolution stream again.
+
- Errors in stream playback (e.g., connection failures, codec issues, or buffering timeouts) that cause the fallback to low bandwidth mode (jsmpeg) are logged to the browser console for easier debugging. These errors may include:
- - Network issues (e.g., MSE or WebRTC network connection problems).
- - Unsupported codecs or stream formats (e.g., H.265 in WebRTC, which is not supported in some browsers).
- - Buffering timeouts or low bandwidth conditions causing fallback to jsmpeg.
- - Browser compatibility problems (e.g., iOS Safari limitations with MSE).
+If your camera is streaming (as indicated by a red dot in the upper right, or if it has been set to continuous streaming mode), your browser may be blocking audio until you interact with the page. This is an intentional browser limitation. See [this article](https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide#autoplay_availability). Many browsers have a whitelist feature to change this behavior.
- To view browser console logs:
- 1. Open the Frigate Live View in your browser.
- 2. Open the browser's Developer Tools (F12 or right-click > Inspect > Console tab).
- 3. Reproduce the error (e.g., load a problematic stream or simulate network issues).
- 4. Look for messages prefixed with the camera name.
+
- These logs help identify if the issue is player-specific (MSE vs. WebRTC) or related to camera configuration (e.g., go2rtc streams, codecs). If you see frequent errors:
- - Verify your camera's H.264/AAC settings (see [Frigate's camera settings recommendations](#camera-settings-recommendations)).
- - Check go2rtc configuration for transcoding (e.g., audio to AAC/OPUS).
- - Test with a different stream via the UI dropdown (if `live -> streams` is configured).
- - For WebRTC-specific issues, ensure port 8555 is forwarded and candidates are set (see (WebRTC Extra Configuration)(#webrtc-extra-configuration)).
- - If your cameras are streaming at a high resolution, your browser may be struggling to load all of the streams before the buffering timeout occurs. Frigate prioritizes showing a true live view as quickly as possible. If the fallback occurs often, change your live view settings to use a lower bandwidth substream.
+
-3. **It doesn't seem like my cameras are streaming on the Live dashboard. Why?**
+The debug view plays the `detect` stream processed by Frigate itself, while the Live view plays your go2rtc stream directly in the browser. If the debug view works but the Live view doesn't, your browser usually can't decode what the camera is sending, most often H.265 video or an incompatible audio track.
- On the default Live dashboard ("All Cameras"), your camera images will update once per minute when no detectable activity is occurring to conserve bandwidth and resources. As soon as any activity is detected, cameras seamlessly switch to a full-resolution live stream. If you want to customize this behavior, use a camera group.
+Work through the [go2rtc troubleshooting guide](/troubleshooting/go2rtc#live-view-is-black-buffering-or-stuck-in-low-bandwidth-mode) to isolate the problem. Two fixes resolve the majority of cases:
-4. **I see a strange diagonal line on my live view, but my recordings look fine. How can I fix it?**
+1. Restream through go2rtc's FFmpeg module by prefixing your source with `ffmpeg:`, for example `- ffmpeg:rtsp://user:password@192.168.1.5:554/stream`.
+2. If that doesn't help, transcode to compatible codecs: `- ffmpeg:rtsp://user:password@192.168.1.5:554/stream#video=h264#audio=aac#hardware`.
- This is caused by incorrect dimensions set in your detect width or height (or incorrectly auto-detected), causing the jsmpeg player's rendering engine to display a slightly distorted image. You should enlarge the width and height of your `detect` resolution up to a standard aspect ratio (example: 640x352 becomes 640x360, and 800x443 becomes 800x450, 2688x1520 becomes 2688x1512, etc). If changing the resolution to match a standard (4:3, 16:9, or 32:9, etc) aspect ratio does not solve the issue, you can enable "compatibility mode" in your camera group dashboard's stream settings. Depending on your browser and device, more than a few cameras in compatibility mode may not be supported, so only use this option if changing your `detect` width and height fails to resolve the color artifacts and diagonal line.
+
-5. **How does "smart streaming" work?**
+
- Because a static image of a scene looks exactly the same as a live stream with no motion or activity, smart streaming updates your camera images once per minute when no detectable activity is occurring to conserve bandwidth and resources. As soon as any activity (motion or object/audio detection) occurs, cameras seamlessly switch to a live stream.
+For a full-resolution, low-latency live view in Home Assistant dashboards, use the [Advanced Camera Card](https://card.camera) with the [go2rtc live provider](https://card.camera/#/configuration/cameras/live-provider?id=go2rtc), which streams directly from Frigate's bundled go2rtc. This also supports audio and [two-way talk](#two-way-talk) on capable cameras. See the [Home Assistant integration docs](/integrations/home-assistant) for setup.
- This static image is pulled from the stream defined in your config with the `detect` role. When activity is detected, images from the `detect` stream immediately begin updating at ~5 frames per second so you can see the activity until the live player is loaded and begins playing. This usually only takes a second or two. If the live player times out, buffers, or has streaming errors, the jsmpeg player is loaded and plays a video-only stream from the `detect` role. When activity ends, the players are destroyed and a static image is displayed until activity is detected again, and the process repeats.
+
- Smart streaming depends on having your camera's motion `threshold` and `contour_area` config values dialed in. Use the Motion Tuner in Settings in the UI to tune these values in real-time.
+### Streaming Behavior
- This is Frigate's default and recommended setting because it results in a significant bandwidth savings, especially for high resolution cameras.
+
-6. **I have unmuted some cameras on my dashboard, but I do not hear sound. Why?**
+Because a static image of a scene looks exactly the same as a live stream with no motion or activity, smart streaming updates your camera images once per minute when no detectable activity is occurring to conserve bandwidth and resources. As soon as any activity (motion or object/audio detection) occurs, cameras seamlessly switch to a live stream.
- If your camera is streaming (as indicated by a red dot in the upper right, or if it has been set to continuous streaming mode), your browser may be blocking audio until you interact with the page. This is an intentional browser limitation. See [this article](https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide#autoplay_availability). Many browsers have a whitelist feature to change this behavior.
+This static image is pulled from the stream defined in your config with the `detect` role. When activity is detected, images from the `detect` stream immediately begin updating at ~5 frames per second so you can see the activity until the live player is loaded and begins playing. This usually only takes a second or two. If the live player times out, buffers, or has streaming errors, the jsmpeg player is loaded and plays a video-only stream from the `detect` role. When activity ends, the players are destroyed and a static image is displayed until activity is detected again, and the process repeats.
-7. **My camera streams have lots of visual artifacts / distortion.**
+Smart streaming depends on having your camera's motion `threshold` and `contour_area` config values dialed in. Use the Motion Tuner in Settings in the UI to tune these values in real-time.
- Some cameras don't include the hardware to support multiple connections to the high resolution stream, and this can cause unexpected behavior. In this case it is recommended to [restream](./restream.md) the high resolution stream so that it can be used for live view and recordings.
+This is Frigate's default and recommended setting because it results in a significant bandwidth savings, especially for high resolution cameras.
-8. **Why does my camera stream switch aspect ratios on the Live dashboard?**
+
- Your camera may change aspect ratios on the dashboard because Frigate uses different streams for different purposes. With go2rtc and Smart Streaming, Frigate shows a static image from the `detect` stream when no activity is present, and switches to the live stream when motion is detected. The camera image will change size if your streams use different aspect ratios.
+
- To prevent this, make the `detect` stream match the go2rtc live stream's aspect ratio (resolution does not need to match, just the aspect ratio). You can either adjust the camera's output resolution or set the `width` and `height` values in your config's `detect` section to a resolution with an aspect ratio that matches.
+On the default Live dashboard ("All Cameras"), your camera images will update once per minute when no detectable activity is occurring to conserve bandwidth and resources. As soon as any activity is detected, cameras seamlessly switch to a full-resolution live stream. If you want to customize this behavior, use a camera group.
- Example: Resolutions from two streams
- - Mismatched (may cause aspect ratio switching on the dashboard):
- - Live/go2rtc stream: 1920x1080 (16:9)
- - Detect stream: 640x352 (~1.82:1, not 16:9)
+
- - Matched (prevents switching):
- - Live/go2rtc stream: 1920x1080 (16:9)
- - Detect stream: 640x360 (16:9)
+
- You can update the detect settings in your camera config to match the aspect ratio of your go2rtc live stream. For example:
+Frigate intelligently selects the live streaming technology based on a number of factors (user-selected modes like two-way talk, camera settings, browser capabilities, available bandwidth) and prioritizes showing an actual up-to-date live view of your camera's stream as quickly as possible.
- ```yaml
- cameras:
- front_door:
- detect:
- width: 640
- height: 360 # set this to 360 instead of 352
- ffmpeg:
- inputs:
- - path: rtsp://127.0.0.1:8554/front_door # main stream 1920x1080
- roles:
- - record
- - path: rtsp://127.0.0.1:8554/front_door_sub # sub stream 640x352
- roles:
- - detect
- ```
+When you have go2rtc configured, Live view initially attempts to load and play back your stream with a clearer, fluent stream technology (MSE). An initial timeout, a low bandwidth condition that would cause buffering of the stream, or decoding errors in the stream will cause Frigate to switch to the stream defined by the `detect` role, using the jsmpeg format. This is what the UI labels as "low bandwidth mode". On Live dashboards, the mode will automatically reset when smart streaming is configured and activity stops. Continuous streaming mode does not have an automatic reset mechanism, but you can use the _Reset_ option to force a reload of your stream.
+
+If you are using continuous streaming or you are loading more than a few high resolution streams at once on the dashboard, your browser may struggle to begin playback of your streams before the timeout. Frigate always prioritizes showing a live stream as quickly as possible, even if it is a lower quality jsmpeg stream. You can use the "Reset" link/button to try loading your high resolution stream again.
+
+Errors in stream playback (e.g., connection failures, codec issues, or buffering timeouts) that cause the fallback to low bandwidth mode (jsmpeg) are logged to the browser console for easier debugging. These errors may include:
+
+- Network issues (e.g., MSE or WebRTC network connection problems).
+- Unsupported codecs or stream formats (e.g., H.265 in WebRTC, which is not supported in some browsers).
+- Buffering timeouts or low bandwidth conditions causing fallback to jsmpeg.
+- Browser compatibility problems (e.g., iOS Safari limitations with MSE).
+
+To view browser console logs:
+
+1. Open the Frigate Live View in your browser.
+2. Open the browser's Developer Tools (F12 or right-click > Inspect > Console tab).
+3. Reproduce the error (e.g., load a problematic stream or simulate network issues).
+4. Look for messages prefixed with the camera name.
+
+These logs help identify if the issue is player-specific (MSE vs. WebRTC) or related to camera configuration (e.g., go2rtc streams, codecs). If you see frequent errors:
+
+- Verify your camera's H.264/AAC settings (see [Frigate's camera settings recommendations](#camera-settings-recommendations)).
+- Check go2rtc configuration for transcoding (e.g., audio to AAC/OPUS).
+- Test with a different stream via the UI dropdown (if `live -> streams` is configured).
+- For WebRTC-specific issues, ensure port 8555 is forwarded and candidates are set (see [WebRTC Extra Configuration](#webrtc-extra-configuration)).
+- If your cameras are streaming at a high resolution, your browser may be struggling to load all of the streams before the buffering timeout occurs. Frigate prioritizes showing a true live view as quickly as possible. If the fallback occurs often, change your live view settings to use a lower bandwidth substream.
+
+
+
+
+
+A delay when a stream first starts is usually caused by your camera's I-frame (keyframe) interval. Playback cannot begin until a keyframe arrives, so an interval set higher than your camera's frame rate makes the stream take longer to start. Set the I-frame interval to match the frame rate (or "1x" on Reolink) per the [camera settings recommendations](#camera-settings-recommendations).
+
+A stream that starts on time but falls further behind live is buffering, which is usually the browser struggling to decode too many high-resolution streams at once. Select a lower-bandwidth substream for your dashboards (see [Setting Streams For Live UI](#setting-streams-for-live-ui)), reduce the number of streams open at once, or improve the network connection between your browser and Frigate. Frigate's player automatically speeds up playback to catch up to live after buffering, and falls back to low bandwidth mode if it stalls for too long. The _Reset_ option forces a fresh connection at the live edge.
+
+
+
+
+
+Frigate prefers MSE because it delivers a better out-of-the-box experience than WebRTC on nearly every axis that matters for a security camera system. MSE is an open standard optimized and supported by all modern browsers, works without any extra configuration (WebRTC requires port forwarding and candidate setup, and lacks H.265 support in some browsers), and requires no internet access for NAT traversal. More importantly, MSE runs over TCP, so every frame arrives and is decoded in order, so nothing is ever silently skipped. WebRTC optimizes for latency over UDP by discarding late or incomplete frames, which works against you on cellular or spotty Wi-Fi: you can end up with frozen video, visual corruption, or gaps in the feed without ever knowing you missed something. Frigate's enhanced MSE player has adaptive speed playback and has been tuned for latency and connection robustness that meets or exceeds WebRTC, so you get near-real-time playback with a guarantee that when the video plays, every frame is actually there - which, for an NVR whose whole purpose is letting you see what happened, matters more than shaving fractions of a second off a latency number. That's why Frigate defaults to MSE and reserves WebRTC for cases that require it, like two-way talk.
+
+
+
+### Video Quality Issues
+
+
+
+This is caused by incorrect dimensions set in your detect width or height (or incorrectly auto-detected), causing the jsmpeg player's rendering engine to display a slightly distorted image. You should enlarge the width and height of your `detect` resolution up to a standard aspect ratio (example: 640x352 becomes 640x360, and 800x443 becomes 800x450, 2688x1520 becomes 2688x1512, etc). If changing the resolution to match a standard (4:3, 16:9, or 32:9, etc) aspect ratio does not solve the issue, you can enable "compatibility mode" in your camera group dashboard's stream settings. Depending on your browser and device, more than a few cameras in compatibility mode may not be supported, so only use this option if changing your `detect` width and height fails to resolve the color artifacts and diagonal line.
+
+
+
+
+
+Some cameras don't include the hardware to support multiple connections to the high resolution stream, and this can cause unexpected behavior. In this case it is recommended to [restream](./restream.md) the high resolution stream so that it can be used for live view and recordings.
+
+
+
+
+
+Your camera may change aspect ratios on the dashboard because Frigate uses different streams for different purposes. With go2rtc and Smart Streaming, Frigate shows a static image from the `detect` stream when no activity is present, and switches to the live stream when motion is detected. The camera image will change size if your streams use different aspect ratios.
+
+To prevent this, make the `detect` stream match the go2rtc live stream's aspect ratio (resolution does not need to match, just the aspect ratio). You can either adjust the camera's output resolution or set the `width` and `height` values in your config's `detect` section to a resolution with an aspect ratio that matches.
+
+Example: Resolutions from two streams
+
+- Mismatched (may cause aspect ratio switching on the dashboard):
+ - Live/go2rtc stream: 1920x1080 (16:9)
+ - Detect stream: 640x352 (~1.82:1, not 16:9)
+
+- Matched (prevents switching):
+ - Live/go2rtc stream: 1920x1080 (16:9)
+ - Detect stream: 640x360 (16:9)
+
+You can update the detect settings in your camera config to match the aspect ratio of your go2rtc live stream. For example:
+
+```yaml
+cameras:
+ front_door:
+ detect:
+ width: 640
+ height: 360 # set this to 360 instead of 352
+ ffmpeg:
+ inputs:
+ - path: rtsp://127.0.0.1:8554/front_door # main stream 1920x1080
+ roles:
+ - record
+ - path: rtsp://127.0.0.1:8554/front_door_sub # sub stream 640x352
+ roles:
+ - detect
+```
+
+The same applies to your `record` stream: if its aspect ratio differs from your `detect` stream, your recordings will appear in a different shape than the live view. For consistent framing across live view and recordings, use the same aspect ratio for all of a camera's streams (the resolution can still differ).
+
+
diff --git a/docs/docs/configuration/masks.md b/docs/docs/configuration/masks.md
index e497de2c19..50c9fffc31 100644
--- a/docs/docs/configuration/masks.md
+++ b/docs/docs/configuration/masks.md
@@ -7,9 +7,11 @@ import ConfigTabs from "@site/src/components/ConfigTabs";
import TabItem from "@theme/TabItem";
import NavPath from "@site/src/components/NavPath";
+Frigate has two kinds of masks: motion masks and object filter masks. Both are narrow tools for fine-tuning, **not for hiding an area from Frigate**. Masks should be used sparingly; in most cases where users reach for one, a [zone](zones.md) with [`required_zones`](zones.md#restricting-alerts-and-detections-to-specific-zones) is the right tool instead. See [Which tool do I need?](#which-tool-do-i-need) and [Common mistakes](#common-mistakes) below if you're new to Frigate's mask behavior.
+
## Motion masks
-Motion masks are used to prevent unwanted types of motion from triggering detection. Try watching the Debug feed (Settings --> Debug) with `Motion Boxes` enabled to see what may be regularly detected as motion. For example, you want to mask out your timestamp, the sky, rooftops, etc. Keep in mind that this mask only prevents motion from being detected and does not prevent objects from being detected if object detection was started due to motion in unmasked areas. Motion is also used during object tracking to refine the object detection area in the next frame. _Over-masking will make it more difficult for objects to be tracked._
+Motion masks are used to prevent unwanted types of motion from triggering detection. Try watching the [Debug view](/usage/live#the-single-camera-view) with `Motion Boxes` enabled to see what may be regularly detected as motion. For example, you want to mask out your timestamp, the sky, rooftops, etc. Keep in mind that this mask only prevents motion from being detected and does not prevent objects from being detected if object detection was started due to motion in unmasked areas. Motion is also used during object tracking to refine the object detection area in the next frame. _Over-masking will make it more difficult for objects to be tracked._
See [further clarification](#further-clarification) below on why you may not want to use a motion mask.
@@ -21,7 +23,16 @@ Object filter masks can be used to filter out stubborn false positives in fixed

-## Creating masks
+## Which tool do I need?
+
+| What you're trying to do | Recommended tool | How it works |
+| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Only get alerts/detections for activity in the areas you care about, ignoring activity elsewhere (e.g., alert when someone enters your yard, but not when they walk past on the sidewalk) | A [zone](zones.md) combined with [`required_zones`](zones.md#restricting-alerts-and-detections-to-specific-zones) | Frigate keeps detecting and tracking activity everywhere in the frame, but a review item is only created once the bottom-center of an object's bounding box enters a required zone. |
+| Stop a stubborn false positive at a specific fixed spot (e.g., a tree base that keeps being detected as a person) | An **object filter mask** for that object type | Any detection of that object type whose bounding-box bottom-center lands inside the mask is treated as a false positive and discarded. |
+| Ignore motion in an area that obviously isn't an object of interest (e.g., the camera timestamp, sky, flags, treetops swaying) | A **motion mask** | Motion inside the mask is ignored when deciding whether to run object detection. Objects can still be detected in a motion masked area if motion elsewhere in the frame triggers detection. |
+| Stop tracking an object type altogether on this camera (e.g., you never care about cats) | Remove the object from the camera's [`objects.track`](objects.md) list | Frigate skips this object type entirely on this camera, regardless of where it appears. |
+
+## Using the mask creator
@@ -124,3 +135,14 @@ This is what `required_zones` are for. You should define a zone (remember this i
> Maybe my specific situation just warrants this. I've just been having a hard time understanding the relevance of this information - it seems to be that it's exactly what would be expected when "masking out" an area of ANY image.
That may be the case for you. Frigate will definitely work harder tracking people on the sidewalk to make sure it doesn't miss anyone who steps foot on your stoop. The trade off with the way you have it now is slower recognition of objects and potential misses. That may be acceptable based on your needs. Also, if your resolution is low enough on the detect stream, your regions may already be so big that they grab the entire object anyway.
+
+## Common mistakes
+
+**"I added a motion mask to ignore my driveway/sidewalk."**
+A motion mask doesn't hide an area from Frigate. Objects can still be detected and tracked inside a masked area. The mask only stops motion _in that area_ from triggering object detection. If you want activity on the sidewalk to never produce a review item, define a [zone](zones.md) over the area you DO care about (your stoop, your driveway) and add it to [`required_zones`](zones.md#restricting-alerts-and-detections-to-specific-zones). Frigate will still see people on the sidewalk, but it won't create an alert until they cross into the zone.
+
+**"I added an object filter mask because I don't care about cars in my yard."**
+Object filter masks are for stubborn false positives at fixed locations, not for filtering whole areas or whole object types. If you only want alerts when a car enters the driveway, use a [zone](zones.md) with [`required_zones`](zones.md#restricting-alerts-and-detections-to-specific-zones). If you don't care about a whole object type on this camera, remove it from [`objects.track`](objects.md).
+
+**"I masked everything except a thin strip on my stoop."**
+Heavy masking hurts tracking. Frigate uses motion near a tracked object's previous bounding box to decide where to look in the next frame; with most of the frame masked, an object walking from an unmasked area into a masked one effectively disappears and gets picked up as a "new" object when it reappears. For example: someone walks down your sidewalk, stops under a tree (masked area) to tie their shoe, then continues. Frigate sees that as two separate people and can create two separate review items. Because Frigate needs several consecutive frames above the confidence threshold to commit to a detection, each re-appearance can also delay or miss alerts. Use [`required_zones`](zones.md#restricting-alerts-and-detections-to-specific-zones) for "only alert me about this spot" and leave the surrounding area unmasked so tracking stays intact.
diff --git a/docs/docs/configuration/metrics.md b/docs/docs/configuration/metrics.md
index d857d5eeee..69db159cc5 100644
--- a/docs/docs/configuration/metrics.md
+++ b/docs/docs/configuration/metrics.md
@@ -59,6 +59,8 @@ Metrics are available at `/api/metrics` by default. No additional Frigate config
- `frigate_storage_used_bytes{storage=""}` - Storage used bytes
- `frigate_storage_mount_type{mount_type="", storage=""}` - Storage mount type info
+These gauges report the operating system's figures for the whole filesystem (the same numbers as `df`), not Frigate's own recording footprint. For how this differs from the recordings usage shown in the UI, see [Understanding storage usage](/configuration/record#understanding-storage-usage).
+
### Service Metrics
- `frigate_service_uptime_seconds` - Uptime in seconds
diff --git a/docs/docs/configuration/motion_detection.md b/docs/docs/configuration/motion_detection.md
index 3f31d27dba..a7928954e2 100644
--- a/docs/docs/configuration/motion_detection.md
+++ b/docs/docs/configuration/motion_detection.md
@@ -11,7 +11,7 @@ import NavPath from "@site/src/components/NavPath";
Frigate uses motion detection as a first line check to see if there is anything happening in the frame worth checking with object detection.
-Once motion is detected, it tries to group up nearby areas of motion together in hopes of identifying a rectangle in the image that will capture the area worth inspecting. These are the red "motion boxes" you see in the debug viewer.
+Once motion is detected, it tries to group up nearby areas of motion together in hopes of identifying a rectangle in the image that will capture the area worth inspecting. These are the red "motion boxes" you see in the [debug viewer](/usage/live#the-single-camera-view).
## The Goal
@@ -66,7 +66,7 @@ motion:
-Lower values mean motion detection is more sensitive to changes in color, making it more likely for example to detect motion when a brown dogs blends in with a brown fence or a person wearing a red shirt blends in with a red car. If the threshold is too low however, it may detect things like grass blowing in the wind, shadows, etc. to be detected as motion.
+Lower values mean motion detection is more sensitive to changes in color, making it more likely for example to detect motion when a brown dog blends in with a brown fence or a person wearing a red shirt blends in with a red car. If the threshold is too low however, it may detect things like grass blowing in the wind, shadows, etc. to be detected as motion.
Watching the motion boxes in the debug view, increase the threshold until you only see motion that is visible to the eye. Once this is done, it is important to test and ensure that desired motion is still detected.
@@ -151,7 +151,7 @@ motion:
Large changes in motion like PTZ moves and camera switches between Color and IR mode should result in a pause in object detection. `lightning_threshold` defines the percentage of the image used to detect these substantial changes. Increasing this value makes motion detection more likely to treat large changes (like IR mode switches) as valid motion. Decreasing it makes motion detection more likely to ignore large amounts of motion, such as a person approaching a doorbell camera.
-Note that `lightning_threshold` does **not** stop motion-based recordings from being saved — it only prevents additional motion analysis after the threshold is exceeded, reducing false positive object detections during high-motion periods (e.g. storms or PTZ sweeps) without interfering with recordings.
+Note that `lightning_threshold` does **not** stop motion-based recordings from being saved. It only prevents additional motion analysis after the threshold is exceeded, reducing false positive object detections during high-motion periods (e.g. storms or PTZ sweeps) without interfering with recordings.
:::warning
@@ -194,6 +194,10 @@ This option is handy when you want to prevent large transient changes from trigg
:::warning
-When the skip threshold is exceeded, **no motion is reported** for that frame, meaning **nothing is recorded** for that frame. That means you can miss something important, like a PTZ camera auto-tracking an object or activity while the camera is moving. If you prefer to guarantee that every frame is saved, leave this unset and accept occasional recordings containing scene noise — they typically only take up a few megabytes and are quick to scan in the timeline UI.
+When the skip threshold is exceeded, **no motion is reported** for that frame, meaning **nothing is recorded** for that frame. That means you can miss something important, like a PTZ camera auto-tracking an object or activity while the camera is moving. If you prefer to guarantee that every frame is saved, leave this unset and accept occasional recordings containing scene noise. They typically only take up a few megabytes and are quick to scan in the timeline UI.
:::
+
+## Reviewing Detected Motion
+
+To review what the detector picked up, or to search past recordings for motion in a specific region, see [Reviewing Motion](/usage/review#reviewing-motion) on the Review page.
diff --git a/docs/docs/configuration/object_detectors.md b/docs/docs/configuration/object_detectors.md
index 2821fb7a27..23595ac6e8 100644
--- a/docs/docs/configuration/object_detectors.md
+++ b/docs/docs/configuration/object_detectors.md
@@ -4,14 +4,19 @@ title: Object Detectors
---
import CommunityBadge from '@site/src/components/CommunityBadge';
-import ConfigTabs from "@site/src/components/ConfigTabs";
-import TabItem from "@theme/TabItem";
-import NavPath from "@site/src/components/NavPath";
+import ConfigTabs from '@site/src/components/ConfigTabs';
+import TabItem from '@theme/TabItem';
+import NavPath from '@site/src/components/NavPath';
+import ModelConfigDropdown from '@site/src/components/ModelConfigDropdown';
+import objectDetectorsModels from '@site/data/object_detectors_models.yaml';
-# Supported Hardware
+### Supported hardware
+
+Object detection is what allows Frigate to identify _what_ is in your camera's view (people, cars, animals, and more) rather than just reacting to pixel changes. When Frigate's motion detection finds activity in a frame, that region is sent to an **object detector**, which returns the objects it recognizes along with their location and a confidence score. These detections are what drive tracked objects, alerts, detections, and notifications.
+
+Object detection is computationally intensive, so Frigate is designed to run it on a dedicated AI accelerator or GPU rather than the CPU. A **detector** is the specific hardware-and-model backend Frigate uses to run inference. Choosing a detector that matches your hardware is one of the most important steps in getting good performance, and the right choice depends on what device Frigate is running on.
:::info
-
Frigate supports multiple different detectors that work on different types of hardware:
**Most Hardware**
@@ -70,6 +75,22 @@ This does not affect using hardware for accelerating other tasks such as [semant
:::
+### Choosing a model size
+
+Along with picking a detector for your hardware, you will choose a model's **input resolution** (such as `320x320` or `640x640`) and, for model families like YOLOv9, a **variant size** (`tiny`, `small`, etc.). Both affect the balance between accuracy and the inference time your hardware can sustain.
+
+**Resolution (320x320 vs 640x640):** Frigate is optimized for `320x320` models, and `320x320` is the best choice for the vast majority of setups. Frigate is specifically designed to compensate for the smaller model by cropping a region of motion from the full frame and zooming into it before running detection, so a `320x320` model is actually _better_ at small and distant objects, not worse. A `640x640` model is slower and uses more resources, and its main benefit is fitting more objects into a single inference when many objects are spread across a large area. Recent versions of Frigate have improved support for `640x640` models, but `320x320` remains the recommended starting point for nearly all setups.
+
+**Variant size (tiny/small/medium):** Larger variants are gradually more accurate but slower. Whether the difference is noticeable depends on your specific cameras and scenes. A good rule of thumb is to use the largest model your hardware can run without skipping detections, which you can monitor on the page in the UI. Better accuracy only helps if your detector keeps up with the detection load across all cameras.
+
+**Acceptable inference time depends on your hardware.** Inference time alone does not tell the whole story, because different hardware has different capacity. A GPU can run multiple instances of the same model concurrently, so an inference time around 30ms can still keep up with several cameras. A Google Coral runs only a single instance of the model, so it needs a much lower inference time (around 10ms) to keep up.
+
+:::tip
+
+The best detection accuracy comes from a model trained on images that look like what Frigate actually sees: security camera footage cropped to regions of interest. You can train or fine-tune your own model on images like this and run it as a custom model (see the per-detector sections below), but [Frigate+](/plus) makes this much easier by handling the training for you on images submitted from your own cameras. For YOLOv9, the `s` (small) variant at `320x320` resolution is a good place to start.
+
+:::
+
# Officially Supported Detectors
Frigate provides a number of builtin detector types. By default, Frigate will use a single CPU detector. Other detectors may require additional configuration as described below. When using multiple detectors they will run in dedicated processes, but pull from a common queue of detection requests from across all cameras.
@@ -91,7 +112,7 @@ See [common Edge TPU troubleshooting steps](/troubleshooting/edgetpu) if the Edg
-Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `usb`.
+Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `usb`.
@@ -111,7 +132,7 @@ detectors:
-Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add** to add multiple detectors, specifying `usb:0` and `usb:1` as the device for each.
+Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add** to add multiple detectors, specifying `usb:0` and `usb:1` as the device for each.
@@ -136,7 +157,7 @@ _warning: may have [compatibility issues](https://github.com/blakeblackshear/fri
-Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add**, then leave the device field empty.
+Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add**, then leave the device field empty.
@@ -156,7 +177,7 @@ detectors:
-Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `pci`.
+Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `pci`.
@@ -176,7 +197,7 @@ detectors:
-Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add** to add multiple detectors, specifying `pci:0` and `pci:1` as the device for each.
+Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add** to add multiple detectors, specifying `pci:0` and `pci:1` as the device for each.
@@ -199,7 +220,7 @@ detectors:
-Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add** to add multiple detectors with different device types (e.g., `usb` and `pci`).
+Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add** to add multiple detectors with different device types (e.g., `usb` and `pci`).
@@ -217,68 +238,9 @@ detectors:
-### EdgeTPU Supported Models
+### Configuration {#configuration-edgetpu}
-| Model | Notes |
-| ----------------------- | ------------------------------------------- |
-| [Mobiledet](#mobiledet) | Default model |
-| [YOLOv9](#yolov9) | More accurate but slower than default model |
-
-#### Mobiledet
-
-A TensorFlow Lite model is provided in the container at `/edgetpu_model.tflite` and is used by this detector type by default. To provide your own model, bind mount the file into the container and provide the path with `model.path`.
-
-#### YOLOv9
-
-YOLOv9 models that are compiled for TensorFlow Lite and properly quantized are supported, but not included by default. [Instructions](#yolov9-for-google-coral-support) for downloading a model with support for the Google Coral.
-
-:::tip
-
-**Frigate+ Users:** Follow the [instructions](/integrations/plus#use-models) to set a model ID in your config file.
-
-:::
-
-
- YOLOv9 Setup & Config
-
-After placing the downloaded files for the tflite model and labels in your config folder, use the following configuration:
-
-
-
-
-Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `usb`. Then navigate to and configure the model settings:
-
-| Field | Value |
-| ---------------------------------------- | ----------------------------------------------------------------- |
-| **Object Detection Model Type** | `yolo-generic` |
-| **Object detection model input width** | `320` (should match the imgsize of the model) |
-| **Object detection model input height** | `320` (should match the imgsize of the model) |
-| **Custom object detector model path** | `/config/model_cache/yolov9-s-relu6-best_320_int8_edgetpu.tflite` |
-| **Label map for custom object detector** | `/config/labels-coco17.txt` |
-
-
-
-
-```yaml
-detectors:
- coral:
- type: edgetpu
- device: usb
-
-model:
- model_type: yolo-generic
- width: 320 # <--- should match the imgsize of the model, typically 320
- height: 320 # <--- should match the imgsize of the model, typically 320
- path: /config/model_cache/yolov9-s-relu6-best_320_int8_edgetpu.tflite
- labelmap_path: /config/labels-coco17.txt
-```
-
-
-
-
-Note that due to hardware limitations of the Coral, the labelmap is a subset of the COCO labels and includes only 17 object classes.
-
-
+
---
@@ -294,150 +256,12 @@ If no custom model is provided, the Hailo detector downloads a default model fro
:::
-### Configuration
+### Configuration {#configuration-hailo}
When configuring the Hailo detector, you have two options to specify the model: a local **path** or a **URL**.
If both are provided, the detector will first check for the model at the given local path. If the file is not found, it will download the model from the specified URL. The model file is cached under `/config/model_cache/hailo`.
-#### YOLO
-
-Use this configuration for YOLO-based models. When no custom model path or URL is provided, the detector automatically downloads the default model based on the detected hardware:
-
-- **Hailo-8 hardware:** Uses **YOLOv6n** (default: `yolov6n.hef`)
-- **Hailo-8L hardware:** Uses **YOLOv6n** (default: `yolov6n.hef`)
-
-
-
-
-Navigate to and select **Hailo-8/Hailo-8L** from the detector type dropdown and click **Add**, then set device to `PCIe`. Then navigate to and configure the model settings:
-
-| Field | Value |
-| ---------------------------------------- | ----------------------- |
-| **Object detection model input width** | `320` |
-| **Object detection model input height** | `320` |
-| **Model Input Tensor Shape** | `nhwc` |
-| **Model Input Pixel Color Format** | `rgb` |
-| **Model Input D Type** | `int` |
-| **Object Detection Model Type** | `yolo-generic` |
-| **Label map for custom object detector** | `/labelmap/coco-80.txt` |
-
-The detector automatically selects the default model based on your hardware. Optionally, specify a local model path or URL to override.
-
-
-
-
-```yaml
-detectors:
- hailo:
- type: hailo8l
- device: PCIe
-
-model:
- width: 320
- height: 320
- input_tensor: nhwc
- input_pixel_format: rgb
- input_dtype: int
- model_type: yolo-generic
- labelmap_path: /labelmap/coco-80.txt
-
- # The detector automatically selects the default model based on your hardware:
- # - For Hailo-8 hardware: YOLOv6n (default: yolov6n.hef)
- # - For Hailo-8L hardware: YOLOv6n (default: yolov6n.hef)
- #
- # Optionally, you can specify a local model path to override the default.
- # If a local path is provided and the file exists, it will be used instead of downloading.
- # Example:
- # path: /config/model_cache/hailo/yolov6n.hef
- #
- # You can also override using a custom URL:
- # path: https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8/yolov6n.hef
- # just make sure to give it the write configuration based on the model
-```
-
-
-
-
-#### SSD
-
-For SSD-based models, provide either a model path or URL to your compiled SSD model. The integration will first check the local path before downloading if necessary.
-
-
-
-
-Navigate to and select **Hailo-8/Hailo-8L** from the detector type dropdown and click **Add**, then set device to `PCIe`. Then navigate to and configure the model settings:
-
-| Field | Value |
-| --------------------------------------- | ------ |
-| **Object detection model input width** | `300` |
-| **Object detection model input height** | `300` |
-| **Model Input Tensor Shape** | `nhwc` |
-| **Model Input Pixel Color Format** | `rgb` |
-| **Object Detection Model Type** | `ssd` |
-
-Specify the local model path or URL for SSD MobileNet v1.
-
-
-
-
-```yaml
-detectors:
- hailo:
- type: hailo8l
- device: PCIe
-
-model:
- width: 300
- height: 300
- input_tensor: nhwc
- input_pixel_format: rgb
- model_type: ssd
- # Specify the local model path (if available) or URL for SSD MobileNet v1.
- # Example with a local path:
- # path: /config/model_cache/h8l_cache/ssd_mobilenet_v1.hef
- #
- # Or override using a custom URL:
- # path: https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8l/ssd_mobilenet_v1.hef
-```
-
-
-
-
-#### Custom Models
-
-The Hailo detector supports all YOLO models compiled for Hailo hardware that include post-processing. You can specify a custom URL or a local path to download or use your model directly. If both are provided, the detector checks the local path first.
-
-
-
-
-Navigate to and select **Hailo-8/Hailo-8L** from the detector type dropdown and click **Add**, then set device to `PCIe`. Then navigate to and configure the model settings to match your custom model dimensions and format.
-
-
-
-
-```yaml
-detectors:
- hailo:
- type: hailo8l
- device: PCIe
-
-model:
- width: 640
- height: 640
- input_tensor: nhwc
- input_pixel_format: rgb
- input_dtype: int
- model_type: yolo-generic
- labelmap_path: /labelmap/coco-80.txt
- # Optional: Specify a local model path.
- # path: /config/model_cache/hailo/custom_model.hef
- #
- # Alternatively, or as a fallback, provide a custom URL:
- # path: https://custom-model-url.com/path/to/model.hef
-```
-
-
-
+
For additional ready-to-use models, please visit: https://github.com/hailo-ai/hailo_model_zoo
@@ -462,14 +286,6 @@ OpenVINO is supported on 6th Gen Intel platforms (Skylake) and newer. It will al
When using many cameras one detector may not be enough to keep up. Multiple detectors can be defined assuming GPU resources are available. An example configuration would be:
-
-
-
-Navigate to and select **OpenVINO** from the detector type dropdown and click **Add** to add multiple detectors, each targeting `GPU` or `NPU`.
-
-
-
-
```yaml
detectors:
ov_0:
@@ -480,381 +296,34 @@ detectors:
device: GPU # or NPU
```
-
-
-
:::
-### OpenVINO Supported Models
+### Configuration {#configuration-openvino}
-| Model | GPU | NPU | Notes |
-| ------------------------------------- | --- | --- | ------------------------------------------------------------ |
-| [YOLOv9](#yolo-v3-v4-v7-v9) | ✅ | ✅ | Recommended for GPU & NPU |
-| [RF-DETR](#rf-detr) | ✅ | ✅ | Requires XE iGPU or Arc |
-| [YOLO-NAS](#yolo-nas) | ✅ | ✅ | |
-| [MobileNet v2](#ssdlite-mobilenet-v2) | ✅ | ✅ | Fast and lightweight model, less accurate than larger models |
-| [YOLOX](#yolox) | ✅ | ? | |
-| [D-FINE](#d-fine) | ❌ | ❌ | |
+
-#### SSDLite MobileNet v2
-
-An OpenVINO model is provided in the container at `/openvino-model/ssdlite_mobilenet_v2.xml` and is used by this detector type by default. The model comes from Intel's Open Model Zoo [SSDLite MobileNet V2](https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/ssdlite_mobilenet_v2) and is converted to an FP16 precision IR model.
-
-
- MobileNet v2 Config
-
-Use the model configuration shown below when using the OpenVINO detector with the default OpenVINO model:
-
-
-
-
-Navigate to and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU` (or `NPU`). Then navigate to and configure:
-
-| Field | Value |
-| ---------------------------------------- | ------------------------------------------ |
-| **Object detection model input width** | `300` |
-| **Object detection model input height** | `300` |
-| **Model Input Tensor Shape** | `nhwc` |
-| **Model Input Pixel Color Format** | `bgr` |
-| **Custom object detector model path** | `/openvino-model/ssdlite_mobilenet_v2.xml` |
-| **Label map for custom object detector** | `/openvino-model/coco_91cl_bkgr.txt` |
-
-
-
-
-```yaml
-detectors:
- ov:
- type: openvino
- device: GPU # Or NPU
-
-model:
- width: 300
- height: 300
- input_tensor: nhwc
- input_pixel_format: bgr
- path: /openvino-model/ssdlite_mobilenet_v2.xml
- labelmap_path: /openvino-model/coco_91cl_bkgr.txt
-```
-
-
-
-
-
-
-#### YOLOX
-
-This detector also supports YOLOX. Frigate does not come with any YOLOX models preloaded, so you will need to supply your own models.
-
-#### YOLO-NAS
-
-[YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) models are supported, but not included by default. See [the models section](#downloading-yolo-nas-model) for more information on downloading the YOLO-NAS model for use in Frigate.
-
-
- YOLO-NAS Setup & Config
-
-After placing the downloaded onnx model in your config folder, use the following configuration:
-
-
-
-
-Navigate to and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU`. Then navigate to and configure:
-
-| Field | Value |
-| ---------------------------------------- | ------------------------------------------------- |
-| **Object Detection Model Type** | `yolonas` |
-| **Object detection model input width** | `320` (should match whatever was set in notebook) |
-| **Object detection model input height** | `320` (should match whatever was set in notebook) |
-| **Model Input Tensor Shape** | `nchw` |
-| **Model Input Pixel Color Format** | `bgr` |
-| **Custom object detector model path** | `/config/yolo_nas_s.onnx` |
-| **Label map for custom object detector** | `/labelmap/coco-80.txt` |
-
-
-
-
-```yaml
-detectors:
- ov:
- type: openvino
- device: GPU
-
-model:
- model_type: yolonas
- width: 320 # <--- should match whatever was set in notebook
- height: 320 # <--- should match whatever was set in notebook
- input_tensor: nchw
- input_pixel_format: bgr
- path: /config/yolo_nas_s.onnx
- labelmap_path: /labelmap/coco-80.txt
-```
-
-
-
-
-Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects.
-
-
-
-#### YOLO (v3, v4, v7, v9)
-
-YOLOv3, YOLOv4, YOLOv7, and [YOLOv9](https://github.com/WongKinYiu/yolov9) models are supported, but not included by default.
-
-:::tip
-
-The YOLO detector has been designed to support YOLOv3, YOLOv4, YOLOv7, and YOLOv9 models, but may support other YOLO model architectures as well.
-
-:::
-
-
- YOLOv Setup & Config
-
-:::warning
-
-If you are using a Frigate+ model, you should not define any of the below `model` parameters in your config except for `path`. See [the Frigate+ model docs](/plus/first_model#step-3-set-your-model-id-in-the-config) for more information on setting up your model.
-
-:::
-
-After placing the downloaded onnx model in your config folder, use the following configuration:
-
-
-
-
-Navigate to and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU` (or `NPU`). Then navigate to and configure:
-
-| Field | Value |
-| ---------------------------------------- | -------------------------------------------------------- |
-| **Object Detection Model Type** | `yolo-generic` |
-| **Object detection model input width** | `320` (should match the imgsize set during model export) |
-| **Object detection model input height** | `320` (should match the imgsize set during model export) |
-| **Model Input Tensor Shape** | `nchw` |
-| **Model Input D Type** | `float` |
-| **Custom object detector model path** | `/config/model_cache/yolo.onnx` |
-| **Label map for custom object detector** | `/labelmap/coco-80.txt` |
-
-
-
-
-```yaml
-detectors:
- ov:
- type: openvino
- device: GPU # or NPU
-
-model:
- model_type: yolo-generic
- width: 320 # <--- should match the imgsize set during model export
- height: 320 # <--- should match the imgsize set during model export
- input_tensor: nchw
- input_dtype: float
- path: /config/model_cache/yolo.onnx
- labelmap_path: /labelmap/coco-80.txt
-```
-
-
-
-
-Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects.
-
-
-
-#### RF-DETR
-
-[RF-DETR](https://github.com/roboflow/rf-detr) is a DETR based model. The ONNX exported models are supported, but not included by default. See [the models section](#downloading-rf-detr-model) for more informatoin on downloading the RF-DETR model for use in Frigate.
-
-:::warning
-
-Due to the size and complexity of the RF-DETR model, it is only recommended to be run with discrete Arc Graphics Cards.
-
-:::
-
-
- RF-DETR Setup & Config
-
-After placing the downloaded onnx model in your `config/model_cache` folder, use the following configuration:
-
-
-
-
-Navigate to and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU`. Then navigate to and configure:
-
-| Field | Value |
-| --------------------------------------- | --------------------------------- |
-| **Object Detection Model Type** | `rfdetr` |
-| **Object detection model input width** | `320` |
-| **Object detection model input height** | `320` |
-| **Model Input Tensor Shape** | `nchw` |
-| **Model Input D Type** | `float` |
-| **Custom object detector model path** | `/config/model_cache/rfdetr.onnx` |
-
-
-
-
-```yaml
-detectors:
- ov:
- type: openvino
- device: GPU
-
-model:
- model_type: rfdetr
- width: 320
- height: 320
- input_tensor: nchw
- input_dtype: float
- path: /config/model_cache/rfdetr.onnx
-```
-
-
-
-
-
-
-#### D-FINE
-
-[D-FINE](https://github.com/Peterande/D-FINE) is a DETR based model. The ONNX exported models are supported, but not included by default. See [the models section](#downloading-d-fine-model) for more information on downloading the D-FINE model for use in Frigate.
-
-:::warning
-
-Currently D-FINE models only run on OpenVINO in CPU mode, GPUs currently fail to compile the model
-
-:::
-
-
- D-FINE Setup & Config
-
-After placing the downloaded onnx model in your config/model_cache folder, use the following configuration:
-
-
-
-
-Navigate to and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `CPU`. Then navigate to and configure:
-
-| Field | Value |
-| ---------------------------------------- | ---------------------------------- |
-| **Object Detection Model Type** | `dfine` |
-| **Object detection model input width** | `640` |
-| **Object detection model input height** | `640` |
-| **Model Input Tensor Shape** | `nchw` |
-| **Model Input D Type** | `float` |
-| **Custom object detector model path** | `/config/model_cache/dfine-s.onnx` |
-| **Label map for custom object detector** | `/labelmap/coco-80.txt` |
-
-
-
-
-```yaml
-detectors:
- ov:
- type: openvino
- device: CPU
-
-model:
- model_type: dfine
- width: 640
- height: 640
- input_tensor: nchw
- input_dtype: float
- path: /config/model_cache/dfine-s.onnx
- labelmap_path: /labelmap/coco-80.txt
-```
-
-
-
-
-Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects.
-
-
+---
## Apple Silicon detector
The NPU in Apple Silicon can't be accessed from within a container, so the [Apple Silicon detector client](https://github.com/frigate-nvr/apple-silicon-detector) must first be setup. It is recommended to use the Frigate docker image with `-standard-arm64` suffix, for example `ghcr.io/blakeblackshear/frigate:stable-standard-arm64`.
-### Setup
+### Setup {#setup-apple-silicon}
1. Setup the [Apple Silicon detector client](https://github.com/frigate-nvr/apple-silicon-detector) and run the client
2. Configure the detector in Frigate and startup Frigate
-### Configuration
+### Configuration {#configuration-apple-silicon}
Using the detector config below will connect to the client:
-
-
-
-Navigate to and select **ZMQ IPC** from the detector type dropdown and click **Add**, then set the endpoint to `tcp://host.docker.internal:5555`.
-
-
-
-
-```yaml
-detectors:
- apple-silicon:
- type: zmq
- endpoint: tcp://host.docker.internal:5555
-```
-
-
-
-
-### Apple Silicon Supported Models
-
-There is no default model provided, the following formats are supported:
-
-#### YOLO (v3, v4, v7, v9)
-
-YOLOv3, YOLOv4, YOLOv7, and [YOLOv9](https://github.com/WongKinYiu/yolov9) models are supported, but not included by default.
-
-:::tip
-
-The YOLO detector has been designed to support YOLOv3, YOLOv4, YOLOv7, and YOLOv9 models, but may support other YOLO model architectures as well. See [the models section](#downloading-yolo-models) for more information on downloading YOLO models for use in Frigate.
-
-:::
-
-When Frigate is started with the following config it will connect to the detector client and transfer the model automatically:
-
-
-
-
-Navigate to and select **ZMQ IPC** from the detector type dropdown and click **Add**, then set the endpoint to `tcp://host.docker.internal:5555`. Then navigate to and configure:
-
-| Field | Value |
-| ---------------------------------------- | -------------------------------------------------------- |
-| **Object Detection Model Type** | `yolo-generic` |
-| **Object detection model input width** | `320` (should match the imgsize set during model export) |
-| **Object detection model input height** | `320` (should match the imgsize set during model export) |
-| **Model Input Tensor Shape** | `nchw` |
-| **Model Input D Type** | `float` |
-| **Custom object detector model path** | `/config/model_cache/yolo.onnx` |
-| **Label map for custom object detector** | `/labelmap/coco-80.txt` |
-
-
-
-
-```yaml
-detectors:
- apple-silicon:
- type: zmq
- endpoint: tcp://host.docker.internal:5555
-
-model:
- model_type: yolo-generic
- width: 320 # <--- should match the imgsize set during model export
- height: 320 # <--- should match the imgsize set during model export
- input_tensor: nchw
- input_dtype: float
- path: /config/model_cache/yolo.onnx
- labelmap_path: /labelmap/coco-80.txt
-```
-
-
-
+
Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects.
## AMD/ROCm GPU detector
-### Setup
+### Setup {#setup-rocm}
Support for AMD GPUs is provided using the [ONNX detector](#onnx). In order to utilize the AMD GPU for object detection use a frigate docker image with `-rocm` suffix, for example `ghcr.io/blakeblackshear/frigate:stable-rocm`.
@@ -932,7 +401,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)'
```
-### ROCm Supported Models
+### Configuration {#configuration-rocm}
:::tip
@@ -945,11 +414,13 @@ The AMD GPU kernel is known problematic especially when converting models to mxr
:::
-See [ONNX supported models](#supported-models) for supported models, there are some caveats:
+See [ONNX supported models](#onnx) for supported models, there are some caveats:
-- D-FINE models are not supported
+- D-FINE / DEIMv2 models are not supported
- YOLO-NAS models are known to not run well on integrated GPUs
+
+
## ONNX
ONNX is an open format for building machine learning models, Frigate supports running ONNX models on CPU, OpenVINO, ROCm, and TensorRT. On startup Frigate will automatically try to use a GPU if one is available.
@@ -974,14 +445,6 @@ If the correct build is used for your GPU then the GPU will be detected and used
When using many cameras one detector may not be enough to keep up. Multiple detectors can be defined assuming GPU resources are available. An example configuration would be:
-
-
-
-Navigate to and select **ONNX** from the detector type dropdown and click **Add** to add multiple detectors.
-
-
-
-
```yaml
detectors:
onnx_0:
@@ -990,279 +453,13 @@ detectors:
type: onnx
```
-
-
-
:::
-### ONNX Supported Models
+### Configuration {#configuration-onnx}
-| Model | Nvidia GPU | AMD GPU | Notes |
-| ----------------------------- | ---------- | ------- | --------------------------------------------------- |
-| [YOLOv9](#yolo-v3-v4-v7-v9-2) | ✅ | ✅ | Supports CUDA Graphs for optimal Nvidia performance |
-| [RF-DETR](#rf-detr) | ✅ | ❌ | Supports CUDA Graphs for optimal Nvidia performance |
-| [YOLO-NAS](#yolo-nas-1) | ⚠️ | ⚠️ | Not supported by CUDA Graphs |
-| [YOLOX](#yolox-1) | ✅ | ✅ | Supports CUDA Graphs for optimal Nvidia performance |
-| [D-FINE](#d-fine) | ⚠️ | ❌ | Not supported by CUDA Graphs |
+
-There is no default model provided, the following formats are supported:
-
-#### YOLO-NAS
-
-[YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) models are supported, but not included by default. See [the models section](#downloading-yolo-nas-model) for more information on downloading the YOLO-NAS model for use in Frigate.
-
-
- YOLO-NAS Setup & Config
-
-:::warning
-
-If you are using a Frigate+ YOLO-NAS model, you should not define any of the below `model` parameters in your config except for `path`. See [the Frigate+ model docs](/plus/first_model#step-3-set-your-model-id-in-the-config) for more information on setting up your model.
-
-:::
-
-After placing the downloaded onnx model in your config folder, use the following configuration:
-
-
-
-
-Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then navigate to and configure:
-
-| Field | Value |
-| ---------------------------------------- | ------------------------------------------------- |
-| **Object Detection Model Type** | `yolonas` |
-| **Object detection model input width** | `320` (should match whatever was set in notebook) |
-| **Object detection model input height** | `320` (should match whatever was set in notebook) |
-| **Model Input Pixel Color Format** | `bgr` |
-| **Model Input Tensor Shape** | `nchw` |
-| **Custom object detector model path** | `/config/yolo_nas_s.onnx` |
-| **Label map for custom object detector** | `/labelmap/coco-80.txt` |
-
-
-
-
-```yaml
-detectors:
- onnx:
- type: onnx
-
-model:
- model_type: yolonas
- width: 320 # <--- should match whatever was set in notebook
- height: 320 # <--- should match whatever was set in notebook
- input_pixel_format: bgr
- input_tensor: nchw
- path: /config/yolo_nas_s.onnx
- labelmap_path: /labelmap/coco-80.txt
-```
-
-
-
-
-
-
-#### 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.
-
-:::
-
-
- YOLOv Setup & Config
-
-:::warning
-
-If you are using a Frigate+ model, you should not define any of the below `model` parameters in your config except for `path`. See [the Frigate+ model docs](/plus/first_model#step-3-set-your-model-id-in-the-config) for more information on setting up your model.
-
-:::
-
-After placing the downloaded onnx model in your config folder, use the following configuration:
-
-
-
-
-Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then navigate to and configure:
-
-| Field | Value |
-| ---------------------------------------- | -------------------------------------------------------- |
-| **Object Detection Model Type** | `yolo-generic` |
-| **Object detection model input width** | `320` (should match the imgsize set during model export) |
-| **Object detection model input height** | `320` (should match the imgsize set during model export) |
-| **Model Input Tensor Shape** | `nchw` |
-| **Model Input D Type** | `float` |
-| **Custom object detector model path** | `/config/model_cache/yolo.onnx` |
-| **Label map for custom object detector** | `/labelmap/coco-80.txt` |
-
-
-
-
-```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.
-
-#### YOLOx
-
-[YOLOx](https://github.com/Megvii-BaseDetection/YOLOX) models are supported, but not included by default. See [the models section](#downloading-yolo-models) for more information on downloading the YOLOx model for use in Frigate.
-
-
- YOLOx Setup & Config
-
-After placing the downloaded onnx model in your config folder, use the following configuration:
-
-
-
-
-Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then navigate to and configure:
-
-| Field | Value |
-| ---------------------------------------- | -------------------------------------------------------- |
-| **Object Detection Model Type** | `yolox` |
-| **Object detection model input width** | `416` (should match the imgsize set during model export) |
-| **Object detection model input height** | `416` (should match the imgsize set during model export) |
-| **Model Input Tensor Shape** | `nchw` |
-| **Model Input D Type** | `float_denorm` |
-| **Custom object detector model path** | `/config/model_cache/yolox_tiny.onnx` |
-| **Label map for custom object detector** | `/labelmap/coco-80.txt` |
-
-
-
-
-```yaml
-detectors:
- onnx:
- type: onnx
-
-model:
- model_type: yolox
- width: 416 # <--- should match the imgsize set during model export
- height: 416 # <--- should match the imgsize set during model export
- input_tensor: nchw
- input_dtype: float_denorm
- path: /config/model_cache/yolox_tiny.onnx
- labelmap_path: /labelmap/coco-80.txt
-```
-
-
-
-
-Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects.
-
-
-
-#### RF-DETR
-
-[RF-DETR](https://github.com/roboflow/rf-detr) is a DETR based model. The ONNX exported models are supported, but not included by default. See [the models section](#downloading-rf-detr-model) for more information on downloading the RF-DETR model for use in Frigate.
-
-
- RF-DETR Setup & Config
-
-After placing the downloaded onnx model in your `config/model_cache` folder, use the following configuration:
-
-
-
-
-Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then navigate to and configure:
-
-| Field | Value |
-| --------------------------------------- | --------------------------------- |
-| **Object Detection Model Type** | `rfdetr` |
-| **Object detection model input width** | `320` |
-| **Object detection model input height** | `320` |
-| **Model Input Tensor Shape** | `nchw` |
-| **Model Input D Type** | `float` |
-| **Custom object detector model path** | `/config/model_cache/rfdetr.onnx` |
-
-
-
-
-```yaml
-detectors:
- onnx:
- type: onnx
-
-model:
- model_type: rfdetr
- width: 320
- height: 320
- input_tensor: nchw
- input_dtype: float
- path: /config/model_cache/rfdetr.onnx
-```
-
-
-
-
-
-
-#### D-FINE
-
-[D-FINE](https://github.com/Peterande/D-FINE) is a DETR based model. The ONNX exported models are supported, but not included by default. See [the models section](#downloading-d-fine-model) for more information on downloading the D-FINE model for use in Frigate.
-
-
- D-FINE Setup & Config
-
-After placing the downloaded onnx model in your `config/model_cache` folder, use the following configuration:
-
-
-
-
-Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then navigate to and configure:
-
-| Field | Value |
-| ---------------------------------------- | ------------------------------------------- |
-| **Object Detection Model Type** | `dfine` |
-| **Object detection model input width** | `640` |
-| **Object detection model input height** | `640` |
-| **Model Input Tensor Shape** | `nchw` |
-| **Model Input D Type** | `float` |
-| **Custom object detector model path** | `/config/model_cache/dfine_m_obj2coco.onnx` |
-| **Label map for custom object detector** | `/labelmap/coco-80.txt` |
-
-
-
-
-```yaml
-detectors:
- onnx:
- type: onnx
-
-model:
- model_type: dfine
- width: 640
- height: 640
- input_tensor: nchw
- input_dtype: float
- path: /config/model_cache/dfine_m_obj2coco.onnx
- labelmap_path: /labelmap/coco-80.txt
-```
-
-
-
-
-
-
-Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects.
+---
## CPU Detector (not recommended)
@@ -1278,29 +475,9 @@ The number of threads used by the interpreter can be specified using the `"num_t
A TensorFlow Lite model is provided in the container at `/cpu_model.tflite` and is used by this detector type by default. To provide your own model, bind mount the file into the container and provide the path with `model.path`.
-
-
+### Configuration {#configuration-cpu}
-Navigate to and select **CPU** from the detector type dropdown and click **Add**. Configure the number of threads and click **Add** again to add additional CPU detectors as needed (one per camera is recommended).
-
-
-
-
-```yaml
-detectors:
- cpu1:
- type: cpu
- num_threads: 3
- cpu2:
- type: cpu
- num_threads: 3
-
-model:
- path: "/custom_model.tflite"
-```
-
-
-
+
When using CPU detectors, you can add one CPU detector per camera. Adding more detectors than the number of cameras should not improve performance.
@@ -1308,30 +485,15 @@ When using CPU detectors, you can add one CPU detector per camera. Adding more d
The Deepstack / CodeProject.AI Server detector for Frigate allows you to integrate Deepstack and CodeProject.AI object detection capabilities into Frigate. CodeProject.AI and DeepStack are open-source AI platforms that can be run on various devices such as the Raspberry Pi, Nvidia Jetson, and other compatible hardware. It is important to note that the integration is performed over the network, so the inference times may not be as fast as native Frigate detectors, but it still provides an efficient and reliable solution for object detection and tracking.
-### Setup
+### Setup {#setup-deepstack}
To get started with CodeProject.AI, visit their [official website](https://www.codeproject.com/Articles/5322557/CodeProject-AI-Server-AI-the-easy-way) to follow the instructions to download and install the AI server on your preferred device. Detailed setup instructions for CodeProject.AI are outside the scope of the Frigate documentation.
To integrate CodeProject.AI into Frigate, configure the detector as follows:
-
-
+### Configuration {#configuration-deepstack}
-Navigate to and select **DeepStack** from the detector type dropdown and click **Add**. Set the API URL to point to your CodeProject.AI server (e.g., `http://:/v1/vision/detection`).
-
-
-
-
-```yaml
-detectors:
- deepstack:
- api_url: http://:/v1/vision/detection
- type: deepstack
- api_timeout: 0.1 # seconds
-```
-
-
-
+
Replace `` and `` with the IP address and port of your CodeProject.AI server.
@@ -1347,273 +509,45 @@ See the [installation docs](../frigate/installation.md#memryx-mx3) for informati
To configure a MemryX detector, simply set the `type` attribute to `memryx` and follow the configuration guide below.
-### Configuration
+### Configuration {#configuration-memryx}
-To configure the MemryX detector, use the following example configuration:
-
-#### Single PCIe MemryX MX3
-
-
-
-
-Navigate to and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`.
-
-
-
-
-```yaml
-detectors:
- memx0:
- type: memryx
- device: PCIe:0
-```
-
-
-
-
-#### Multiple PCIe MemryX MX3 Modules
-
-
-
-
-Navigate to and select **MemryX** from the detector type dropdown and click **Add** to add multiple detectors, specifying `PCIe:0`, `PCIe:1`, `PCIe:2`, etc. as the device for each.
-
-
-
-
-```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:
-
-
-
-
-Navigate to and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then navigate to and configure:
-
-| Field | Value |
-| ---------------------------------------- | ------------------------------------------------- |
-| **Object Detection Model Type** | `yolonas` |
-| **Object detection model input width** | `320` (can be set to `640` for higher resolution) |
-| **Object detection model input height** | `320` (can be set to `640` for higher resolution) |
-| **Model Input Tensor Shape** | `nchw` |
-| **Model Input D Type** | `float` |
-| **Label map for custom object detector** | `/labelmap/coco-80.txt` |
-
-
-
-
-```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:
-
-
-
-
-Navigate to and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then navigate to and configure:
-
-| Field | Value |
-| ---------------------------------------- | ------------------------------------------------- |
-| **Object Detection Model Type** | `yolo-generic` |
-| **Object detection model input width** | `320` (can be set to `640` for higher resolution) |
-| **Object detection model input height** | `320` (can be set to `640` for higher resolution) |
-| **Model Input Tensor Shape** | `nchw` |
-| **Model Input D Type** | `float` |
-| **Label map for custom object detector** | `/labelmap/coco-80.txt` |
-
-
-
-
-```yaml
-detectors:
- memx0:
- type: memryx
- device: PCIe:0
-
-model:
- model_type: yolo-generic
- width: 320 # (Can be set to 640 for higher resolution)
- height: 320 # (Can be set to 640 for higher resolution)
- input_tensor: nchw
- input_dtype: float
- labelmap_path: /labelmap/coco-80.txt
- # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model.
- # path: /config/yolov9.zip
- # The .zip file must contain:
- # ├── yolov9.dfp (a file ending with .dfp)
-```
-
-
-
-
-#### YOLOX
-
-The model is sourced from the [OpenCV Model Zoo](https://github.com/opencv/opencv_zoo) and precompiled to DFP.
-
-##### Configuration
-
-Below is the recommended configuration for using the **YOLOX** (small) model with the MemryX detector:
-
-
-
-
-Navigate to and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then navigate to and configure:
-
-| Field | Value |
-| ---------------------------------------- | ----------------------- |
-| **Object Detection Model Type** | `yolox` |
-| **Object detection model input width** | `640` |
-| **Object detection model input height** | `640` |
-| **Model Input Tensor Shape** | `nchw` |
-| **Model Input D Type** | `float_denorm` |
-| **Label map for custom object detector** | `/labelmap/coco-80.txt` |
-
-
-
-
-```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:
-
-
-
-
-Navigate to and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then navigate to and configure:
-
-| Field | Value |
-| ---------------------------------------- | ----------------------- |
-| **Object Detection Model Type** | `ssd` |
-| **Object detection model input width** | `320` |
-| **Object detection model input height** | `320` |
-| **Model Input Tensor Shape** | `nchw` |
-| **Model Input D Type** | `float` |
-| **Label map for custom object detector** | `/labelmap/coco-80.txt` |
-
-
-
-
-```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:
+To use your own custom model, first compile it into a [.dfp](https://developer.memryx.com/2p1/specs/files.html#dataflow-program) file, which is the format used by MemryX.
-1. Package your compiled model into a `.zip` file.
+#### Compile the Model
-2. The `.zip` must contain the compiled `.dfp` file.
+Custom models must be compiled using **MemryX SDK 2.1**.
-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`.
+Before compiling your model, install the MemryX Neural Compiler tools from the
+[Install Tools](https://developer.memryx.com/2p1/get_started/install_tools.html) page on the **host**.
-4. Bind-mount the `.zip` file into the container and specify its path using `model.path` in your config.
+> **Note:** It is recommended to compile the model on the host machine, or on another separate machine, rather than inside the Frigate Docker container. Installing the compiler inside Docker may conflict with container packages. It is recommended to create a Python virtual environment and install the compiler there.
-5. Update the `labelmap_path` to match your custom model's labels.
+Once the SDK 2.1 environment is set up, follow the
+[MemryX Compiler](https://developer.memryx.com/2p1/tools/neural_compiler.html#usage) documentation to compile your model.
-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).
+Example:
+
+```bash
+mx_nc -m yolonas.onnx -c 4 --autocrop -v --dfp_fname yolonas.dfp
+```
+
+For detailed instructions on compiling models, refer to the [MemryX Compiler](https://developer.memryx.com/2p1/tools/neural_compiler.html#usage) docs and [Tutorials](https://developer.memryx.com/2p1/tutorials/tutorials.html).
+
+#### Package the Compiled Model
+
+1. Package your compiled model into a `.zip` file.
+
+2. The `.zip` file must contain the compiled `.dfp` file.
+
+3. Depending on the model, the compiler may also generate a cropped post-processing network. If present, it will be named with the suffix `_post.onnx`.
+
+4. Bind-mount the `.zip` file into the container and specify its path using `model.path` in your config.
+
+5. Update `labelmap_path` to match your custom model's labels.
```yaml
# The detector automatically selects the default model if nothing is provided in the config.
@@ -1698,40 +632,7 @@ The TensorRT detector uses `.trt` model files that are located in `/config/model
Use the config below to work with generated TRT models:
-
-
-
-Navigate to and select **TensorRT** from the detector type dropdown and click **Add**, then set the device to `0` (the default GPU index). Then navigate to and configure:
-
-| Field | Value |
-| ---------------------------------------- | ------------------------------------------------------------ |
-| **Custom object detector model path** | `/config/model_cache/tensorrt/yolov7-320.trt` |
-| **Label map for custom object detector** | `/labelmap/coco-80.txt` |
-| **Model Input Tensor Shape** | `nchw` |
-| **Model Input Pixel Color Format** | `rgb` |
-| **Object detection model input width** | `320` (MUST match the chosen model, e.g., yolov7-320 -> 320) |
-| **Object detection model input height** | `320` (MUST match the chosen model, e.g., yolov7-320 -> 320) |
-
-
-
-
-```yaml
-detectors:
- tensorrt:
- type: tensorrt
- device: 0 #This is the default, select the first GPU
-
-model:
- path: /config/model_cache/tensorrt/yolov7-320.trt
- labelmap_path: /labelmap/coco-80.txt
- input_tensor: nchw
- input_pixel_format: rgb
- width: 320 # MUST match the chosen model i.e yolov7-320 -> 320, yolov4-416 -> 416
- height: 320 # MUST match the chosen model i.e yolov7-320 -> 320 yolov4-416 -> 416
-```
-
-
-
+
## Synaptics
@@ -1745,47 +646,11 @@ This implementation is based on sdk `v1.5.0`.
See the [installation docs](../frigate/installation.md#synaptics) for information on configuring the SL-series NPU hardware.
-### Configuration
+### Configuration {#configuration-synaptics}
When configuring the Synap detector, you have to specify the model: a local **path**.
-#### SSD Mobilenet
-
-A synap model is provided in the container at /mobilenet.synap and is used by this detector type by default. The model comes from [Synap-release Github](https://github.com/synaptics-astra/synap-release/tree/v1.5.0/models/dolphin/object_detection/coco/model/mobilenet224_full80).
-
-Use the model configuration shown below when using the synaptics detector with the default synap model:
-
-
-
-
-Navigate to and select **Synaptics** from the detector type dropdown and click **Add**. Then navigate to and configure:
-
-| Field | Value |
-| ---------------------------------------- | ---------------------------- |
-| **Custom object detector model path** | `/synaptics/mobilenet.synap` |
-| **Object detection model input width** | `224` |
-| **Object detection model input height** | `224` |
-| **Model Input Tensor Shape** | `nhwc` |
-| **Label map for custom object detector** | `/labelmap/coco-80.txt` |
-
-
-
-
-```yaml
-detectors: # required
- synap_npu: # required
- type: synaptics # required
-
-model: # required
- path: /synaptics/mobilenet.synap # required
- width: 224 # required
- height: 224 # required
- input_tensor: nhwc # default value (optional. If you change the model, it is required)
- labelmap_path: /labelmap/coco-80.txt # required
-```
-
-
-
+
## Rockchip platform
@@ -1809,14 +674,6 @@ If no custom model is provided, the RKNN detector downloads a default model from
When using many cameras one detector may not be enough to keep up. Multiple detectors can be defined assuming NPU resources are available. An example configuration would be:
-
-
-
-Navigate to and select **RKNN** from the detector type dropdown and click **Add** to add multiple detectors, each with `num_cores` set to `0` for automatic selection.
-
-
-
-
```yaml
detectors:
rknn_0:
@@ -1827,9 +684,6 @@ detectors:
num_cores: 0
```
-
-
-
:::
### Prerequisites
@@ -1851,27 +705,6 @@ $ cat /sys/kernel/debug/rknpu/load
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.
-
-
-
-Navigate to and select **RKNN** from the detector type dropdown and click **Add**. Set `num_cores` to `0` for automatic selection (increase for better performance on multicore NPUs, e.g., set to `3` on rk3588).
-
-
-
-
-```yaml
-detectors: # required
- rknn: # required
- type: rknn # required
- # number of NPU cores to use
- # 0 means choose automatically
- # increase for better performance if you have a multicore NPU e.g. set to 3 on rk3588
- num_cores: 0
-```
-
-
-
-
The inference time was determined on a rk3588 with 3 NPU cores.
| Model | Size in mb | Inference time in ms |
@@ -1886,136 +719,13 @@ The inference time was determined on a rk3588 with 3 NPU cores.
- All models are automatically downloaded and stored in the folder `config/model_cache/rknn_cache`. After upgrading Frigate, you should remove older models to free up space.
- You can also provide your own `.rknn` model. You should not save your own models in the `rknn_cache` folder, store them directly in the `model_cache` folder or another subfolder. To convert a model to `.rknn` format see the `rknn-toolkit2` (requires a x86 machine). Note, that there is only post-processing for the supported models.
-#### YOLO-NAS
-
-
-
-
-Navigate to and configure:
-
-| Field | Value |
-| ---------------------------------------- | ----------------------------------------------------------------------- |
-| **Custom object detector model path** | `deci-fp16-yolonas_s` (or `deci-fp16-yolonas_m`, `deci-fp16-yolonas_l`) |
-| **Object Detection Model Type** | `yolonas` |
-| **Object detection model input width** | `320` |
-| **Object detection model input height** | `320` |
-| **Model Input Pixel Color Format** | `bgr` |
-| **Model Input Tensor Shape** | `nhwc` |
-| **Label map for custom object detector** | `/labelmap/coco-80.txt` |
-
-
-
-
-```yaml
-model: # required
- # name of model (will be automatically downloaded) or path to your own .rknn model file
- # possible values are:
- # - deci-fp16-yolonas_s
- # - deci-fp16-yolonas_m
- # - deci-fp16-yolonas_l
- # your yolonas_model.rknn
- path: deci-fp16-yolonas_s
- model_type: yolonas
- width: 320
- height: 320
- input_pixel_format: bgr
- input_tensor: nhwc
- labelmap_path: /labelmap/coco-80.txt
-```
-
-
-
-
-:::warning
-
-The pre-trained YOLO-NAS weights from DeciAI are subject to their license and can't be used commercially. For more information, see: https://docs.deci.ai/super-gradients/latest/LICENSE.YOLONAS.html
-
-:::
-
-#### YOLO (v9)
-
-
-
-
-Navigate to and configure:
-
-| Field | Value |
-| ---------------------------------------- | -------------------------------------------------- |
-| **Custom object detector model path** | `frigate-fp16-yolov9-t` (or other yolov9 variants) |
-| **Object Detection Model Type** | `yolo-generic` |
-| **Object detection model input width** | `320` |
-| **Object detection model input height** | `320` |
-| **Model Input Tensor Shape** | `nhwc` |
-| **Label map for custom object detector** | `/labelmap/coco-80.txt` |
-
-
-
-
-```yaml
-model: # required
- # name of model (will be automatically downloaded) or path to your own .rknn model file
- # possible values are:
- # - frigate-fp16-yolov9-t
- # - frigate-fp16-yolov9-s
- # - frigate-fp16-yolov9-m
- # - frigate-fp16-yolov9-c
- # - frigate-fp16-yolov9-e
- # your yolo_model.rknn
- path: frigate-fp16-yolov9-t
- model_type: yolo-generic
- width: 320
- height: 320
- input_tensor: nhwc
- labelmap_path: /labelmap/coco-80.txt
-```
-
-
-
-
-#### YOLOx
-
-
-
-
-Navigate to and configure:
-
-| Field | Value |
-| ---------------------------------------- | ---------------------------------------------- |
-| **Custom object detector model path** | `rock-i8-yolox_nano` (or other yolox variants) |
-| **Object Detection Model Type** | `yolox` |
-| **Object detection model input width** | `416` |
-| **Object detection model input height** | `416` |
-| **Model Input Tensor Shape** | `nhwc` |
-| **Label map for custom object detector** | `/labelmap/coco-80.txt` |
-
-
-
-
-```yaml
-model: # required
- # name of model (will be automatically downloaded) or path to your own .rknn model file
- # possible values are:
- # - rock-i8-yolox_nano
- # - rock-i8-yolox_tiny
- # - rock-fp16-yolox_nano
- # - rock-fp16-yolox_tiny
- # your yolox_model.rknn
- path: rock-i8-yolox_nano
- model_type: yolox
- width: 416
- height: 416
- input_tensor: nhwc
- labelmap_path: /labelmap/coco-80.txt
-```
-
-
-
+
### Converting your own onnx model to rknn format
To convert a onnx model to the rknn format using the [rknn-toolkit2](https://github.com/airockchip/rknn-toolkit2/) you have to:
-- Place one ore more models in onnx format in the directory `config/model_cache/rknn_cache/onnx` on your docker host (this might require `sudo` privileges).
+- Place one or more models in onnx format in the directory `config/model_cache/rknn_cache/onnx` on your docker host (this might require `sudo` privileges).
- Save the configuration file under `config/conv2rknn.yaml` (see below for details).
- Run `docker exec python3 /opt/conv2rknn.py`. If the conversion was successful, the rknn models will be placed in `config/model_cache/rknn_cache`.
@@ -2033,13 +743,13 @@ config:
quant_img_RGB2BGR: true
```
-Explanation of the paramters:
+Explanation of the parameters:
- `soc`: A list of all SoCs you want to build the rknn model for. If you don't specify this parameter, the script tries to find out your SoC and builds the rknn model for this one.
- `quantization`: true: 8 bit integer (i8) quantization, false: 16 bit float (fp16). Default: false.
- `output_name`: The output name of the model. The following variables are available:
- `quant`: "i8" or "fp16" depending on the config
- - `input_basename`: the basename of the input model (e.g. "my_model" if the input model is calles "my_model.onnx")
+ - `input_basename`: the basename of the input model (e.g. "my_model" if the input model is called "my_model.onnx")
- `soc`: the SoC this model was build for (e.g. "rk3588")
- `tk_version`: Version of `rknn-toolkit2` (e.g. "2.3.0")
- **example**: Specifying `output_name = "frigate-{quant}-{input_basename}-{soc}-v{tk_version}"` could result in a model called `frigate-i8-my_model-rk3588-v2.3.0.rknn`.
@@ -2049,7 +759,7 @@ Explanation of the paramters:
DeGirum is a detector that can use any type of hardware listed on [their website](https://hub.degirum.com). DeGirum can be used with local hardware through a DeGirum AI Server, or through the use of `@local`. You can also connect directly to DeGirum's AI Hub to run inferences. **Please Note:** This detector _cannot_ be used for commercial purposes.
-### Configuration
+### Configuration {#configuration-degirum}
#### AI Server Inference
@@ -2068,24 +778,7 @@ All supported hardware will automatically be found on your AI server host as lon
Once completed, configure the detector as follows:
-
-
-
-Navigate to and select **DeGirum** from the detector type dropdown and click **Add**. Set the location to your AI server (e.g., service name, container name, or `host:port`), the zoo to `degirum/public`, and provide your authentication token if needed.
-
-
-
-
-```yaml
-degirum_detector:
- type: degirum
- location: degirum # Set to service name (degirum_detector), container_name (degirum), or a host:port (192.168.29.4:8778)
- zoo: degirum/public # DeGirum's public model zoo. Zoo name should be in format "workspace/zoo_name". degirum/public is available to everyone, so feel free to use it if you don't know where to start. If you aren't pulling a model from the AI Hub, leave this and 'token' blank.
- token: dg_example_token # For authentication with the AI Hub. Get this token through the "tokens" section on the main page of the [AI Hub](https://hub.degirum.com). This can be left blank if you're pulling a model from the public zoo and running inferences on your local hardware using @local or a local DeGirum AI Server
-```
-
-
-
+
Setting up a model in the `config.yml` is similar to setting up an AI server.
You can set it to:
@@ -2111,24 +804,7 @@ It is also possible to eliminate the need for an AI server and run the hardware
2. To double check the runtime is detected by the DeGirum detector, make sure the `degirum sys-info` command properly shows whatever runtimes you mean to install.
3. Create a DeGirum detector in your configuration.
-
-
-
-Navigate to and select **DeGirum** from the detector type dropdown and click **Add**. Set the location to `@local`, the zoo to `degirum/public`, and provide your authentication token.
-
-
-
-
-```yaml
-degirum_detector:
- type: degirum
- location: "@local" # For accessing AI Hub devices and models
- zoo: degirum/public # DeGirum's public model zoo. Zoo name should be in format "workspace/zoo_name". degirum/public is available to everyone, so feel free to use it if you don't know where to start.
- token: dg_example_token # For authentication with the AI Hub. Get this token through the "tokens" section on the main page of the [AI Hub](https://hub.degirum.com). This can be left blank if you're pulling a model from the public zoo and running inferences on your local hardware using @local or a local DeGirum AI Server
-```
-
-
-
+
Once `degirum_detector` is setup, you can choose a model through 'model' section in the `config.yml` file.
@@ -2148,24 +824,7 @@ If you do not possess whatever hardware you want to run, there's also the option
2. Get an access token.
3. Create a DeGirum detector in your configuration.
-
-
-
-Navigate to and select **DeGirum** from the detector type dropdown and click **Add**. Set the location to `@cloud`, the zoo to `degirum/public`, and provide your authentication token.
-
-
-
-
-```yaml
-degirum_detector:
- type: degirum
- location: "@cloud" # For accessing AI Hub devices and models
- zoo: degirum/public # DeGirum's public model zoo. Zoo name should be in format "workspace/zoo_name". degirum/public is available to everyone, so feel free to use it if you don't know where to start.
- token: dg_example_token # For authentication with the AI Hub. Get this token through the "tokens" section on the main page of the (AI Hub)[https://hub.degirum.com).
-```
-
-
-
+
Once `degirum_detector` is setup, you can choose a model through 'model' section in the `config.yml` file.
@@ -2194,159 +853,8 @@ The AXEngine detector downloads its default model from HuggingFace on first star
:::
-### Configuration
+### Configuration {#configuration-axengine}
When configuring the AXEngine detector, you have to specify the model name.
-#### yolov9
-
-A yolov9 model is provided in the container at `/axmodels` and is used by this detector type by default.
-
-Use the model configuration shown below when using the axengine detector with the default axmodel:
-
-
-
-
-Navigate to and select **AXEngine NPU** from the detector type dropdown and click **Add**. Then navigate to and configure:
-
-| Field | Value |
-| ---------------------------------------- | ----------------------- |
-| **Custom object detector model path** | `frigate-yolov9-tiny` |
-| **Object Detection Model Type** | `yolo-generic` |
-| **Object detection model input width** | `320` |
-| **Object detection model input height** | `320` |
-| **Model Input D Type** | `int` |
-| **Model Input Pixel Color Format** | `bgr` |
-| **Label map for custom object detector** | `/labelmap/coco-80.txt` |
-
-
-
-
-```yaml
-detectors:
- axengine:
- type: axengine
-
-model:
- path: frigate-yolov9-tiny
- model_type: yolo-generic
- width: 320
- height: 320
- input_dtype: int
- input_pixel_format: bgr
- labelmap_path: /labelmap/coco-80.txt
-```
-
-
-
-
-# Models
-
-Some model types are not included in Frigate by default.
-
-## Downloading Models
-
-Here are some tips for getting different model types
-
-### Downloading D-FINE Model
-
-D-FINE can be exported as ONNX by running the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=s` in the first line to `s`, `m`, or `l` size.
-
-```sh
-docker build . --build-arg MODEL_SIZE=s --output . -f- <<'EOF'
-FROM python:3.11 AS build
-RUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/*
-COPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/
-WORKDIR /dfine
-RUN git clone https://github.com/Peterande/D-FINE.git .
-RUN uv pip install --system -r requirements.txt
-RUN uv pip install --system onnx onnxruntime onnxsim onnxscript
-# Create output directory and download checkpoint
-RUN mkdir -p output
-ARG MODEL_SIZE
-RUN wget https://github.com/Peterande/storage/releases/download/dfinev1.0/dfine_${MODEL_SIZE}_obj2coco.pth -O output/dfine_${MODEL_SIZE}_obj2coco.pth
-# Modify line 58 of export_onnx.py to change batch size to 1
-RUN sed -i '58s/data = torch.rand(.*)/data = torch.rand(1, 3, 640, 640)/' tools/deployment/export_onnx.py
-RUN python3 tools/deployment/export_onnx.py -c configs/dfine/objects365/dfine_hgnetv2_${MODEL_SIZE}_obj2coco.yml -r output/dfine_${MODEL_SIZE}_obj2coco.pth
-FROM scratch
-ARG MODEL_SIZE
-COPY --from=build /dfine/output/dfine_${MODEL_SIZE}_obj2coco.onnx /dfine-${MODEL_SIZE}.onnx
-EOF
-```
-
-### Downloading RF-DETR Model
-
-RF-DETR can be exported as ONNX by running the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=Nano` in the first line to `Nano`, `Small`, or `Medium` size.
-
-```sh
-docker build . --build-arg MODEL_SIZE=Nano --rm --output . -f- <<'EOF'
-FROM python:3.12 AS build
-RUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/*
-COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/
-WORKDIR /rfdetr
-RUN uv pip install --system rfdetr[onnxexport] torch==2.8.0 onnx==1.19.1 transformers==4.57.6 onnxscript
-ARG MODEL_SIZE
-RUN python3 -c "from rfdetr import RFDETR${MODEL_SIZE}; x = RFDETR${MODEL_SIZE}(resolution=320); x.export(simplify=True)"
-FROM scratch
-ARG MODEL_SIZE
-COPY --from=build /rfdetr/output/inference_model.onnx /rfdetr-${MODEL_SIZE}.onnx
-EOF
-```
-
-### Downloading YOLO-NAS Model
-
-You can build and download a compatible model with pre-trained weights using [this notebook](https://github.com/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb) [](https://colab.research.google.com/github/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb) which can be run directly in [Google Colab](https://colab.research.google.com/github/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb).
-
-:::warning
-
-The pre-trained YOLO-NAS weights from DeciAI are subject to their license and can't be used commercially. For more information, see: https://docs.deci.ai/super-gradients/latest/LICENSE.YOLONAS.html
-
-:::
-
-The input image size in this notebook is set to 320x320. This results in lower CPU usage and faster inference times without impacting performance in most cases due to the way Frigate crops video frames to areas of interest before running detection. The notebook and config can be updated to 640x640 if desired.
-
-### Downloading YOLO Models
-
-#### YOLOx
-
-YOLOx models can be downloaded [from the YOLOx repo](https://github.com/Megvii-BaseDetection/YOLOX/tree/main/demo/ONNXRuntime).
-
-#### YOLOv3, YOLOv4, and YOLOv7
-
-To export as ONNX:
-
-```sh
-git clone https://github.com/NateMeyer/tensorrt_demos
-cd tensorrt_demos/yolo
-./download_yolo.sh
-python3 yolo_to_onnx.py -m yolov7-320
-```
-
-#### YOLOv9 for Google Coral Support
-
-[Download the model](https://github.com/dbro/frigate-detector-edgetpu-yolo9/releases/download/v1.0/yolov9-s-relu6-best_320_int8_edgetpu.tflite), bind mount the file into the container, and provide the path with `model.path`. Note that the linked model requires a 17-label [labelmap file](https://raw.githubusercontent.com/dbro/frigate-detector-edgetpu-yolo9/refs/heads/main/labels-coco17.txt) that includes only 17 COCO classes.
-
-#### YOLOv9 for other detectors
-
-YOLOv9 model can be exported as ONNX using the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=t` and `IMG_SIZE=320` in the first line to the [model size](https://github.com/WongKinYiu/yolov9#performance) you would like to convert (available model sizes are `t`, `s`, `m`, `c`, and `e`, common image sizes are `320` and `640`).
-
-```sh
-docker build . --build-arg MODEL_SIZE=t --build-arg IMG_SIZE=320 --output . -f- <<'EOF'
-FROM python:3.11 AS build
-RUN apt-get update && apt-get install --no-install-recommends -y cmake libgl1 && rm -rf /var/lib/apt/lists/*
-COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/
-WORKDIR /yolov9
-ADD https://github.com/WongKinYiu/yolov9.git .
-RUN uv pip install --system -r requirements.txt
-RUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier==0.4.* onnxscript
-ARG MODEL_SIZE
-ARG IMG_SIZE
-ADD https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-${MODEL_SIZE}-converted.pt yolov9-${MODEL_SIZE}.pt
-RUN sed -i "s/ckpt = torch.load(attempt_download(w), map_location='cpu')/ckpt = torch.load(attempt_download(w), map_location='cpu', weights_only=False)/g" models/experimental.py
-RUN python3 export.py --weights ./yolov9-${MODEL_SIZE}.pt --imgsz ${IMG_SIZE} --simplify --include onnx
-FROM scratch
-ARG MODEL_SIZE
-ARG IMG_SIZE
-COPY --from=build /yolov9/yolov9-${MODEL_SIZE}.onnx /yolov9-${MODEL_SIZE}-${IMG_SIZE}.onnx
-EOF
-```
+
diff --git a/docs/docs/configuration/object_filters.md b/docs/docs/configuration/object_filters.md
index 8a492960df..b2e7fca319 100644
--- a/docs/docs/configuration/object_filters.md
+++ b/docs/docs/configuration/object_filters.md
@@ -7,7 +7,7 @@ import ConfigTabs from "@site/src/components/ConfigTabs";
import TabItem from "@theme/TabItem";
import NavPath from "@site/src/components/NavPath";
-There are several types of object filters that can be used to reduce false positive rates.
+There are several types of object filters that can be used to reduce [false positive](/frigate/glossary#false-positive) rates.
## Object Scores
@@ -26,9 +26,9 @@ In frame 2, the score is below the `min_score` value, so Frigate ignores it and
The **top score** is the highest computed score the tracked object has ever reached during its lifetime. Because the computed score rises and falls as new frames come in, the top score can be thought of as the peak confidence Frigate had in the object. In Frigate's UI (such as the Tracking Details pane in Explore), you may see all three values:
-- **Score** — the raw detector score for that single frame.
-- **Computed Score** — the median of the most recent score history at that moment. This is the value compared against `threshold`.
-- **Top Score** — the highest computed score reached so far for the tracked object.
+- **Score**: the raw detector score for that single frame.
+- **Computed Score**: the median of the most recent score history at that moment. This is the value compared against `threshold`.
+- **Top Score**: the highest computed score reached so far for the tracked object.
### Minimum Score
@@ -36,7 +36,7 @@ Any detection below `min_score` will be immediately thrown out and never tracked
### Threshold
-`threshold` is used to determine that the object is a true positive. Once an object is detected with a score >= `threshold` object is considered a true positive. If `threshold` is too low then some higher scoring false positives may create an tracked object. If `threshold` is too high then true positive tracked objects may be missed due to the object never scoring high enough.
+`threshold` is used to determine that the object is a true positive. Once an object is detected with a score >= `threshold` object is considered a true positive. If `threshold` is too low then some higher scoring false positives may create a tracked object. If `threshold` is too high then true positive tracked objects may be missed due to the object never scoring high enough.
## Configuring Object Scores
@@ -144,8 +144,8 @@ cameras:
### Zones
-[Required zones](/configuration/zones.md) can be a great tool to reduce false positives that may be detected in the sky or other areas that are not of interest. The required zones will only create tracked objects for objects that enter the zone.
+[Required zones](/configuration/zones.md#restricting-alerts-and-detections-to-specific-zones) can be a great tool to reduce false positives that may be detected in the sky or other areas that are not of interest. The required zones will only create tracked objects for objects that enter the zone.
### Object Masks
-[Object Filter Masks](/configuration/masks) are a last resort but can be useful when false positives are in the relatively same place but can not be filtered due to their size or shape. Object filter masks can be configured in .
+[Object Filter Masks](/configuration/masks#object-filter-masks) are a last resort but can be useful when false positives are in the relatively same place but can not be filtered due to their size or shape. Object filter masks can be configured in .
diff --git a/docs/docs/configuration/objects.md b/docs/docs/configuration/objects.md
index 9925ae8fe1..e5a24ea081 100644
--- a/docs/docs/configuration/objects.md
+++ b/docs/docs/configuration/objects.md
@@ -158,4 +158,4 @@ Models for both CPU and EdgeTPU (Coral) are bundled in the image. You can use yo
- EdgeTPU Model: `/edgetpu_model.tflite`
- Labels: `/labelmap.txt`
-You also need to update the [model config](advanced.md#model) if they differ from the defaults.
+You also need to update the [model config](advanced/system.md#model) if they differ from the defaults.
diff --git a/docs/docs/configuration/profiles.md b/docs/docs/configuration/profiles.md
index acb6cf4826..3eab2bd131 100644
--- a/docs/docs/configuration/profiles.md
+++ b/docs/docs/configuration/profiles.md
@@ -14,13 +14,13 @@ Profiles allow you to define named sets of camera configuration overrides that c
Profiles operate as a two-level system:
1. **Profile definitions** are declared at the top level of your config under `profiles`. Each definition has a machine name (the key) and a `friendly_name` for display in the UI.
-2. **Camera profile overrides** are declared under each camera's `profiles` section, keyed by the profile name. Only the settings you want to change need to be specified — everything else is inherited from the camera's base configuration.
+2. **Camera profile overrides** are declared under each camera's `profiles` section, keyed by the profile name. Only the settings you want to change need to be specified. Everything else is inherited from the camera's base configuration.
When a profile is activated, Frigate merges each camera's profile overrides on top of its base config. When the profile is deactivated, all cameras revert to their original settings. Only one profile can be active at a time.
:::info
-Profile changes are applied in-memory and take effect immediately — no restart is required. The active profile is persisted across Frigate restarts (stored in the `/config/.profiles` file).
+Profile changes are applied in-memory and take effect immediately. No restart is required. The active profile is persisted across Frigate restarts (stored in the `/config/.profiles` file).
:::
@@ -33,10 +33,10 @@ The easiest way to define profiles is to use the Frigate UI. Profiles can also b
-1. **Create a profile** — Navigate to . Click the **Add Profile** button, enter a name (and optionally a profile ID).
-2. **Configure overrides** — Navigate to a camera configuration section (e.g. Motion detection, Record, Notifications). In the top right, two buttons will appear - choose a camera and a profile from the profile selector to edit overrides for that camera and section. Only the fields you change will be stored as overrides — fields that require a restart are hidden since profiles are applied at runtime. You can click the **Remove Profile Override** button to clear overrides.
-3. **Activate a profile** — Use the **Profiles** option in Frigate's main menu to choose a profile. Alternatively, in Settings, navigate to , then choose a profile in the Active Profile dropdown to activate it. The active profile is also shown in the status bar at the bottom of the screen on desktop browsers.
-4. **Delete a profile** — Navigate to , then click the trash icon for a profile. This removes the profile definition and all camera overrides associated with it.
+1. **Create a profile**: Navigate to . Click the **Add Profile** button, enter a name (and optionally a profile ID).
+2. **Configure overrides**: Navigate to a camera configuration section (e.g. Motion detection, Record, Notifications). In the top right, two buttons will appear - choose a camera and a profile from the profile selector to edit overrides for that camera and section. Only the fields you change will be stored as overrides. Fields that require a restart are hidden since profiles are applied at runtime. You can click the **Remove Profile Override** button to clear overrides.
+3. **Activate a profile**: Use the **Profiles** option in Frigate's main menu to choose a profile. Alternatively, in Settings, navigate to , then choose a profile in the Active Profile dropdown to activate it. The active profile is also shown in the status bar at the bottom of the screen on desktop browsers.
+4. **Delete a profile**: Navigate to , then click the trash icon for a profile. This removes the profile definition and all camera overrides associated with it.
@@ -126,7 +126,11 @@ Only the fields you explicitly set in a profile override are applied. All other
## Activating Profiles
-Profiles can be activated and deactivated from the Frigate UI. Open the Settings cog and select **Profiles** from the submenu to see all defined profiles. From there you can activate any profile or deactivate the current one. The active profile is indicated in the UI so you always know which profile is in effect.
+Profiles can be activated and deactivated via the Frigate UI, [MQTT](/integrations/mqtt#frigateprofileset), or the Home Assistant integration.
+
+In the Frigate UI, open the Settings cog and select **Profiles** from the submenu to see all defined profiles. From there you can activate any profile or deactivate the current one. The active profile is indicated in the UI so you always know which profile is in effect.
+
+Activating or deactivating a profile clears any [runtime toggle overrides](/configuration/live#runtime-toggle-persistence) so the profile's settings aren't silently undone by a stale toggle from before the switch.
## Example: Home / Away Setup
@@ -135,10 +139,10 @@ A common use case is having different detection and notification settings based
-1. Navigate to and create two profiles: **Home** and **Away**.
+1. Navigate to and create two profiles: **Home** and **Away**.
2. From to the Camera configuration section in Settings, choose the **front_door** camera, and select the **Away** profile from the profile dropdown. Then, enable notifications from the Notifications pane, and set alert labels to `person` and `car` from the Review pane. Then, from the profile dropdown choose **Home** profile, then navigate to Notifications to disable notifications.
3. For the **indoor_cam** camera, perform similar steps - configure the **Away** profile to enable the camera, detection, and recording. Configure the **Home** profile to disable the camera entirely for privacy.
-4. Activate the desired profile from or from the **Profiles** option in Frigate's main menu.
+4. Activate the desired profile from or from the **Profiles** option in Frigate's main menu.
@@ -207,3 +211,33 @@ In this example:
- **Away profile**: The front door camera enables notifications and tracks specific alert labels. The indoor camera is fully enabled with detection and recording.
- **Home profile**: The front door camera disables notifications. The indoor camera is completely disabled for privacy.
- **No profile active**: All cameras use their base configuration values.
+
+## FAQ
+
+### Can I define a zone or mask in a profile but not have it in the base config?
+
+No. Profiles are pure overrides. Every zone and mask defined under a profile must reference an entry that already exists on the base camera config. Configurations that introduce profile-only zones or masks are rejected at startup.
+
+If you want a zone or mask to be active only under a specific profile, define it on the base config with `enabled: false`, then enable it in that profile's overrides.
+
+### How do I revert a profile zone or mask override back to the base configuration?
+
+Delete the override. In the Frigate UI, edit the profile and use the "Revert override" action (the trash can icon) on the zone or mask. The base entry is left untouched, and once the override is removed the profile inherits the base values for that zone or mask.
+
+### Can multiple profiles be active at the same time?
+
+No. Only one profile can be active at a time. Activating a new profile automatically deactivates the current one.
+
+### What happens to my profile overrides if I delete a zone or mask from the base?
+
+When you delete a base zone or mask in the Frigate UI, any profile overrides for that entry are deleted automatically as part of the same operation. If you remove a base entry by editing your config file directly and leave a profile override behind, the config will fail validation at startup until the orphaned override is removed as well.
+
+### Why are some settings missing when I configure a profile override?
+
+Fields that require a Frigate restart to take effect cannot be overridden by profiles, since profiles are applied at runtime without restarting. Those fields are hidden when editing a profile override and can only be changed on the base configuration.
+
+### Can I schedule profiles to be enabled or disabled at certain times?
+
+Not within Frigate itself. Frigate is an NVR, not an automation platform, so it intentionally does not include a scheduler for activating profiles. Instead, activate profiles from an automation platform that already handles time- and event-based triggers well, such as [Home Assistant](https://www.home-assistant.io/) or [Node-RED](https://nodered.org/). These integrate with Frigate and give you far more robust and flexible scheduling than a built-in scheduler could.
+
+If you prefer something lightweight, a simple script driven by a cron job that toggles profiles on a schedule works too.
diff --git a/docs/docs/configuration/record.md b/docs/docs/configuration/record.md
index 614beafed7..87a24d3d60 100644
--- a/docs/docs/configuration/record.md
+++ b/docs/docs/configuration/record.md
@@ -11,6 +11,12 @@ Recordings can be enabled and are stored at `/media/frigate/recordings`. The fol
New recording segments are written from the camera stream to cache, they are only moved to disk if they match the setup recording retention policy.
+:::tip
+
+To keep a specific clip beyond your retention window, [export](/usage/exports) it rather than increasing retention for the whole camera. Exports are saved separately and are never removed by retention.
+
+:::
+
H265 recordings can be viewed in Chrome 108+, Edge and Safari only. All other browsers require recordings to be encoded with H264.
## Common recording configurations
@@ -164,9 +170,9 @@ record:
The `pre_capture` and `post_capture` values define the **time window** around a review item, but only recording segments that also match the configured **retention mode** are actually kept on disk.
-- **`mode: all`** — Retains every segment within the capture window, regardless of whether motion was detected.
-- **`mode: motion`** (default) — Only retains segments within the capture window that contain motion. This includes segments with active tracked objects, since object motion implies motion. Segments without any motion are discarded even if they fall within the pre/post capture range.
-- **`mode: active_objects`** — Only retains segments within the capture window where tracked objects were actively moving. Segments with general motion but no active objects are discarded.
+- **`mode: all`**: Retains every segment within the capture window, regardless of whether motion was detected.
+- **`mode: motion`** (default): Only retains segments within the capture window that contain motion. This includes segments with active tracked objects, since object motion implies motion. Segments without any motion are discarded even if they fall within the pre/post capture range.
+- **`mode: active_objects`**: Only retains segments within the capture window where tracked objects were actively moving. Segments with general motion but no active objects are discarded.
This means that with the default `motion` mode, you may see less footage than the configured pre/post capture duration if parts of the capture window had no motion.
@@ -191,11 +197,7 @@ Because recording segments are written in 10 second chunks, pre-capture timing d
### Where to view pre/post capture footage
-Pre and post capture footage is included in the **recording timeline**, visible in the History view. Note that pre/post capture settings only affect which recording segments are **retained on disk** — they do not change the start and end points shown in the UI. The History view will still center on the review item's actual time range, but you can scrub backward and forward through the retained pre/post capture footage on the timeline. The Explore view shows object-specific clips that are trimmed to when the tracked object was actually visible, so pre/post capture time will not be reflected there.
-
-## Will Frigate delete old recordings if my storage runs out?
-
-As of Frigate 0.12 if there is less than an hour left of storage, the oldest 2 hours of recordings will be deleted.
+Pre and post capture footage is included in the **recording timeline**, visible in the History view. Note that pre/post capture settings only affect which recording segments are **retained on disk**. They do not change the start and end points shown in the UI. The History view will still center on the review item's actual time range, but you can scrub backward and forward through the retained pre/post capture footage on the timeline. The Explore view shows object-specific clips that are trimmed to when the tracked object was actually visible, so pre/post capture time will not be reflected there.
## Configuring Recording Retention
@@ -349,3 +351,63 @@ Setting `verbose: true` writes a detailed report of every orphaned file and data
This operation uses considerable CPU resources and includes a safety threshold that aborts if more than 50% of files would be deleted. Only run when necessary. If you set `force: true` the safety threshold will be bypassed; do not use `force` unless you are certain the deletions are intended.
:::
+
+## Understanding storage usage
+
+The storage usage Frigate reports will not exactly match what the operating system reports with `df` or `du`. This is expected, not a bug. The sections below explain how Frigate derives its storage figures and why they differ from the disk's own accounting.
+
+### How Frigate measures recording usage
+
+The **Recordings** value on the Storage Metrics page ( ), and the per-camera **Camera Storage** breakdown, is the sum of the recording segment sizes Frigate has written, taken from Frigate's database. It is **not** computed by a scan of the disk. Frigate tracks usage this way by design: repeatedly walking the entire drive to total its size would keep hard drives spun up and add unnecessary I/O.
+
+The disk **total** shown beside it, and the free-space figure Frigate uses to decide when to delete recordings, instead come from the operating system's report for the whole filesystem mounted at `/media/frigate`. As a result, the **Unused** value on the page is _total disk capacity minus Frigate's recordings_, not the drive's real free space, which will be lower whenever anything else is stored on the disk.
+
+### What counts toward usage, and why it won't match `df`
+
+Only **recording segments** (`/media/frigate/recordings`) are included in the recordings storage total. Plenty of other things consume real disk space but are **not** part of that number:
+
+- **Snapshots and thumbnails** (`/media/frigate/clips`): see [Snapshots](/configuration/snapshots). These are retained independently of recordings.
+- **Preview videos** and **review thumbnails** (also under `/media/frigate/clips`).
+- **Exports** (`/media/frigate/exports`): exports are never removed by retention.
+- **The database, downloaded detection models, and face / license plate training images** (stored under `/config`).
+- **Debug images from enrichments** (`/media/frigate/clips`): when enabled, License Plate Recognition's `debug_save_plates` and GenAI's `debug_save_thumbnails` save plate crops and request images for troubleshooting.
+
+These files are the usual explanation for an "other" or seemingly unaccounted bucket of space: it is real, it is Frigate's, and it simply isn't part of the _recordings_ total. They are also why comparing the **Recordings** figure to `df -h` always shows a gap: `df` additionally counts any non-Frigate data on the disk, filesystem overhead and reserved blocks (ext4 reserves ~5% for root by default, so a disk can read "full" before recordings approach the total), and recently deleted recordings whose space has not yet been reclaimed.
+
+:::tip
+
+The Storage page is not intended to be a system-wide disk monitor: it shows how much space _Frigate's recordings_ use. To see true disk usage, use `df -h` (free space) and `du -sh` (per-directory usage) on the host.
+
+:::
+
+### Free space and the `/media/frigate` mount
+
+Frigate reports the capacity and free space of whatever filesystem is actually mounted at `/media/frigate` **inside the container**. If an external drive or network share isn't truly mounted there (a missing `/etc/fstab` entry, a share that was offline when the container started, or a host that doesn't pass the path through), the container falls back to the host's OS disk, and Frigate will correctly report that smaller disk instead of the drive you intended.
+
+If the reported capacity doesn't match your drive, the mount is the place to look, not Frigate. Verify what is actually mounted from inside the container:
+
+```bash
+docker exec -it frigate df -h /media/frigate
+docker exec -it frigate mount | grep media
+```
+
+See the [storage mount layout](/frigate/installation#storage) for how the volumes are expected to be configured.
+
+### The `/tmp/cache` area is separate
+
+Recording segments are first written to `/tmp/cache`, a small, in-memory (`tmpfs`) area, before being checked and moved to `/media/frigate/recordings`. Because it is separate and small, `/tmp/cache` can fill up and produce `No space left on device` errors even when the recordings disk has plenty of room. They are different storage areas. See [Recordings troubleshooting](/troubleshooting/recordings) for diagnosing cache and slow-storage issues.
+
+### When the metrics don't match what's on disk
+
+Because usage is tracked in the database, deleting recording files directly on disk, or files left behind after an upgrade, will not update the reported usage, and can even push it above 100%. Frigate is unaware of files it didn't record and won't count or remove them automatically. Use [Syncing Media Files With Disk](#syncing-media-files-with-disk) to reconcile the database with what is actually on disk.
+
+## Will Frigate delete old recordings if my storage runs out?
+
+Yes. Frigate continuously checks the **free space of the disk** holding `/media/frigate/recordings`. This is different from adding up the size of every recording: free space is a single number the operating system already tracks, so Frigate can ask for it instantly without reading through your files or spinning up the disk, which is exactly why it relies on this check rather than scanning the drive. When less than roughly one hour of recording space remains (estimated from the current recording bitrate, **not** a fixed percentage), Frigate deletes the oldest recordings to reclaim space and logs a message. This emergency cleanup removes the oldest recordings first **regardless of retention settings**.
+
+Two consequences follow from this being based on whole-disk free space:
+
+- Because the check uses the disk's real free space, **anything** filling the drive, including non-Frigate files, can trigger deletion of your oldest recordings.
+- Cleanup can run while a meaningful percentage of the disk is still free (for example, with high bitrates or many cameras), because the threshold is "less than ~1 hour of recording headroom," not "X% full."
+
+Frequent emergency cleanups usually mean your configured retention exceeds what the disk can hold. Reduce your retention days so the normal retention cleanup keeps up and the emergency path rarely triggers.
diff --git a/docs/docs/configuration/restream.md b/docs/docs/configuration/restream.md
index af4d635c6e..1ae96b64cc 100644
--- a/docs/docs/configuration/restream.md
+++ b/docs/docs/configuration/restream.md
@@ -11,7 +11,7 @@ import NavPath from "@site/src/components/NavPath";
Frigate can restream your video feed as an RTSP feed for other applications such as Home Assistant to utilize it at `rtsp://:8554/`. Port 8554 must be open. [This allows you to use a video feed for detection in Frigate and Home Assistant live view at the same time without having to make two separate connections to the camera](#reduce-connections-to-camera). The video feed is copied from the original video feed directly to avoid re-encoding. This feed does not include any annotation by Frigate.
-Frigate uses [go2rtc](https://github.com/AlexxIT/go2rtc/tree/v1.9.13) to provide its restream and MSE/WebRTC capabilities. The go2rtc config is hosted at the `go2rtc` in the config, see [go2rtc docs](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#configuration) for more advanced configurations and features.
+Frigate uses [go2rtc](https://github.com/AlexxIT/go2rtc/tree/v1.9.14) to provide its restream and MSE/WebRTC capabilities. The go2rtc config is hosted at the `go2rtc` in the config, see [go2rtc docs](https://github.com/AlexxIT/go2rtc/tree/v1.9.14#configuration) for more advanced configurations and features.
:::note
@@ -61,7 +61,7 @@ Configure the go2rtc stream and point the camera inputs at the local restream.
-Navigate to and add stream entries for each camera. Then navigate to for each camera and set the input paths to use the local restream URL (`rtsp://127.0.0.1:8554/`).
+Navigate to and add stream entries for each camera. Then navigate to for each camera. For each input, choose **Restream (go2rtc)** and pick the matching stream from the dropdown. Frigate uses the local restream URL (`rtsp://127.0.0.1:8554/`) and the `preset-rtsp-restream` input args for that input automatically. (Choose **Manual input path** instead to type a URL directly.)
@@ -111,7 +111,7 @@ Two connections are made to the camera. One for the sub stream, one for the rest
-Navigate to and add stream entries for each camera and its sub stream. Then navigate to for each camera and configure separate inputs for the main and sub streams using the local restream URLs.
+Navigate to and add stream entries for each camera and its sub stream. Then navigate to for each camera and add separate inputs for the main and sub streams. Set each input's source to **Restream (go2rtc)** and pick the matching stream from the dropdown. Frigate uses the local restream URL and the `preset-rtsp-restream` input args for that input automatically.
@@ -236,7 +236,7 @@ Enabling arbitrary exec sources allows execution of arbitrary commands through g
## Advanced Restream Configurations
-The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#source-exec) source in go2rtc can be used for custom ffmpeg commands. An example is below:
+The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.9.14#source-exec) source in go2rtc can be used for custom ffmpeg commands and other applications. An example is below:
:::warning
@@ -244,16 +244,11 @@ The `exec:`, `echo:`, and `expr:` sources are disabled by default for security.
:::
-:::warning
-
-The `exec:`, `echo:`, and `expr:` sources are disabled by default for security. You must set `GO2RTC_ALLOW_ARBITRARY_EXEC=true` to use them. See [Security: Restricted Stream Sources](#security-restricted-stream-sources) for more information.
-
-:::
-
-NOTE: The output will need to be passed with two curly braces `{{output}}`
+NOTE: RTSP output will need to be passed with two curly braces `{{output}}`, whereas pipe output must be passed without curly braces.
```yaml
go2rtc:
streams:
stream1: exec:ffmpeg -hide_banner -re -stream_loop -1 -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {{output}}
+ stream2: exec:rpicam-vid -t 0 --libav-format h264 -o -
```
diff --git a/docs/docs/configuration/review.md b/docs/docs/configuration/review.md
index 4f39611dbe..e9ba4df254 100644
--- a/docs/docs/configuration/review.md
+++ b/docs/docs/configuration/review.md
@@ -23,7 +23,7 @@ In 0.14 and later, all of that is bundled into a single review item which starts
## Alerts and Detections
-Not every segment of video captured by Frigate may be of the same level of interest to you. Video of people who enter your property may be a different priority than those walking by on the sidewalk. For this reason, Frigate 0.14 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.
+Not every segment of video captured by Frigate may be of the same level of interest to you. Video of people who enter your property may be a different priority than those walking by on the sidewalk. For this reason, Frigate categorizes review items as _alerts_ and _detections_. By default, all person and car objects are considered alerts. You can refine categorization of your review items by configuring [required zones](/configuration/zones#restricting-alerts-and-detections-to-specific-zones) for them.
:::note
@@ -130,3 +130,7 @@ By default a review item will be created if any `review -> alerts -> labels` and
Because zones don't apply to audio, audio labels will always be marked as a detection by default.
:::
+
+## Reviewing Motion
+
+The Review page can also surface periods of motion that didn't produce a tracked object, and lets you search past recordings for motion in a region you draw. See [Reviewing Motion](/usage/review#reviewing-motion) in the Usage docs for how to use **Motion Previews** and **Motion Search**, and [Tuning Motion Detection](motion_detection.md) for configuring the underlying motion detector.
diff --git a/docs/docs/configuration/semantic_search.md b/docs/docs/configuration/semantic_search.md
index b2c5d16395..c58b634fd8 100644
--- a/docs/docs/configuration/semantic_search.md
+++ b/docs/docs/configuration/semantic_search.md
@@ -7,7 +7,7 @@ import ConfigTabs from "@site/src/components/ConfigTabs";
import TabItem from "@theme/TabItem";
import NavPath from "@site/src/components/NavPath";
-Semantic Search in Frigate allows you to find tracked objects within your review items using either the image itself, a user-defined text description, or an automatically generated one. This feature works by creating _embeddings_ — numerical vector representations — for both the images and text descriptions of your tracked objects. By comparing these embeddings, Frigate assesses their similarities to deliver relevant search results.
+Semantic Search in Frigate allows you to find tracked objects within your review items using either the image itself, a user-defined text description, or an automatically generated one. This feature works by creating _embeddings_, numerical vector representations, for both the images and text descriptions of your tracked objects. By comparing these embeddings, Frigate assesses their similarities to deliver relevant search results.
Frigate uses models from [Jina AI](https://huggingface.co/jinaai) to create and save embeddings to Frigate's database. All of this runs locally.
@@ -222,16 +222,11 @@ See the [Hardware Accelerated Enrichments](/configuration/hardware_acceleration_
## Usage and Best Practices
-1. Semantic Search is used in conjunction with the other filters available on the Explore page. Use a combination of traditional filtering and Semantic Search for the best results.
-2. Use the thumbnail search type when searching for particular objects in the scene. Use the description search type when attempting to discern the intent of your object.
-3. Because of how the AI models Frigate uses have been trained, the comparison between text and image embedding distances generally means that with multi-modal (`thumbnail` and `description`) searches, results matching `description` will appear first, even if a `thumbnail` embedding may be a better match. Play with the "Search Type" setting to help find what you are looking for. Note that if you are generating descriptions for specific objects or zones only, this may cause search results to prioritize the objects with descriptions even if the the ones without them are more relevant.
-4. Make your search language and tone closely match exactly what you're looking for. If you are using thumbnail search, **phrase your query as an image caption**. Searching for "red car" may not work as well as "red sedan driving down a residential street on a sunny day".
-5. Semantic search on thumbnails tends to return better results when matching large subjects that take up most of the frame. Small things like "cat" tend to not work well.
-6. Experiment! Find a tracked object you want to test and start typing keywords and phrases to see what works for you.
+For tips on getting the best results from Semantic Search (choosing between thumbnail and description search, phrasing queries effectively, and combining search with the other Explore filters), see [Usage and best practices](/usage/explore#usage-and-best-practices) in the Usage docs.
## Triggers
-Triggers utilize Semantic Search to automate actions when a tracked object matches a specified image or description. Triggers can be configured so that Frigate executes a specific actions when a tracked object's image or description matches a predefined image or text, based on a similarity threshold. Triggers are managed per camera and can be configured via the Frigate UI in the Settings page under the Triggers tab.
+Triggers utilize Semantic Search to automate actions when a tracked object matches a specified image or description. Triggers can be configured so that Frigate executes specific actions when a tracked object's image or description matches a predefined image or text, based on a similarity threshold. Triggers are managed per camera and can be configured via the Frigate UI in the Settings page under the Triggers tab.
:::note
diff --git a/docs/docs/configuration/snapshots.md b/docs/docs/configuration/snapshots.md
index 675e68a9ca..b2aba9463f 100644
--- a/docs/docs/configuration/snapshots.md
+++ b/docs/docs/configuration/snapshots.md
@@ -7,13 +7,17 @@ import ConfigTabs from "@site/src/components/ConfigTabs";
import TabItem from "@theme/TabItem";
import NavPath from "@site/src/components/NavPath";
-Frigate can save a snapshot image to `/media/frigate/clips` for each object that is detected named as `--clean.webp`. They are also accessible [via the api](../integrations/api/event-snapshot-events-event-id-snapshot-jpg-get.api.mdx)
+A snapshot is a single still image that captures a tracked object at its best moment: the clearest frame Frigate saw while following that object across the scene. Unlike a [recording](./record.md), which is continuous video, a snapshot is one representative image saved per tracked object once tracking ends.
-Snapshots are accessible in the UI in the Explore pane. This allows for quick submission to the Frigate+ service.
+When snapshots are enabled, Frigate saves one image to `/media/frigate/clips` for each tracked object, named `--clean.webp`. A clean image is always stored without any annotations (no timestamp, bounding boxes, or cropping) so you have an unmodified copy of the original frame. Annotations like bounding boxes and timestamps are applied on demand when a snapshot is requested [via the HTTP API](../integrations/api/event-snapshot-events-event-id-snapshot-jpg-get.api.mdx). See [Rendering](#rendering) below.
-To only save snapshots for objects that enter a specific zone, [see the zone docs](./zones.md#restricting-snapshots-to-specific-zones)
+A few things to keep in mind:
-Snapshots sent via MQTT are configured separately under the camera MQTT settings, not here.
+- Snapshots are saved per tracked object, so a camera with no detected objects produces no snapshots even if recording is enabled.
+- Snapshots and recordings are configured and retained independently. Enabling one does not enable the other.
+- Snapshots are accessible in the UI in the Explore pane, which allows for quick submission to the Frigate+ service.
+- To only save snapshots for objects that enter a specific zone, [see the zone docs](./zones.md#restricting-snapshots-to-specific-zones).
+- Snapshots sent via MQTT are configured separately under the camera MQTT settings, not here.
## Enabling Snapshots
@@ -107,7 +111,6 @@ Navigate to .
| Field | Description |
| -------------------------------------------------- | ----------------------------------------------------------------------------------- |
| **Snapshot retention > Default retention** | Number of days to retain snapshots (default: 10) |
-| **Snapshot retention > Retention mode** | Retention mode: `all`, `motion`, or `active_objects` |
| **Snapshot retention > Object retention > Person** | Per-object overrides for retention days (e.g., keep `person` snapshots for 15 days) |
@@ -118,7 +121,6 @@ snapshots:
enabled: True
retain:
default: 10
- mode: motion
objects:
person: 15
```
@@ -130,7 +132,7 @@ snapshots:
Frigate does not save every frame. It picks a single "best" frame for each tracked object based on detection confidence, object size, and the presence of key attributes like faces or license plates. Frames where the object touches the edge of the frame are deprioritized. That best frame is written to disk once tracking ends.
-MQTT snapshots are published more frequently — each time a better thumbnail frame is found during tracking, or when the current best image is older than `best_image_timeout` (default: 60s). These use their own annotation settings configured under the camera MQTT settings.
+MQTT snapshots are published more frequently: each time a better thumbnail frame is found during tracking, or when the current best image is older than `best_image_timeout` (default: 60s). These use their own annotation settings configured under the camera MQTT settings.
## Rendering
diff --git a/docs/docs/configuration/stationary_objects.md b/docs/docs/configuration/stationary_objects.md
index 63d03374c8..40dda40e07 100644
--- a/docs/docs/configuration/stationary_objects.md
+++ b/docs/docs/configuration/stationary_objects.md
@@ -43,7 +43,7 @@ Let's look at an example use case: I want to record any cars that enter my drive
One might simply think "Why not just run object detection any time there is motion around the driveway area and notify if the bounding box is in that zone?"
-With that approach, what video is related to the car that entered the driveway? Did it come from the left or right? Was it parked across the street for an hour before turning into the driveway? One approach is to just record 24/7 or for motion (on any changed changed pixels) and not attempt to do that at all. This is what most other NVRs do. Just don't even try to identify a start and end for that object since it's hard and you will be wrong some portion of the time.
+With that approach, what video is related to the car that entered the driveway? Did it come from the left or right? Was it parked across the street for an hour before turning into the driveway? One approach is to just record 24/7 or for motion (on any changed pixels) and not attempt to do that at all. This is what most other NVRs do. Just don't even try to identify a start and end for that object since it's hard and you will be wrong some portion of the time.
Couldn't you just look at when motion stopped and started? Motion for a video feed is nothing more than looking for pixels that are different than they were in previous frames. If the car entered the driveway while someone was mowing the grass, how would you know which motion was for the car and which was for the person when they mow along the driveway or street? What if another car was driving the other direction on the street? Or what if its a windy day and the bush by your mailbox is blowing around?
@@ -61,4 +61,4 @@ Now you have to determine which of the bounding boxes in this frame should be ma
Now let's assume that those other 3 cars were already being tracked as stationary objects, so the car driving down the street is a new 4th car. The object tracker knows we have had 3 cars and we now have 4. As the new car approaches the parked cars, the bounding boxes for all 4 cars is predicted based on the previous frames. The predicted boxes for the parked cars is pretty much a 100% overlap with the bounding boxes in the new frame. The parked cars are slam dunk matches to the tracking ids they had before and the only one left is the remaining bounding box which gets assigned to the new car. This results in a much lower error rate. Not perfect, but better.
-The most difficult scenario that causes IDs to be assigned incorrectly is when an object completely occludes another object. When a car drives in front of another car and its no longer visible, a bounding box disappeared and it's a bit of a toss up when assigning the id since it's difficult to know which one is in front of the other. This happens for cars passing in front of other cars fairly often. It's something that we want to improve in the future.
+The most difficult scenario that causes IDs to be assigned incorrectly is when an object completely occludes another object. When a car drives in front of another car and it's no longer visible, a bounding box disappeared and it's a bit of a toss up when assigning the id since it's difficult to know which one is in front of the other. This happens for cars passing in front of other cars fairly often. It's something that we want to improve in the future.
diff --git a/docs/docs/configuration/zones.md b/docs/docs/configuration/zones.md
index 2cb3c8ebeb..2a26453203 100644
--- a/docs/docs/configuration/zones.md
+++ b/docs/docs/configuration/zones.md
@@ -18,7 +18,7 @@ Zones cannot have the same name as a camera. If desired, a single zone can inclu
Zones can be toggled on or off without removing them from the configuration. Disabled zones are completely ignored at runtime - objects will not be tracked for zone presence, and zones will not appear in the debug view. This is useful for temporarily disabling a zone during certain seasons or times of day without modifying the configuration.
-During testing, enable the Zones option for the Debug view of your camera (Settings --> Debug) so you can adjust as needed. The zone line will increase in thickness when any object enters the zone.
+During testing, enable the Zones option for the [Debug view](/usage/live#the-single-camera-view) of your camera so you can adjust as needed. The zone line will increase in thickness when any object enters the zone.
## Creating a Zone
@@ -61,7 +61,7 @@ Navigate to .
| Field | Description |
| ---------------------------------- | ----------------------------------------------------------------------------------------- |
-| **Alerts config > Required zones** | Zones that an object must enter to be considered an alert; leave empty to allow any zone. |
+| **Alerts config > Required zones** | Set to `entire_yard` so an object must enter that zone to be considered an alert; leave empty to allow alerts anywhere in the frame. |
@@ -82,7 +82,7 @@ cameras:
-You may also want to filter detections to only be created when an object enters a secondary area of interest. For example, to trigger alerts when an object enters the inner area of the yard but detections when an object enters the edge of the yard:
+You may also want to filter detections to only be created when an object enters a secondary area of interest. For example, to trigger alerts when an object enters the inner area of the yard (an `inner_yard` zone) but detections when an object enters the edge of the yard (an `edge_yard` zone):
@@ -91,8 +91,8 @@ Navigate to .
| Field | Description |
| -------------------------------------- | -------------------------------------------------------------------------------------------- |
-| **Alerts config > Required zones** | Zones that an object must enter to be considered an alert; leave empty to allow any zone. |
-| **Detections config > Required zones** | Zones that an object must enter to be considered a detection; leave empty to allow any zone. |
+| **Alerts config > Required zones** | Set to `inner_yard` so an object must enter that zone to be considered an alert; leave empty to allow alerts anywhere in the frame. |
+| **Detections config > Required zones** | Set to `edge_yard` so an object must enter that zone to be considered a detection; leave empty to allow detections anywhere in the frame. |
@@ -121,7 +121,7 @@ cameras:
### Restricting snapshots to specific zones
-To only save snapshots when an object enters a specific zone:
+To only save snapshots when an object enters a specific zone, for example an `entire_yard` zone:
diff --git a/docs/docs/frigate/camera_setup.md b/docs/docs/frigate/camera_setup.md
index 4cb56dc508..0299a9aab8 100644
--- a/docs/docs/frigate/camera_setup.md
+++ b/docs/docs/frigate/camera_setup.md
@@ -5,7 +5,7 @@ title: Camera setup
Cameras configured to output H.264 video and AAC audio will offer the most compatibility with all features of Frigate and Home Assistant. H.265 has better compression, but less compatibility. Firefox 134+/136+/137+ (Windows/Mac/Linux & Android), Chrome 108+, Safari and Edge are the only browsers able to play H.265 and only support a limited number of H.265 profiles. Ideally, cameras should be configured directly for the desired resolutions and frame rates you want to use in Frigate. Reducing frame rates within Frigate will waste CPU resources decoding extra frames that are discarded. There are three different goals that you want to tune your stream configurations around.
-- **Detection**: This is the only stream that Frigate will decode for processing. Also, this is the stream where snapshots will be generated from. The resolution for detection should be tuned for the size of the objects you want to detect. See [Choosing a detect resolution](#choosing-a-detect-resolution) for more details. The recommended frame rate is 5fps, but may need to be higher (10fps is the recommended maximum for most users) for very fast moving objects. Higher resolutions and frame rates will drive higher CPU usage on your server.
+- **Detection**: This is the only stream that Frigate will decode for processing. Also, this is the stream where snapshots will be generated from. The resolution for detection should be tuned for the size of the objects you want to detect. See [Choosing a detect resolution](#choosing-a-detect-resolution) for more details. The default frame rate of 5fps is correct for almost all cameras and rarely needs to be changed; see [Choosing a detect frame rate](#choosing-a-detect-frame-rate). Higher resolutions and frame rates will drive higher CPU usage on your server.
- **Recording**: This stream should be the resolution you wish to store for reference. Typically, this will be the highest resolution your camera supports. I recommend setting this feed in your camera's firmware to 15 fps.
@@ -25,6 +25,44 @@ Larger resolutions **do** improve performance if the objects are very small in t

+### Choosing a detect frame rate
+
+`detect.fps` controls how many times per second Frigate runs object detection. It does **not** need to match your camera's frame rate. The default of **5** is correct for the vast majority of cameras.
+
+:::warning
+
+Most users who raise `detect.fps` above the default don't need to. Increasing it consumes more CPU/GPU (detection load scales directly with the frame rate) while providing **no benefit to tracking** once objects are already being followed smoothly. Leave it at **5** unless you have a specific scene that fails the test below, and confirm any change actually helps in the [debug view](/usage/live#the-single-camera-view).
+
+:::
+
+#### Why 5 is enough for almost everyone
+
+Frigate follows an object by matching its bounding box from one detection frame to the next, which requires the object to be detected often enough while it is on screen. At 5 fps this is satisfied in normal scenes: an object crossing a yard, porch, driveway, or walkway is in view for several seconds and produces ~15 or more detections, which is more than enough for a reliable track and a good snapshot. This includes fast subjects such as a running person or a bolting pet, which on a wide-angle view remain on screen for several seconds.
+
+A higher rate helps only when an object crosses the **entire frame in less than two seconds**, which is determined by camera framing rather than object speed - for example, a camera aimed down a street at fast cross-traffic. In those scenes 5 fps may produce too few detections to hold a track. Cameras covering normal approaches and open areas are unaffected.
+
+#### Checking whether a higher rate is needed
+
+Estimate how long an object is visible as it crosses the area of interest, aiming for roughly 8–10 detections during the pass:
+
+> **`detect.fps` ≈ 10 ÷ (seconds the object is in view)**
+
+Most objects (people walking or running, pets, and vehicles in a yard, driveway, or walkway) stay in view for two seconds or more, so the default of 5 fps is correct. Slowly try raising it to 10 (the recommended maximum) in increments only when objects routinely cross the entire frame in about a second, such as a camera aimed at a street or sidewalk with fast cross-traffic. Objects that transit in under a second cannot be tracked reliably at any practical rate, so reposition the camera instead.
+
+:::tip
+
+If the formula calls for more than 10, the fix is **camera placement, not frame rate**. Angle the camera so objects move toward it rather than across the view, or aim it where traffic slows. A higher `detect.fps` increases CPU load proportionally without producing more detections of a too-brief object.
+
+:::
+
+#### Verify in the debug view
+
+Confirm any change in the Debug view or Debug Replay. Watch a typical object cross the scene: if its bounding box follows it smoothly while visible, the rate is sufficient. A box that jumps erratically, drops out, or splits one object into multiple events indicates the rate should be increased one step.
+
+#### Dedicated LPR cameras
+
+A dedicated license plate recognition camera is the most common reason to use something higher than 5 fps: the camera is highly zoomed, the plate is small, and it moves at full vehicle speed, so it transits the frame quickly. However, the same ceiling applies: above 10 fps is unnecessary, and **placement matters most**: aim LPR cameras where vehicles slow down, such as gates, driveways, and parking entrances. A tight view of a fast through-road will not likely read plates reliably at any frame rate. See [License Plate Recognition](/configuration/license_plate_recognition) for details.
+
### Example Camera Configuration
For the Dahua/Loryta 5442 camera, I use the following settings:
diff --git a/docs/docs/frigate/glossary.md b/docs/docs/frigate/glossary.md
index 5bfbfafa5a..d550015670 100644
--- a/docs/docs/frigate/glossary.md
+++ b/docs/docs/frigate/glossary.md
@@ -5,20 +5,40 @@ title: Glossary
The glossary explains terms commonly used in Frigate's documentation.
+## Alert
+
+The higher-priority of the two [review item](#review-item) severities, the other being a [detection](#detection). By default a review item is an alert when it involves a `person` or `car`; the qualifying [labels](#label) and [zones](#zone) can be configured. [See the review docs for more info](/configuration/review)
+
+## Attribute
+
+A property detected on an [object](#object) that exists alongside its [label](#label). Unlike a [sub label](#sub-label), an object can carry several attributes at once. Some attributes come directly from the object detection [model](#model) (for example `face`, `license_plate`, or delivery carrier logos such as `amazon`, `ups`, and `fedex`), while others come from a [custom object classification model](/configuration/custom_classification/object_classification) configured with the `attribute` type. Attributes are visible in the Tracked Object Details pane in Explore, in `frigate/events` MQTT messages, and through the HTTP API.
+
## Bounding Box
-A box returned from the object detection model that outlines an object in the frame. These have multiple colors depending on object type in the debug live view.
+A box returned by the object detection [model](#model) that outlines a detected [object](#object) in the frame. In the [Debug view](/usage/live#the-single-camera-view), bounding boxes are colored by object [label](#label).
### Bounding Box Colors
- At startup different colors will be assigned to each object label
- A dark blue thin line indicates that object is not detected at this current point in time
- A gray thin line indicates that object is detected as being stationary
-- A thick line indicates that object is the subject of autotracking (when enabled).
+- A thick line indicates that object is the subject of autotracking (when enabled)
+
+## Class
+
+The categories a classification [model](#model) is trained to distinguish between. Each class is a distinct visual category the model predicts, plus a `none` class for inputs that don't fit any category. For example, a custom object classification model for `person` objects might use the classes `delivery_person`, `resident`, and `none`. The predicted class is applied to the [object](#object) as either a [sub label](#sub-label) or an [attribute](#attribute), depending on the model's configuration. [See the object classification docs for more info](/configuration/custom_classification/object_classification)
+
+## Detection
+
+The lower-priority of the two [review item](#review-item) severities, the other being an [alert](#alert). By default, any review item that does not qualify as an alert is a detection; the qualifying [labels](#label) and [zones](#zone) can be configured. Despite the name, a detection is a category of review item, not the same as the object detection performed by the [model](#model). [See the review docs for more info](/configuration/review)
## False Positive
-An incorrect detection of an object type. For example a dog being detected as a person, a chair being detected as a dog, etc. A person being detected in an area you want to ignore is not a false positive.
+An incorrect result from the object detection [model](#model), where it assigns the wrong [label](#label) to something in the frame, for example a dog identified as a person, or a chair identified as a dog. A person correctly identified in an area you want to ignore is not a false positive.
+
+## Label
+
+The type assigned to a detected [object](#object) by the object detection [model](#model), drawn from the model's labelmap, for example `person`, `car`, or `dog`. Frigate tracks `person` by default; additional labels are tracked by adding them to the objects configuration. [See the available objects docs for the full list](/configuration/objects)
## Mask
@@ -26,44 +46,56 @@ There are two types of masks in Frigate. [See the mask docs for more info](/conf
### Motion Mask
-Motion masks prevent detection of [motion](#motion) in masked areas from triggering Frigate to run object detection, but do not prevent objects from being detected if object detection runs due to motion in nearby areas. For example: camera timestamps, skies, the tops of trees, etc.
+A motion mask stops [motion](#motion) in the masked area from triggering object detection. It does not stop an object from being detected when object detection runs because of motion in a nearby area. Use motion masks for parts of the frame that change constantly but never contain objects you care about: camera timestamps, the sky, the tops of trees, and so on.
### Object Mask
-Object filter masks drop any bounding boxes where the bottom center (overlap doesn't matter) is in the masked area. It forces them to be considered a [false positive](#false-positive) so that they are ignored.
+An object filter mask drops any [bounding box](#bounding-box) whose bottom center falls inside the masked area (overlap elsewhere doesn't matter). The object is forced to be treated as a [false positive](#false-positive) and ignored.
## Min Score
-The lowest score that an object can be detected with during tracking, any detection with a lower score will be assumed to be a false positive
+The lowest score a detected object can have to be kept during tracking. Anything scoring below the minimum is assumed to be a [false positive](#false-positive) and discarded.
+
+## Model
+
+A machine learning model that Frigate uses to detect or classify objects. The object detection model locates [objects](#object) in each frame and returns their [labels](#label) and [bounding boxes](#bounding-box). Additional enrichment models run on tracked objects to add detail: face recognition, license plate recognition, bird classification, custom object and state classification, and the embedding models used for semantic search. [See the object detectors docs for more info](/configuration/object_detectors)
## Motion
-When pixels in the current camera frame are different than previous frames. When many nearby pixels are different in the current frame they grouped together and indicated with a red motion box in the live debug view. [See the motion detection docs for more info](/configuration/motion_detection)
+A change in pixels between the current camera frame and previous frames. When many nearby pixels change together, they are grouped and shown as a red motion box in the debug live view. [See the motion detection docs for more info](/configuration/motion_detection)
+
+## Object
+
+Something Frigate can detect and follow in a camera frame, identified by its [label](#label) (for example a person or a car). The object types Frigate watches for are set in the `objects` configuration. Once an object is detected and followed across frames it becomes a [tracked object](#tracked-object-event-in-previous-versions), which may also carry a [sub label](#sub-label) and [attributes](#attribute). [See the available objects docs for more info](/configuration/objects)
## Region
-A portion of the camera frame that is sent to object detection, regions can be sent due to motion, active objects, or occasionally for stationary objects. These are represented by green boxes in the debug live view.
+A portion of the camera frame sent to the object detection [model](#model). Regions are selected because of [motion](#motion), active objects, or occasionally to recheck stationary objects, and are shown as green boxes in the debug live view.
## Review Item
-A review item is a time period where any number of events/tracked objects were active. [See the review docs for more info](/configuration/review)
+A period of time during which one or more [tracked objects](#tracked-object-event-in-previous-versions) were active, grouped together for review. Each review item is categorized as either an [alert](#alert) or a [detection](#detection). [See the review docs for more info](/configuration/review)
## Snapshot Score
-The score shown in a snapshot is the score of that object at that specific moment in time.
+The object's score at the specific moment the snapshot was captured.
+
+## Sub Label
+
+A more specific identity assigned to a [tracked object](#tracked-object-event-in-previous-versions) in addition to its [label](#label). A `person` may get the name of a recognized face, a `car` may get the name of a known license plate, and a `bird` may get its species. An object can have only one sub label at a time. Sub labels are produced by face recognition, license plate recognition, bird classification, custom object classification configured with the `sub label` type, and semantic search triggers.
## Threshold
-The threshold is the median score that an object must reach in order to be considered a true positive.
+The median score an object must reach to be considered a true positive.
## Top Score
-The top score for an object is the highest median score for an object.
+The highest median score an object reached over its lifetime.
## Tracked Object ("event" in previous versions)
-The time period starting when a tracked object entered the frame and ending when it left the frame, including any time that the object remained still. Tracked objects are saved when it is considered a [true positive](#threshold) and meets the requirements for a snapshot or recording to be saved.
+An [object](#object) followed from the moment it enters the frame until it leaves, including any time it stays still. A tracked object is saved once it is considered a [true positive](#threshold) and meets the requirements for a snapshot or recording.
## Zone
-Zones are areas of interest, zones can be used for notifications and for limiting the areas where Frigate will create a [review item](#review-item). [See the zone docs for more info](/configuration/zones)
+A user-defined area of interest within the camera frame. Zones can be used for notifications and to limit where Frigate creates a [review item](#review-item). [See the zone docs for more info](/configuration/zones)
diff --git a/docs/docs/frigate/hardware.md b/docs/docs/frigate/hardware.md
index 7df2ae0bb5..1df3bb8368 100644
--- a/docs/docs/frigate/hardware.md
+++ b/docs/docs/frigate/hardware.md
@@ -55,7 +55,7 @@ Frigate supports multiple different detectors that work on different types of ha
**Most Hardware**
- [Hailo](#hailo-8): The Hailo8 and Hailo8L AI Acceleration module is available in m.2 format with a HAT for RPi devices offering a wide range of compatibility with devices.
- - [Supports many model architectures](../../configuration/object_detectors#configuration)
+ - [Supports many model architectures](../../configuration/object_detectors#configuration-hailo)
- Runs best with tiny or small size models
- [Google Coral EdgeTPU](#google-coral-tpu): The Google Coral EdgeTPU is available in USB and m.2 format allowing for a wide range of compatibility with devices.
@@ -68,26 +68,26 @@ Frigate supports multiple different detectors that work on different types of ha
**AMD**
- [ROCm](#rocm---amd-gpu): ROCm can run on AMD Discrete GPUs to provide efficient object detection
- - [Supports limited model architectures](../../configuration/object_detectors#rocm-supported-models)
+ - [Supports limited model architectures](../../configuration/object_detectors#amdrocm-gpu-detector)
- Runs best on discrete AMD GPUs
**Apple Silicon**
- [Apple Silicon](#apple-silicon): Apple Silicon is usable on all M1 and newer Apple Silicon devices to provide efficient and fast object detection
- - [Supports primarily ssdlite and mobilenet model architectures](../../configuration/object_detectors#apple-silicon-supported-models)
+ - [Supports primarily ssdlite and mobilenet model architectures](../../configuration/object_detectors#apple-silicon-detector)
- Runs well with any size models including large
- Runs via ZMQ proxy which adds some latency, only recommended for local connection
**Intel**
- [OpenVino](#openvino---intel): OpenVino can run on Intel Arc GPUs, Intel integrated GPUs, and Intel NPUs to provide efficient object detection.
- - [Supports majority of model architectures](../../configuration/object_detectors#openvino-supported-models)
+ - [Supports majority of model architectures](../../configuration/object_detectors#openvino-detector)
- Runs best with tiny, small, or medium models
**Nvidia**
- [Nvidia GPU](#nvidia-gpus): Nvidia GPUs can provide efficient object detection.
- - [Supports majority of model architectures via ONNX](../../configuration/object_detectors#onnx-supported-models)
+ - [Supports majority of model architectures via ONNX](../../configuration/object_detectors#onnx)
- Runs well with any size models including large
- [Jetson](#nvidia-jetson): Jetson devices are supported via the TensorRT or ONNX detectors when running Jetpack 6.
@@ -111,14 +111,14 @@ Frigate supports multiple different detectors that work on different types of ha
### Hailo-8
-Frigate supports both the Hailo-8 and Hailo-8L AI Acceleration Modules on compatible hardware platforms—including the Raspberry Pi 5 with the PCIe hat from the AI kit. The Hailo detector integration in Frigate automatically identifies your hardware type and selects the appropriate default model when a custom model isn’t provided.
+Frigate supports both the Hailo-8 and Hailo-8L AI Acceleration Modules on compatible hardware platforms, including the Raspberry Pi 5 with the PCIe hat from the AI kit. The Hailo detector integration in Frigate automatically identifies your hardware type and selects the appropriate default model when a custom model isn’t provided.
**Default Model Configuration:**
- **Hailo-8L:** Default model is **YOLOv6n**.
- **Hailo-8:** Default model is **YOLOv6n**.
-In real-world deployments, even with multiple cameras running concurrently, Frigate has demonstrated consistent performance. Testing on x86 platforms—with dual PCIe lanes—yields further improvements in FPS, throughput, and latency compared to the Raspberry Pi setup.
+In real-world deployments, even with multiple cameras running concurrently, Frigate has demonstrated consistent performance. Testing on x86 platforms, with dual PCIe lanes, yields further improvements in FPS, throughput, and latency compared to the Raspberry Pi setup.
| Name | Hailo‑8 Inference Time | Hailo‑8L Inference Time |
| ---------------- | ---------------------- | ----------------------- |
@@ -223,10 +223,11 @@ Apple Silicon can not run within a container, so a ZMQ proxy is utilized to comm
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 | t-320: ~ 14 ms s-320: 20 ms | 320: ~ 25 ms 640: ~ 50 ms |
-| AMD 8700G | | 320: ~ 20 ms 640: ~ 40 ms |
+| Name | YOLOv9 Inference Time | YOLO-NAS Inference Time | RF-DETR Inference Time |
+| -------------- | --------------------------- | ------------------------- | ---------------------- |
+| AMD 780M | t-320: ~ 14 ms s-320: 20 ms | 320: ~ 25 ms 640: ~ 50 ms | |
+| AMD 8700G | | 320: ~ 20 ms 640: ~ 40 ms | |
+| AMD 9060XT 16G | t-320: ~ 4 ms s-320: 5 ms | 320: ~ 6 ms | Nano-320: ~ 90 ms |
## Community Supported Detectors
diff --git a/docs/docs/frigate/installation.md b/docs/docs/frigate/installation.md
index 5d228a609a..5cdbc13627 100644
--- a/docs/docs/frigate/installation.md
+++ b/docs/docs/frigate/installation.md
@@ -4,12 +4,15 @@ title: Installation
---
import ShmCalculator from '@site/src/components/ShmCalculator'
+import DockerComposeGenerator from '@site/src/components/DockerComposeGenerator'
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
Frigate is a Docker container that can be run on any Docker host including as a [Home Assistant App](https://www.home-assistant.io/apps/). Note that the Home Assistant App is **not** the same thing as the integration. The [integration](/integrations/home-assistant) is required to integrate Frigate into Home Assistant, whether you are running Frigate as a standalone Docker container or as a Home Assistant App.
:::tip
-If you already have Frigate installed as a Home Assistant App, check out the [getting started guide](../guides/getting_started#configuring-frigate) to configure Frigate.
+If you already have Frigate installed as a Home Assistant App, check out the [getting started guide](../guides/getting_started.md#configuring-frigate) to configure Frigate.
:::
@@ -91,7 +94,7 @@ The following sections contain additional setup steps that are only required if
By default, the Raspberry Pi limits the amount of memory available to the GPU. In order to use ffmpeg hardware acceleration, you must increase the available memory by setting `gpu_mem` to the maximum recommended value in `config.txt` as described in the [official docs](https://www.raspberrypi.org/documentation/computers/config_txt.html#memory-options).
-Additionally, the USB Coral draws a considerable amount of power. If using any other USB devices such as an SSD, you will experience instability due to the Pi not providing enough power to USB devices. You will need to purchase an external USB hub with it's own power supply. Some have reported success with this (affiliate link).
+Additionally, the USB Coral draws a considerable amount of power. If using any other USB devices such as an SSD, you will experience instability due to the Pi not providing enough power to USB devices. You will need to purchase an external USB hub with its own power supply. Some have reported success with this (affiliate link).
### Hailo-8
@@ -286,7 +289,7 @@ The MemryX MX3 Accelerator is available in the M.2 2280 form factor (like an NVM
#### 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).
+To get started with MX3 hardware setup for your system, refer to the [Hardware Setup Guide](https://developer.memryx.com/2p1/get_started/install_hardware.html).
Then follow these steps for installing the correct driver/runtime configuration:
@@ -295,6 +298,12 @@ Then follow these steps for installing the correct driver/runtime configuration:
3. Run the script with `./user_installation.sh`
4. **Restart your computer** to complete driver installation.
+:::warning
+
+For manual setup, use **MemryX SDK 2.1** only. Other SDK versions are not supported for this setup. See the [SDK 2.1 documentation](https://developer.memryx.com/2p1/index.html)
+
+:::
+
#### Setup
To set up Frigate, follow the default installation instructions, for example: `ghcr.io/blakeblackshear/frigate:stable`
@@ -468,6 +477,16 @@ Finally, configure [hardware object detection](/configuration/object_detectors#a
Running through Docker with Docker Compose is the recommended install method.
+
+
+
+Generate a Frigate Docker Compose configuration based on your hardware and requirements.
+
+
+
+
+
+
```yaml
services:
frigate:
@@ -501,6 +520,10 @@ services:
environment:
FRIGATE_RTSP_PASSWORD: "password"
```
+
+
+
+**Docker CLI**
If you can't use Docker Compose, you can run the container with something similar to this:
@@ -577,7 +600,7 @@ There are several variants of the App available:
If you are using hardware acceleration for ffmpeg, you **may** need to use the _Full Access_ variant of the App. This is because the Frigate App runs in a container with limited access to the host system. The _Full Access_ variant allows you to disable _Protection mode_ and give Frigate full access to the host system.
-You can also edit the Frigate configuration file through the [VS Code App](https://github.com/hassio-addons/addon-vscode) or similar. In that case, the configuration file will be at `/addon_configs//config.yml`, where `` is specific to the variant of the Frigate App you are running. See the list of directories [here](../configuration/index.md#accessing-app-config-dir).
+You can also edit the Frigate configuration file through the [VS Code App](https://github.com/hassio-addons/addon-vscode) or similar. In that case, the configuration file will be at `/addon_configs//config.yml`, where `` is specific to the variant of the Frigate App you are running. See the list of directories [here](../configuration/config.md#accessing-app-config-dir).
## Kubernetes
@@ -726,7 +749,7 @@ Failure to remap port 5000 on the host will result in the WebUI and all API endp
:::
-Docker containers on macOS can be orchestrated by either [Docker Desktop](https://docs.docker.com/desktop/setup/install/mac-install/) or [OrbStack](https://orbstack.dev) (native swift app). The difference in inference speeds is negligable, however CPU, power consumption and container start times will be lower on OrbStack because it is a native Swift application.
+Docker containers on macOS can be orchestrated by either [Docker Desktop](https://docs.docker.com/desktop/setup/install/mac-install/) or [OrbStack](https://orbstack.dev) (native Swift app). The difference in inference speeds is negligible, however CPU, power consumption and container start times will be lower on OrbStack because it is a native Swift application.
To allow Frigate to use the Apple Silicon Neural Engine / Processing Unit (NPU) the host must be running [Apple Silicon Detector](../configuration/object_detectors.md#apple-silicon-detector) on the host (outside Docker)
@@ -745,7 +768,7 @@ services:
- /path/to/your/recordings:/recordings
ports:
- "8971:8971"
- # If exposing on macOS map to a diffent host port like 5001 or any orher port with no conflicts
+ # If exposing on macOS map to a different host port like 5001 or any other port with no conflicts
# - "5001:5000" # Internal unauthenticated access. Expose carefully.
- "8554:8554" # RTSP feeds
extra_hosts:
diff --git a/docs/docs/frigate/network_requirements.md b/docs/docs/frigate/network_requirements.md
index 49d64272e4..0c28f8389c 100644
--- a/docs/docs/frigate/network_requirements.md
+++ b/docs/docs/frigate/network_requirements.md
@@ -11,9 +11,9 @@ Frigate is designed to run locally and does not require a persistent internet co
Frigate's internet usage falls into three categories:
-1. **One-time model downloads** — ML models are downloaded the first time a feature is enabled, then cached locally. No internet is needed on subsequent startups.
-2. **Optional cloud services** — Features like Frigate+ and Generative AI connect to external APIs only when explicitly configured.
-3. **Build-time dependencies** — Components bundled into the Docker image during the build process. These require no internet at runtime.
+1. **One-time model downloads**: ML models are downloaded the first time a feature is enabled, then cached locally. No internet is needed on subsequent startups.
+2. **Optional cloud services**: Features like Frigate+ and Generative AI connect to external APIs only when explicitly configured.
+3. **Build-time dependencies**: Components bundled into the Docker image during the build process. These require no internet at runtime.
:::tip
@@ -32,7 +32,7 @@ The following models are downloaded automatically the first time their associate
| [License plate recognition](/configuration/license_plate_recognition) | PaddleOCR (detection, classification, recognition) + YOLOv9 plate detector | GitHub |
| [Bird classification](/configuration/bird_classification) | MobileNetV2 bird model + label map | GitHub |
| [Custom classification](/configuration/custom_classification/state_classification) (training) | MobileNetV2 ImageNet base weights (via Keras) | Google storage |
-| [Audio transcription](/configuration/advanced) | Whisper or Sherpa-ONNX streaming model | HuggingFace / OpenAI |
+| [Audio transcription](/configuration/advanced/system) | Whisper or Sherpa-ONNX streaming model | HuggingFace / OpenAI |
### Hardware-Specific Detector Models
@@ -91,13 +91,13 @@ See [Frigate+](/integrations/plus) for details.
When a Generative AI provider is configured, Frigate sends images and prompts to the configured provider for event descriptions, chat, and camera monitoring. Available providers:
-| Provider | Internet Required |
-| ------------- | ---------------------------------------------------------------- |
-| OpenAI | Yes — connects to OpenAI API (or custom base URL) |
-| Google Gemini | Yes — connects to Google Generative AI API |
-| Azure OpenAI | Yes — connects to your Azure endpoint |
-| Ollama | Depends — typically local (`localhost:11434`), but can be remote |
-| llama.cpp | No — runs entirely locally |
+| Provider | Internet Required |
+| ------------- | --------------------------------------------------------------- |
+| OpenAI | Yes, connects to OpenAI API (or custom base URL) |
+| Google Gemini | Yes, connects to Google Generative AI API |
+| Azure OpenAI | Yes, connects to your Azure endpoint |
+| Ollama | Depends: typically local (`localhost:11434`), but can be remote |
+| llama.cpp | No, runs entirely locally |
Disable Generative AI by removing the `genai` configuration from your cameras. See [Generative AI](/configuration/genai/genai_config) for details.
@@ -126,30 +126,30 @@ When using the [DeepStack detector plugin](/configuration/object_detectors), Fri
For [WebRTC live streaming](/configuration/live), Frigate uses STUN for NAT traversal:
-- **go2rtc** defaults to a local STUN listener (`stun:8555`) — no internet required.
+- **go2rtc** defaults to a local STUN listener (`stun:8555`), no internet required.
- **The web UI's WebRTC player** includes a fallback to Google's public STUN server (`stun:stun.l.google.com:19302`), which requires internet.
## Home Assistant Supervisor
-When running as a Home Assistant add-on, the go2rtc startup script queries the local Supervisor API (`http://supervisor/`) to discover the host IP address and WebRTC port. This is a local network call to the Home Assistant host, not an internet connection.
+When running as a Home Assistant App, the go2rtc startup script queries the local Supervisor API (`http://supervisor/`) to discover the host IP address and WebRTC port. This is a local network call to the Home Assistant host, not an internet connection.
## What Does NOT Require Internet
-- **Object detection** — CPU, EdgeTPU, OpenVINO, and other bundled detector models are included in the Docker image.
-- **Recording and playback** — All video is stored and served locally.
-- **Live streaming** — Camera streams are pulled over your local network. MSE and HLS streaming work without any external connections.
-- **The web interface** — Fully self-contained with no external fonts, scripts, analytics, or CDN dependencies. All translations are bundled locally.
-- **Custom classification inference** — After training, custom models run entirely locally.
-- **Audio detection** — The YAMNet audio classification model is bundled in the Docker image.
+- **Object detection**: CPU, EdgeTPU, OpenVINO, and other bundled detector models are included in the Docker image.
+- **Recording and playback**: All video is stored and served locally.
+- **Live streaming**: Camera streams are pulled over your local network. MSE and HLS streaming work without any external connections.
+- **The web interface**: Fully self-contained with no external fonts, scripts, analytics, or CDN dependencies. All translations are bundled locally.
+- **Custom classification inference**: After training, custom models run entirely locally.
+- **Audio detection**: The YAMNet audio classification model is bundled in the Docker image.
## Running Frigate Offline
To run Frigate in an air-gapped or offline environment:
-1. **Pre-download models** — Start Frigate with internet access once with all desired features enabled. Models will be cached in `/config/model_cache/`.
-2. **Disable version check** — Set `telemetry.version_check: false` in your configuration.
-3. **Block outbound model requests** — Set the `HF_HUB_OFFLINE=1` and `TRANSFORMERS_OFFLINE=1` environment variables to prevent HuggingFace and Transformers from attempting any network requests.
-4. **Avoid cloud features** — Do not configure Frigate+, Generative AI providers that require internet, or cloud MQTT brokers.
-5. **Use local model mirrors** — If limited internet is available, set the `HF_ENDPOINT`, `GITHUB_ENDPOINT`, and `GITHUB_RAW_ENDPOINT` environment variables to point to local mirrors.
+1. **Pre-download models**: Start Frigate with internet access once with all desired features enabled. Models will be cached in `/config/model_cache/`.
+2. **Disable version check**: Set `telemetry.version_check: false` in your configuration.
+3. **Block outbound model requests**: Set the `HF_HUB_OFFLINE=1` and `TRANSFORMERS_OFFLINE=1` environment variables to prevent HuggingFace and Transformers from attempting any network requests.
+4. **Avoid cloud features**: Do not configure Frigate+, Generative AI providers that require internet, or cloud MQTT brokers.
+5. **Use local model mirrors**: If limited internet is available, set the `HF_ENDPOINT`, `GITHUB_ENDPOINT`, and `GITHUB_RAW_ENDPOINT` environment variables to point to local mirrors.
After these steps, Frigate will operate with no outbound internet connections.
diff --git a/docs/docs/frigate/planning_setup.md b/docs/docs/frigate/planning_setup.md
index 85c6eb6482..4e0d82ffb7 100644
--- a/docs/docs/frigate/planning_setup.md
+++ b/docs/docs/frigate/planning_setup.md
@@ -42,6 +42,8 @@ Frigate requires a CPU with AVX + AVX2 instructions. Most modern CPUs (post-2011
Storage is an important consideration when planning a new installation. To get a more precise estimate of your storage requirements, you can use an IP camera storage calculator. Websites like [IPConfigure Storage Calculator](https://calculator.ipconfigure.com/) can help you determine the necessary disk space based on your camera settings.
+Once running, see [Understanding storage usage](/configuration/record#understanding-storage-usage) for how Frigate measures and reports disk usage, and why its numbers won't exactly match `df` or `du`.
+
#### SSDs (Solid State Drives)
SSDs are an excellent choice for Frigate, offering high speed and responsiveness. The older concern that SSDs would quickly "wear out" from constant video recording is largely no longer valid for modern consumer and enterprise-grade SSDs.
diff --git a/docs/docs/guides/configuring_go2rtc.md b/docs/docs/guides/configuring_go2rtc.md
deleted file mode 100644
index 26fb266440..0000000000
--- a/docs/docs/guides/configuring_go2rtc.md
+++ /dev/null
@@ -1,116 +0,0 @@
----
-id: configuring_go2rtc
-title: Configuring go2rtc
----
-
-Use of the bundled go2rtc is optional. You can still configure FFmpeg to connect directly to your cameras. However, adding go2rtc to your configuration is required for the following features:
-
-- WebRTC or MSE for live viewing with audio, higher resolutions and frame rates than the jsmpeg stream which is limited to the detect stream and does not support audio
-- Live stream support for cameras in Home Assistant Integration
-- RTSP relay for use with other consumers to reduce the number of connections to your camera streams
-
-## Setup a go2rtc stream
-
-First, you will want to configure go2rtc to connect to your camera stream by adding the stream you want to use for live view in your Frigate config file. Avoid changing any other parts of your config at this step. Note that go2rtc supports [many different stream types](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#module-streams), not just rtsp.
-
-:::tip
-
-For the best experience, you should set the stream name under `go2rtc` to match the name of your camera so that Frigate will automatically map it and be able to use better live view options for the camera.
-
-See [the live view docs](../configuration/live.md#setting-streams-for-live-ui) for more information.
-
-:::
-
-```yaml
-go2rtc:
- streams:
- back:
- - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
-```
-
-After adding this to the config, restart Frigate and try to watch the live stream for a single camera by clicking on it from the dashboard. It should look much clearer and more fluent than the original jsmpeg stream.
-
-### What if my video doesn't play?
-
-- Check Logs:
- - Access the go2rtc logs in the Frigate UI under Logs in the sidebar.
- - If go2rtc is having difficulty connecting to your camera, you should see some error messages in the log.
-
-- Check go2rtc Web Interface: if you don't see any errors in the logs, try viewing the camera through go2rtc's web interface.
- - Navigate to port 1984 in your browser to access go2rtc's web interface.
- - If using Frigate through Home Assistant, enable the web interface at port 1984.
- - If using Docker, forward port 1984 before accessing the web interface.
- - Click `stream` for the specific camera to see if the camera's stream is being received.
-
-- Check Video Codec:
- - If the camera stream works in go2rtc but not in your browser, the video codec might be unsupported.
- - If using H265, switch to H264. Refer to [video codec compatibility](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#codecs-madness) in go2rtc documentation.
- - If unable to switch from H265 to H264, or if the stream format is different (e.g., MJPEG), re-encode the video using [FFmpeg parameters](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#source-ffmpeg). It supports rotating and resizing video feeds and hardware acceleration. Keep in mind that transcoding video from one format to another is a resource intensive task and you may be better off using the built-in jsmpeg view.
- ```yaml
- go2rtc:
- streams:
- back:
- - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
- - "ffmpeg:back#video=h264#hardware"
- ```
-
-- Switch to FFmpeg if needed:
- - Some camera streams may need to use the ffmpeg module in go2rtc. This has the downside of slower startup times, but has compatibility with more stream types.
-
- ```yaml
- go2rtc:
- streams:
- back:
- - ffmpeg:rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
- ```
-
- - If you can see the video but do not have audio, this is most likely because your camera's audio stream codec is not AAC.
- - If possible, update your camera's audio settings to AAC in your camera's firmware.
- - If your cameras do not support AAC audio, you will need to tell go2rtc to re-encode the audio to AAC on demand if you want audio. This will use additional CPU and add some latency. To add AAC audio on demand, you can update your go2rtc config as follows:
-
- ```yaml
- go2rtc:
- streams:
- back:
- - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
- - "ffmpeg:back#audio=aac"
- ```
-
- If you need to convert **both** the audio and video streams, you can use the following:
-
- ```yaml
- go2rtc:
- streams:
- back:
- - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
- - "ffmpeg:back#video=h264#audio=aac#hardware"
- ```
-
- When using the ffmpeg module, you would add AAC audio like this:
-
- ```yaml
- go2rtc:
- streams:
- back:
- - "ffmpeg:rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2#video=copy#audio=copy#audio=aac#hardware"
- ```
-
-:::warning
-
-To access the go2rtc stream externally when utilizing the Frigate App (for
-instance through VLC), you must first enable the RTSP Restream port.
-You can do this by visiting the Frigate App configuration page within Home
-Assistant and revealing the hidden options under the "Show disabled ports"
-section.
-
-:::
-
-### Next steps
-
-1. If the stream you added to go2rtc is also used by Frigate for the `record` or `detect` role, you can migrate your config to pull from the RTSP restream to reduce the number of connections to your camera as shown [here](/configuration/restream#reduce-connections-to-camera).
-2. You can [set up WebRTC](/configuration/live#webrtc-extra-configuration) if your camera supports two-way talk. Note that WebRTC only supports specific audio formats and may require opening ports on your router.
-3. If your camera supports two-way talk, you must configure your stream with `#backchannel=0` to prevent go2rtc from blocking other applications from accessing the camera's audio output. See [preventing go2rtc from blocking two-way audio](/configuration/restream#two-way-talk-restream) in the restream documentation.
-
-## Homekit Configuration
-
-To add camera streams to Homekit Frigate must be configured in docker to use `host` networking mode. Once that is done, you can use the go2rtc WebUI (accessed via port 1984, which is disabled by default) to share export a camera to Homekit. Any changes made will automatically be saved to `/config/go2rtc_homekit.yml`.
diff --git a/docs/docs/guides/getting_started.md b/docs/docs/guides/getting_started.md
index cd456f2014..fe355abe52 100644
--- a/docs/docs/guides/getting_started.md
+++ b/docs/docs/guides/getting_started.md
@@ -144,7 +144,7 @@ At this point you should be able to start Frigate and a basic config will be cre
### Step 2: Add a camera
-Click the **Add Camera** button in to use the camera setup wizard to get your first camera added into Frigate.
+Click the **Add Camera** button in to use the camera setup wizard to get your first camera added into Frigate.
### Step 3: Configure hardware acceleration (recommended)
@@ -192,7 +192,7 @@ cameras:
### Step 4: Configure detectors
-By default, Frigate will use a single CPU detector.
+By default, Frigate will use a single OpenVINO detector running on the CPU.
In many cases, the integrated graphics on Intel CPUs provides sufficient performance for typical Frigate setups. If you have an Intel processor, you can follow the configuration below.
@@ -204,8 +204,8 @@ You need to refer to **Configure hardware acceleration** above to enable the con
-1. Navigate to and add a detector with **Type** `OpenVINO` and **Device** `GPU`
-2. Navigate to and configure the model settings for OpenVINO:
+1. Navigate to and add a detector with **Type** `OpenVINO` and **Device** `GPU`
+2. On the same page, in the **Custom Model** tab, configure the model settings for OpenVINO:
| Field | Value |
| ---------------------------------------- | ------------------------------------------ |
@@ -273,7 +273,7 @@ services:
-Navigate to and add a detector with **Type** `EdgeTPU` and **Device** `usb`.
+Navigate to and add a detector with **Type** `EdgeTPU` and **Device** `usb`.
@@ -301,11 +301,11 @@ cameras:
More details on available detectors can be found [here](../configuration/object_detectors.md).
-Restart Frigate and you should start seeing detections for `person`. If you want to track other objects, they can be configured in or via the [configuration file reference](../configuration/reference.md).
+Restart Frigate and you should start seeing detections for `person`. If you want to track other objects, they can be configured in or via the [configuration file reference](../configuration/advanced/reference.md).
### Step 5: Setup motion masks
-Now that you have optimized your configuration for decoding the video stream, you will want to check to see where to implement motion masks. Click on the camera from the main dashboard, then select the gear icon in the top right, enable Debug View, and finally enable the switch for Motion Boxes. Watch for areas that continuously trigger unwanted motion to be detected. Common areas to mask include camera timestamps and trees that frequently blow in the wind. The goal is to avoid wasting object detection cycles looking at these areas.
+Now that you have optimized your configuration for decoding the video stream, you will want to check to see where to implement motion masks. Click on the camera from the main dashboard, then select the gear icon in the top right, enable the [Debug view](/usage/live#the-single-camera-view), and finally enable the switch for Motion Boxes. Watch for areas that continuously trigger unwanted motion to be detected. Common areas to mask include camera timestamps and trees that frequently blow in the wind. The goal is to avoid wasting object detection cycles looking at these areas.
Use the mask editor to draw polygon masks directly on the camera feed. Navigate to and set up a motion mask over the area. More information about masks can be found [here](../configuration/masks.md).
@@ -348,7 +348,7 @@ In order to review activity in the Frigate UI, recordings need to be enabled.
-1. If you have separate streams for detect and record, navigate to , select your camera, and add a second input with the `record` role pointing to your high-resolution stream
+1. If you have separate streams for detect and record, navigate to , select your camera, and add a second input with the `record` role pointing to your high-resolution stream
2. Navigate to (or for a specific camera) and set **Enable recording** to on
@@ -388,21 +388,20 @@ If you only plan to use Frigate for recording, it is still recommended to define
:::
-By default, Frigate will retain video of all tracked objects for 10 days. The full set of options for recording can be found [here](../configuration/reference.md).
+By default, Frigate will retain video of all tracked objects for 10 days. The full set of options for recording can be found [here](../configuration/advanced/reference.md).
### Step 7: Complete config
At this point you have a complete config with basic functionality.
-- View [common configuration examples](../configuration/index.md#common-configuration-examples) for a list of common configuration examples.
-- View [full config reference](../configuration/reference.md) for a complete list of configuration options.
+- View [common configuration examples](../configuration/config.md#common-configuration-examples) for a list of common configuration examples.
+- View [full config reference](../configuration/advanced/reference.md) for a complete list of configuration options.
### Follow up
Now that you have a working install, you can use the following documentation for additional features:
-1. [Configuring go2rtc](configuring_go2rtc.md) - Additional live view options and RTSP relay
-2. [Zones](../configuration/zones.md)
-3. [Review](../configuration/review.md)
-4. [Masks](../configuration/masks.md)
-5. [Home Assistant Integration](../integrations/home-assistant.md) - Integrate with Home Assistant
+1. [Zones](../configuration/zones.md)
+2. [Review](../configuration/review.md)
+3. [Masks](../configuration/masks.md)
+4. [Home Assistant Integration](../integrations/home-assistant.md) - Integrate with Home Assistant
diff --git a/docs/docs/guides/reverse_proxy.md b/docs/docs/guides/reverse_proxy.md
index 5edfb5c60d..bf33bcc278 100644
--- a/docs/docs/guides/reverse_proxy.md
+++ b/docs/docs/guides/reverse_proxy.md
@@ -10,13 +10,14 @@ A reverse proxy is typically needed if you want to set up Frigate on a custom UR
Before setting up a reverse proxy, check if any of the built-in functionality in Frigate suits your needs:
|Topic|Docs|
|-|-|
-|TLS|Please see the `tls` [configuration option](../configuration/tls.md)|
+|TLS|Please see the `tls` [configuration option](../configuration/tls.md)|
|Authentication|Please see the [authentication](../configuration/authentication.md) documentation|
-|IPv6|[Enabling IPv6](../configuration/advanced.md#enabling-ipv6)
+|IPv6|[Enabling IPv6](../configuration/advanced/system.md#enabling-ipv6)
-**Note about TLS**
-When using a reverse proxy, the TLS session is usually terminated at the proxy, sending the internal request over plain HTTP. If this is the desired behavior, TLS must first be disabled in Frigate, or you will encounter an HTTP 400 error: "The plain HTTP request was sent to HTTPS port."
+**Note about TLS**
+When using a reverse proxy, the TLS session is usually terminated at the proxy, sending the internal request over plain HTTP. If this is the desired behavior, TLS must first be disabled in Frigate, or you will encounter an HTTP 400 error: "The plain HTTP request was sent to HTTPS port."
To disable TLS, set the following in your Frigate configuration:
+
```yml
tls:
enabled: false
@@ -24,18 +25,26 @@ tls:
:::warning
A reverse proxy can be used to secure access to an internal web server, but the user will be entirely reliant on the steps they have taken. You must ensure you are following security best practices.
-This page does not attempt to outline the specific steps needed to secure your internal website.
+This page does not attempt to outline the specific steps needed to secure your internal website.
Please use your own knowledge to assess and vet the reverse proxy software before you install anything on your system.
:::
+## WebSocket support
+
+Frigate relies on WebSockets for real-time communication between the browser and the backend. Features such as camera controls (enabling/disabling a camera, audio, detect, recordings, and other toggles), live stream playback, and other live-updating parts of the UI will not function correctly if WebSocket connections are not proxied.
+
+Your reverse proxy must be configured to forward the `Upgrade` and `Connection` headers so that WebSocket connections can be established. Each proxy example below already includes the directives needed to do this, but if you are adapting your own configuration, ensure these headers are passed through.
+
+Note that some proxies disable WebSocket support by default. For example, Nginx Proxy Manager has a "Websockets Support" toggle that must be enabled.
+
## Proxies
There are many solutions available to implement reverse proxies and the community is invited to help out documenting others through a contribution to this page.
-* [Apache2](#apache2-reverse-proxy)
-* [Nginx](#nginx-reverse-proxy)
-* [Traefik](#traefik-reverse-proxy)
-* [Caddy](#caddy-reverse-proxy)
+- [Apache2](#apache2-reverse-proxy)
+- [Nginx](#nginx-reverse-proxy)
+- [Traefik](#traefik-reverse-proxy)
+- [Caddy](#caddy-reverse-proxy)
## Apache2 Reverse Proxy
@@ -159,7 +168,7 @@ The settings below enabled connection upgrade, sets up logging (optional) and pr
## Traefik Reverse Proxy
-This example shows how to add a `label` to the Frigate Docker compose file, enabling Traefik to automatically discover your Frigate instance.
+This example shows how to add a `label` to the Frigate Docker compose file, enabling Traefik to automatically discover your Frigate instance.
Before using the example below, you must first set up Traefik with the [Docker provider](https://doc.traefik.io/traefik/providers/docker/)
```yml
@@ -203,7 +212,7 @@ This example shows Frigate running under a subdomain with logging and a tls cert
}
frigate.YOUR_DOMAIN.TLD {
- reverse_proxy http://localhost:8971
+ reverse_proxy http://localhost:8971
import tls
import logging frigate.YOUR_DOMAIN.TLD
}
diff --git a/docs/docs/integrations/home-assistant.md b/docs/docs/integrations/home-assistant.md
index 5b9c014377..dd7d548047 100644
--- a/docs/docs/integrations/home-assistant.md
+++ b/docs/docs/integrations/home-assistant.md
@@ -9,6 +9,8 @@ The best way to integrate with Home Assistant is to use the [official integratio
### Preparation
+Frigate itself must be installed and running before setting up the integration. See the [installation documentation](../frigate/installation.md) for details.
+
The Frigate integration requires the `mqtt` integration to be installed and
manually configured first.
@@ -122,7 +124,7 @@ Use `http://:8971` as the URL for the integration so that aut
The above URL assumes you have [disabled TLS](../configuration/tls).
By default, TLS is enabled and Frigate will be using a self-signed certificate. HomeAssistant will fail to connect HTTPS to port 8971 since it fails to verify the self-signed certificate.
-Either disable TLS and use HTTP from HomeAssistant, or configure Frigate to be acessible with a valid certificate.
+Either disable TLS and use HTTP from HomeAssistant, or configure Frigate to be accessible with a valid certificate.
:::
@@ -195,7 +197,7 @@ For clips to be castable to media devices, audio is required and may need to be
## Camera API
-To disable a camera dynamically
+To turn a camera off (pauses Frigate's processing of the stream; does not persist across Frigate restarts; see [Camera state](/configuration/live#camera-state)):
```
action: camera.turn_off
@@ -204,7 +206,7 @@ target:
entity_id: camera.back_deck_cam # your Frigate camera entity ID
```
-To enable a camera that has been disabled dynamically
+To turn a camera back on:
```
action: camera.turn_on
@@ -213,6 +215,12 @@ target:
entity_id: camera.back_deck_cam # your Frigate camera entity ID
```
+:::note
+
+These actions toggle Frigate's runtime On/Off state. To permanently disable a camera, set its status to **Disabled** in **Settings → Camera Management** in the Frigate UI.
+
+:::
+
## Notification API
Many people do not want to expose Frigate to the web, so the integration creates some public API endpoints that can be used for notifications.
diff --git a/docs/docs/integrations/mqtt.md b/docs/docs/integrations/mqtt.md
index 28c178d1f5..e929ade300 100644
--- a/docs/docs/integrations/mqtt.md
+++ b/docs/docs/integrations/mqtt.md
@@ -280,7 +280,7 @@ Same data available at `/api/stats` published at a configurable interval.
### `frigate/camera_activity`
-Returns data about each camera, its current features, and if it is detecting motion, objects, etc. Can be triggered by publising to `frigate/onConnect`
+Returns data about each camera, its current features, and if it is detecting motion, objects, etc. Can be triggered by publishing to `frigate/onConnect`
### `frigate/profile/set`
@@ -306,7 +306,7 @@ Publishes the current health status of each role that is enabled (`audio`, `dete
- `online`: Stream is running and being processed
- `offline`: Stream is offline and is being restarted
-- `disabled`: Camera is currently disabled
+- `disabled`: Camera is currently turned off (either at runtime via the `enabled/set` topic, or persistently via the configuration file). See [Camera state](/configuration/live#camera-state) for the distinction.
### `frigate//`
@@ -368,15 +368,15 @@ The published value is the detected state class name (e.g., `open`, `closed`, `o
### `frigate//enabled/set`
-Topic to turn Frigate's processing of a camera on and off. Expected values are `ON` and `OFF`.
+Topic to turn Frigate's processing of a camera on or off at runtime. Expected values are `ON` and `OFF`. The change is persisted across Frigate restarts (see [Runtime toggle persistence](/configuration/live#runtime-toggle-persistence)). To permanently change the configured value, use **Settings → Global configuration → Camera management** in the Frigate UI. See [Camera state](/configuration/live#camera-state) for the difference between turning a camera off and disabling it.
### `frigate//enabled/state`
-Topic with current state of processing for a camera. Published values are `ON` and `OFF`.
+Topic with current runtime state of processing for a camera. Published values are `ON` and `OFF`.
### `frigate//detect/set`
-Topic to turn object detection for a camera on and off. Expected values are `ON` and `OFF`.
+Topic to turn object detection for a camera on and off. Expected values are `ON` and `OFF`. The change is persisted across Frigate restarts (see [Runtime toggle persistence](/configuration/live#runtime-toggle-persistence)).
### `frigate//detect/state`
@@ -384,7 +384,7 @@ Topic with current state of object detection for a camera. Published values are
### `frigate//audio/set`
-Topic to turn audio detection for a camera on and off. Expected values are `ON` and `OFF`.
+Topic to turn audio detection for a camera on and off. Expected values are `ON` and `OFF`. The change is persisted across Frigate restarts (see [Runtime toggle persistence](/configuration/live#runtime-toggle-persistence)).
### `frigate//audio/state`
@@ -392,7 +392,7 @@ Topic with current state of audio detection for a camera. Published values are `
### `frigate//recordings/set`
-Topic to turn recordings for a camera on and off. Expected values are `ON` and `OFF`.
+Topic to turn recordings for a camera on and off. Expected values are `ON` and `OFF`. The change is persisted across Frigate restarts (see [Runtime toggle persistence](/configuration/live#runtime-toggle-persistence)).
### `frigate//recordings/state`
@@ -400,7 +400,7 @@ Topic with current state of recordings for a camera. Published values are `ON` a
### `frigate//snapshots/set`
-Topic to turn snapshots for a camera on and off. Expected values are `ON` and `OFF`.
+Topic to turn snapshots for a camera on and off. Expected values are `ON` and `OFF`. The change is persisted across Frigate restarts (see [Runtime toggle persistence](/configuration/live#runtime-toggle-persistence)).
### `frigate//snapshots/state`
diff --git a/docs/docs/integrations/plus.md b/docs/docs/integrations/plus.md
index 9783cb212a..949a9f49be 100644
--- a/docs/docs/integrations/plus.md
+++ b/docs/docs/integrations/plus.md
@@ -3,6 +3,8 @@ id: plus
title: Frigate+
---
+import NavPath from "@site/src/components/NavPath";
+
For more information about how to use Frigate+ to improve your model, see the [Frigate+ docs](/plus/).
:::info
@@ -57,7 +59,7 @@ You can view all of your submitted images at [https://plus.frigate.video](https:
Once you have [requested your first model](../plus/first_model.md) and gotten your own model ID, it can be used with a special model path. No other information needs to be configured for Frigate+ models because it fetches the remaining config from Frigate+ automatically.
-You can either choose the new model from the Frigate+ pane in the Settings page of the Frigate UI, or manually set the model at the root level in your config:
+You can either choose the new model from the pane in the Frigate UI (the **Frigate+ Model** tab), or manually set the model at the root level in your config:
```yaml
detectors: ...
diff --git a/docs/docs/integrations/third_party_extensions.md b/docs/docs/integrations/third_party_extensions.md
index e0fe835633..64b174a042 100644
--- a/docs/docs/integrations/third_party_extensions.md
+++ b/docs/docs/integrations/third_party_extensions.md
@@ -17,9 +17,13 @@ Please use your own knowledge to assess and vet them before you install anything
The [Advanced Camera Card](https://card.camera/#/README) is a Home Assistant dashboard card with deep Frigate integration.
+## [cctvQL](https://github.com/arunrajiah/cctvql)
+
+[cctvQL](https://github.com/arunrajiah/cctvql) is a natural language query layer for Frigate and other CCTV systems. It connects to Frigate's REST API and MQTT broker to let you ask conversational questions about cameras and events (e.g. "Was there motion at the front door last night?"), with support for real-time event streaming, anomaly detection, PTZ control, alert rules, and a Home Assistant custom component.
+
## [Double Take](https://github.com/skrashevich/double-take)
-[Double Take](https://github.com/skrashevich/double-take) provides an unified UI and API for processing and training images for facial recognition.
+[Double Take](https://github.com/skrashevich/double-take) provides a unified UI and API for processing and training images for facial recognition.
It supports automatically setting the sub labels in Frigate for person objects that are detected and recognized.
This is a fork (with fixed errors and new features) of [original Double Take](https://github.com/jakowenko/double-take) project which, unfortunately, isn't being maintained by author.
@@ -27,6 +31,10 @@ This is a fork (with fixed errors and new features) of [original Double Take](ht
[Frigate Notify](https://github.com/0x2142/frigate-notify) is a simple app designed to send notifications from Frigate to your favorite platforms. Intended to be used with standalone Frigate installations - Home Assistant not required, MQTT is optional but recommended.
+## [Frigate Notify Alert](https://github.com/Sysoev86/frigate-notify-alert)
+
+[Frigate Notify Alert](https://github.com/Sysoev86/frigate-notify-alert) sends Frigate events to Telegram as a photo + video media group. It supports multiple camera groups (each notifying its own chat), optional zone filtering (notify only when an object enters a chosen zone), and in-chat buttons to pause notifications for a set time. Works with standalone Frigate over MQTT; Home Assistant not required.
+
## [Frigate Snap-Sync](https://github.com/thequantumphysicist/frigate-snap-sync/)
[Frigate Snap-Sync](https://github.com/thequantumphysicist/frigate-snap-sync/) is a program that works in tandem with Frigate. It responds to Frigate when a snapshot or a review is made (and more can be added), and uploads them to one or more remote server(s) of your choice.
@@ -35,13 +43,17 @@ This is a fork (with fixed errors and new features) of [original Double Take](ht
[Frigate telegram](https://github.com/OldTyT/frigate-telegram) makes it possible to send events from Frigate to Telegram. Events are sent as a message with a text description, video, and thumbnail.
+## [kiosk-monitor](https://github.com/extremeshok/kiosk-monitor)
+
+[kiosk-monitor](https://github.com/extremeshok/kiosk-monitor) is a Raspberry Pi watchdog that runs Chromium fullscreen on a Frigate dashboard (optionally with VLC on a second monitor for an RTSP camera stream), auto-restarts on frozen screens or unreachable URLs, and ships a Birdseye-aware Chromium helper that auto-sizes the grid to the display.
+
## [Periscope](https://github.com/maksz42/periscope)
[Periscope](https://github.com/maksz42/periscope) is a lightweight Android app that turns old devices into live viewers for Frigate. It works on Android 2.2 and above, including Android TV. It supports authentication and HTTPS.
## [Scrypted - Frigate bridge plugin](https://github.com/apocaliss92/scrypted-frigate-bridge)
-[Scrypted - Frigate bridge](https://github.com/apocaliss92/scrypted-frigate-bridge) is an plugin that allows to ingest Frigate detections, motion, videoclips on Scrypted as well as provide templates to export rebroadcast configurations on Frigate.
+[Scrypted - Frigate bridge](https://github.com/apocaliss92/scrypted-frigate-bridge) is a plugin that allows you to ingest Frigate detections, motion, videoclips on Scrypted as well as provide templates to export rebroadcast configurations on Frigate.
## [Strix](https://github.com/eduard256/Strix)
diff --git a/docs/docs/plus/annotating.md b/docs/docs/plus/annotating.md
index dc8e571be4..3725ab5c2c 100644
--- a/docs/docs/plus/annotating.md
+++ b/docs/docs/plus/annotating.md
@@ -19,7 +19,7 @@ For the best results, follow these guidelines. You may also want to review the d
## AI suggested labels
-If you have an active Frigate+ subscription, new uploads will be scanned for the objects configured for you camera and you will see suggested labels as light blue boxes when annotating in Frigate+. These suggestions are processed via a queue and typically complete within a minute after uploading, but processing times can be longer.
+If you have an active Frigate+ subscription, new uploads will be scanned for the objects configured for your camera and you will see suggested labels as light blue boxes when annotating in Frigate+. These suggestions are processed via a queue and typically complete within a minute after uploading, but processing times can be longer.

diff --git a/docs/docs/plus/first_model.md b/docs/docs/plus/first_model.md
index e9523f6b98..98095554b5 100644
--- a/docs/docs/plus/first_model.md
+++ b/docs/docs/plus/first_model.md
@@ -3,6 +3,10 @@ id: first_model
title: Requesting your first model
---
+import ConfigTabs from "@site/src/components/ConfigTabs";
+import TabItem from "@theme/TabItem";
+import NavPath from "@site/src/components/NavPath";
+
## Step 1: Upload and annotate your images
Before requesting your first model, you will need to upload and verify at least 10 images to Frigate+. The more images you upload, annotate, and verify the better your results will be. Most users start to see very good results once they have at least 100 verified images per camera. Keep in mind that varying conditions should be included. You will want images from cloudy days, sunny days, dawn, dusk, and night. Refer to the [integration docs](../integrations/plus.md#generate-an-api-key) for instructions on how to easily submit images to Frigate+ directly from Frigate.
@@ -16,13 +20,21 @@ For more detailed recommendations, you can refer to the docs on [annotating](./a
Once you have an initial set of verified images, you can request a model on the Models page. For guidance on choosing a model type, refer to [this part of the documentation](./index.md#available-model-types). If you are unsure which type to request, you can test the base model for each version from the "Base Models" tab. Each model request requires 1 of the 12 trainings that you receive with your annual subscription. This model will support all [label types available](./index.md#available-label-types) even if you do not submit any examples for those labels. Model creation can take up to 36 hours.

-## Step 3: Set your model id in the config
+## Step 3: Set your model
You will receive an email notification when your Frigate+ model is ready.

Models available in Frigate+ can be used with a special model path. No other information needs to be configured because it fetches the remaining config from Frigate+ automatically.
+
+
+
+Navigate to . In the **Detection Model** section, choose the **Frigate+** tab. Select your new Frigate+ model from the **Available Frigate+ models** dropdown, then click **Save**. Restart Frigate to apply the change.
+
+
+
+
```yaml
detectors: ...
@@ -30,22 +42,46 @@ model:
path: plus://
```
-:::note
-
-Model IDs are not secret values and can be shared freely. Access to your model is protected by your API key.
-
-:::
-
:::tip
When setting the plus model id, all other fields should be removed as these are configured automatically with the Frigate+ model config
:::
+
+
+
+:::note
+
+Model IDs are not secret values and can be shared freely. Access to your model is protected by your API key.
+
+:::
+
## Step 4: Adjust your object filters for higher scores
Frigate+ models generally have much higher scores than the default model provided in Frigate. You will likely need to increase your `threshold` and `min_score` values. Here is an example of how these values can be refined, but you should expect these to evolve as your model improves. For more information about how `threshold` and `min_score` are related, see the docs on [object filters](../configuration/object_filters.md#object-scores).
+
+
+
+Navigate to . Under **Object filters**, set **Min Score** and **Threshold** for each object type, then click **Save**.
+
+| Object | Min Score | Threshold |
+| ----------------- | --------- | --------- |
+| **dog** | .7 | .9 |
+| **cat** | .65 | .8 |
+| **face** | .7 | |
+| **package** | .65 | .9 |
+| **license_plate** | .6 | |
+| **amazon** | .75 | |
+| **ups** | .75 | |
+| **fedex** | .75 | |
+| **person** | .65 | .85 |
+| **car** | .65 | .85 |
+
+
+
+
```yaml
objects:
filters:
@@ -75,3 +111,6 @@ objects:
min_score: .65
threshold: .85
```
+
+
+
diff --git a/docs/docs/troubleshooting/cpu.md b/docs/docs/troubleshooting/cpu.md
index a9f449ad88..a6c2444a5b 100644
--- a/docs/docs/troubleshooting/cpu.md
+++ b/docs/docs/troubleshooting/cpu.md
@@ -24,7 +24,7 @@ Video decoding is one of the most CPU-intensive tasks in Frigate. While an AI ac
### Configuration
-Frigate provides preset configurations for common hardware acceleration scenarios. Set up `hwaccel_args` based on your hardware in your [configuration](../configuration/reference) as described in the [getting started guide](../guides/getting_started).
+Frigate provides preset configurations for common hardware acceleration scenarios. Set up `hwaccel_args` based on your hardware in your [configuration](../configuration/advanced/reference) as described in the [getting started guide](../guides/getting_started).
### Troubleshooting Hardware Acceleration
@@ -44,7 +44,7 @@ Choosing the right detector for your hardware is the single most important facto
### Understanding Detector Performance
-Frigate uses motion detection as a first-line check before running expensive object detection, as explained in the [motion detection documentation](../configuration/motion_detection). When motion is detected, Frigate creates a "region" (the green boxes in the debug viewer) and sends it to the detector. The detector's inference speed determines how many detections per second your system can handle.
+Frigate uses motion detection as a first-line check before running expensive object detection, as explained in the [motion detection documentation](../configuration/motion_detection). When motion is detected, Frigate creates a "region" (the green boxes in the [debug viewer](/usage/live#the-single-camera-view)) and sends it to the detector. The detector's inference speed determines how many detections per second your system can handle.
**Calculating Detector Capacity:** Your detector has a finite capacity measured in detections per second. With an inference speed of 10ms, your detector can handle approximately 100 detections per second (1000ms / 10ms = 100).If your cameras collectively require more than this capacity, you'll experience delays, missed detections, or the system will fall behind.
@@ -58,7 +58,6 @@ When a single detector cannot keep up with your camera count, some detector type
For detailed instructions on configuring multiple detectors, see the [Object Detectors documentation](../configuration/object_detectors).
-
**When to add a second detector:**
- Skipped FPS is consistently > 0 even during normal activity
@@ -70,4 +69,6 @@ The model you use significantly impacts detector performance. Frigate provides d
**Model Size Trade-offs:**
- Smaller models (320x320): Faster inference, Frigate is specifically optimized for a 320x320 size model.
-- Larger models (640x640): Slower inference, can sometimes have higher accuracy on very large objects that take up a majority of the frame.
\ No newline at end of file
+- Larger models (640x640): Slower inference, can sometimes have higher accuracy on very large objects that take up a majority of the frame.
+
+For more detail on picking the right size, see [Choosing a model size](../configuration/object_detectors.md#choosing-a-model-size).
diff --git a/docs/docs/troubleshooting/dummy-camera.md b/docs/docs/troubleshooting/dummy-camera.md
index e24c821290..630a7921d5 100644
--- a/docs/docs/troubleshooting/dummy-camera.md
+++ b/docs/docs/troubleshooting/dummy-camera.md
@@ -3,6 +3,8 @@ id: dummy-camera
title: Analyzing Object Detection
---
+import NavPath from "@site/src/components/NavPath";
+
Frigate provides several tools for investigating object detection and tracking behavior: reviewing recorded detections through the UI, using the built-in Debug Replay feature, and manually setting up a dummy camera for advanced scenarios.
## Reviewing Detections in the UI
@@ -37,6 +39,10 @@ The per-clip variation is typically quite low and is mostly an artifact of keyfr
Debug Replay lets you re-run Frigate's detection pipeline against a section of recorded video without manually configuring a dummy camera. It automatically extracts the recording, creates a temporary camera with the same detection settings as the original, and loops the clip through the pipeline so you can observe detections in real time.
+The replay camera behaves like a live camera feed rather than History's video player: it loops the clip continuously as Frigate analyzes it and has no playback controls, so you cannot pause, scrub, or step through it frame by frame.
+
+Debug Replay isn't intended to be a one-stop pane for all Frigate diagnostics or a comprehensive debugging environment for every Frigate feature. It merely makes it easier to spin up a "dummy camera" and perform some common adjustments in real time. You'll still need to use the normal tools (logs, an MQTT client, etc) to debug your feature.
+
### When to use
- Reproducing a detection or tracking issue from a specific time range
@@ -49,17 +55,31 @@ Only one replay session can be active at a time. If a session is already running
:::
+### Starting Debug Replay
+
+Debug Replay can be started from several places in the UI. The starting point determines the time range that gets replayed.
+
+- **History: Actions menu.** Navigate to , open the **Actions** menu in the toolbar, and choose **Debug Replay**. From here you can pick a preset (**Last 1 Minute**, **Last 5 Minutes**), select a range directly on the timeline with **From Timeline**, or enter exact start and end times with **Custom**. This is the most flexible option and the best choice when you want to add padding around a detection. On mobile, the same options appear in the Actions drawer.
+- **History: Detail Stream event menu.** While viewing a review item in the Detail Stream, open the menu on a tracked object's event card and choose **Debug Replay**. The replay range is set automatically to that object's start and end times.
+- **Explore: search result menu.** From an Explore card, open the kebab menu and choose **Debug Replay**. The range is taken from the tracked object's lifecycle.
+- **Explore: Tracking Details Actions menu.** Open a tracked object's **Tracking Details** dialog, then choose **Debug Replay** from the Actions menu. Same automatic range as the search result menu.
+- **Exports: export card menu.** From , open the menu on an export and choose **Debug Replay** to loop the exported clip through the detection pipeline for the camera it was exported from.
+
+The Detail Stream, Explore, and Exports entry points use the underlying recording or export's bounds with a small amount of padding. This can be convenient for quick checks, but if a detection is short or you want extra "settle" time for motion and the detector, start the replay from the History Actions menu instead and widen the range manually.
+
### Variables to consider
- The replay will not always produce identical results to the original run. Different frames may be selected on replay, which can change detections and tracking.
- Motion detection depends on the exact frames used; small frame shifts can change motion regions and therefore what gets passed to the detector.
- Object detection is not fully deterministic: models and post-processing can yield slightly different results across runs.
+- In cases where a detection is short and a replay may only be a small number of frames, it is recommended to manually add some padding before and after the detection so that the motion and object detectors have time to settle into the scene. Rather than starting Debug Replay from Explore, navigate to History for your camera, choose Debug Replay from the Actions menu, and click the "From Timeline" or "Custom" option.
+- The replay camera inherits the source camera's zones. Any automations that trigger on those zone names will fire for the replay camera as well. This can be helpful when debugging zone behavior, but may be unexpected. You can add a condition on the source camera's name in your automation if you want to exclude replay triggers.
Treat the replay as a close approximation rather than an exact reproduction. Run multiple loops and examine the debug overlays and logs to understand the behavior.
## Manual Dummy Camera
-For advanced scenarios — such as testing with a clip from a different source, debugging ffmpeg behavior, or running a clip through a completely custom configuration — you can set up a dummy camera manually.
+For advanced scenarios (such as testing with a clip from a different source, debugging ffmpeg behavior, or running a clip through a completely custom configuration), you can set up a dummy camera manually.
### Example config
@@ -91,7 +111,7 @@ cameras:
2. Add the temporary camera to `config/config.yml` (example above). Use a unique name such as `test` or `replay_camera` so it's easy to remove later.
- If you're debugging a specific camera, copy the settings from that camera (frame rate, model/enrichment settings, zones, etc.) into the temporary camera so the replay closely matches the original environment. Leave `record` and `snapshots` disabled unless you are specifically debugging recording or snapshot behavior.
3. Restart Frigate.
-4. Observe the Debug view in the UI and logs as the clip is replayed. Watch detections, zones, or any feature you're looking to debug, and note any errors in the logs to reproduce the issue.
+4. Observe the [Debug view](/usage/live#the-single-camera-view) in the UI and logs as the clip is replayed. Watch detections, zones, or any feature you're looking to debug, and note any errors in the logs to reproduce the issue.
5. Iterate on camera or enrichment settings (model, fps, zones, filters) and re-check the replay until the behavior is resolved.
6. Remove the temporary camera from your config after debugging to avoid spurious telemetry or recordings.
diff --git a/docs/docs/troubleshooting/edgetpu.md b/docs/docs/troubleshooting/edgetpu.md
index 4ee25afd0f..bc50ae5040 100644
--- a/docs/docs/troubleshooting/edgetpu.md
+++ b/docs/docs/troubleshooting/edgetpu.md
@@ -12,7 +12,7 @@ There are many possible causes for a USB coral not being detected and some are O
:::tip
-Using `lsusb` or checking the hardware page in HA OS will show as `1a6e:089a Global Unichip Corp.` until Frigate runs an inferance using the coral. So don't worry about the identification until after Frigate has attempted to detect the coral.
+Using `lsusb` or checking the hardware page in HA OS will show as `1a6e:089a Global Unichip Corp.` until Frigate runs an inference using the coral. So don't worry about the identification until after Frigate has attempted to detect the coral.
:::
@@ -43,13 +43,13 @@ Some users have reported that this older device runs an older kernel causing iss
3. Start the docker container with Coral TPU enabled in the config
4. The TPU would be detected but a few moments later it would disconnect.
5. While leaving the TPU device plugged in, restart the NAS using the reboot command in the UI. Do NOT unplug the NAS/power it off etc.
-6. Open the control panel - info scree. The coral TPU will now be recognised as a USB Device - google inc
+6. Open the control panel - info screen. The coral TPU will now be recognized as a USB Device - google inc
7. Start the frigate container. Everything should work now!
### QNAP NAS
-QNAP NAS devices, such as the TS-253A, may use connected Coral TPU devices if [QuMagie](https://www.qnap.com/en/software/qumagie) is installed along with its QNAP AI Core extension. If any of the features—`facial recognition`, `object recognition`, or `similar photo recognition`—are enabled, Container Station applications such as `Frigate` or `CodeProject.AI Server` will be unable to initialize the TPU device in use.
-To allow the Coral TPU device to be discovered, the you must either:
+QNAP NAS devices, such as the TS-253A, may use connected Coral TPU devices if [QuMagie](https://www.qnap.com/en/software/qumagie) is installed along with its QNAP AI Core extension. If any of the features (`facial recognition`, `object recognition`, or `similar photo recognition`) are enabled, Container Station applications such as `Frigate` or `CodeProject.AI Server` will be unable to initialize the TPU device in use.
+To allow the Coral TPU device to be discovered, you must either:
1. [Disable the AI recognition features in QuMagie](https://docs.qnap.com/application/qumagie/2.x/en-us/configuring-qnap-ai-core-settings-FB13CE03.html),
2. Remove the QNAP AI Core extension or
@@ -76,7 +76,7 @@ This is an issue due to outdated gasket driver when being used with new linux ke
### Not detected on Raspberry Pi5
-A kernel update to the RPi5 means an upate to config.txt is required, see [the raspberry pi forum for more info](https://forums.raspberrypi.com/viewtopic.php?t=363682&sid=cb59b026a412f0dc041595951273a9ca&start=25)
+A kernel update to the RPi5 means an update to config.txt is required, see [the raspberry pi forum for more info](https://forums.raspberrypi.com/viewtopic.php?t=363682&sid=cb59b026a412f0dc041595951273a9ca&start=25)
Specifically, add the following to config.txt
@@ -87,7 +87,7 @@ dtoverlay=pcie-32bit-dma-pi5
## Only One PCIe Coral Is Detected With Coral Dual EdgeTPU
-Coral Dual EdgeTPU is one card with two identical TPU cores. Each core has it's own PCIe interface and motherboard needs to have two PCIe busses on the m.2 slot to make them both work.
+Coral Dual EdgeTPU is one card with two identical TPU cores. Each core has its own PCIe interface and motherboard needs to have two PCIe busses on the m.2 slot to make them both work.
E-key slot implemented to full m.2 electromechanical specification has two PCIe busses. Most motherboard manufacturers implement only one PCIe bus in m.2 E-key connector (this is why only one TPU is working). Some SBCs can have only USB bus on m.2 connector, ie none of TPUs will work.
diff --git a/docs/docs/troubleshooting/faqs.md b/docs/docs/troubleshooting/faqs.md
index ff2379ea72..1a51cb64e2 100644
--- a/docs/docs/troubleshooting/faqs.md
+++ b/docs/docs/troubleshooting/faqs.md
@@ -29,7 +29,7 @@ You can open `chrome://media-internals/` in another tab and then try to playback
### What do I do if my cameras sub stream is not good enough?
-Frigate generally [recommends cameras with configurable sub streams](/frigate/hardware.md). However, if your camera does not have a sub stream that a suitable resolution, the main stream can be resized.
+Frigate generally [recommends cameras with configurable sub streams](/frigate/hardware.md). However, if your camera does not have a sub stream that is a suitable resolution, the main stream can be resized.
To do this efficiently the following setup is required:
@@ -49,13 +49,13 @@ This almost always means that the width/height defined for your camera are not c
These messages in the logs are expected in certain situations. Frigate checks the integrity of the recordings before storing. Occasionally these cached files will be invalid and cleaned up automatically.
-### "On connect called"
+### "MQTT connected" repeats in the logs
-If you see repeated "On connect called" messages in your logs, check for another instance of Frigate. This happens when multiple Frigate containers are trying to connect to MQTT with the same `client_id`.
+If you see repeated "MQTT connected" messages in your logs, check for another instance of Frigate. This happens when multiple Frigate containers are trying to connect to MQTT with the same `client_id`.
### Error: Database Is Locked
-SQLite does not work well on a network share, if the `/media` folder is mapped to a network share then [this guide](../configuration/advanced.md#database) should be used to move the database to a location on the internal drive.
+SQLite does not work well on a network share, if the `/media` folder is mapped to a network share then [this guide](../configuration/advanced/system.md#database) should be used to move the database to a location on the internal drive.
### Unable to publish to MQTT: client is not connected
@@ -110,3 +110,46 @@ No. Frigate uses the TCP protocol to connect to your camera's RTSP URL. VLC auto
TCP ensures that all data packets arrive in the correct order. This is crucial for video recording, decoding, and stream processing, which is why Frigate enforces a TCP connection. UDP is faster but less reliable, as it does not guarantee packet delivery or order, and VLC does not have the same requirements as Frigate.
You can still configure Frigate to use UDP by using ffmpeg input args or the preset `preset-rtsp-udp`. See the [ffmpeg presets](/configuration/ffmpeg_presets) documentation.
+
+### Frigate is slow to start up with a "probing detect stream" message in the logs
+
+When `detect.width` and `detect.height` are not set, Frigate probes each camera's detect stream on startup (and when saving the config) to auto-detect its resolution. For RTSP streams Frigate probes with ffprobe and automatically retries over TCP if UDP doesn't respond, with a 5 second timeout per attempt. A camera that cannot be reached over either transport will add up to ~10 seconds to startup before Frigate falls through with default dimensions, which may show up as width `0` and height `0` in Camera Probe Info under System Metrics.
+
+To skip the probe entirely and make startup instant, set `detect.width` and `detect.height` explicitly in your camera config:
+
+```yaml
+cameras:
+ my_camera:
+ detect:
+ width: 1280
+ height: 720
+```
+
+### Why does Frigate keep creating new tracked objects for my parked car?
+
+Stationary tracking is designed to _prevent_ this: a parked car should remain a single tracked object rather than generating new ones. If you're repeatedly getting new tracked objects for the same car, it's likely that Frigate is losing the object and re-detecting it as a new one.
+
+Open one of the tracked objects in Explore → **Tracking Details**. If the detection scores are low (< 70% or so), the model isn't confident the parked car is a car. This is common with the free [COCO-trained](https://cocodataset.org/#explore) object detection models on steep/top-down angles, partially occluded cars, foliage, or low-light footage. When detections fall below `min_score` for too many frames the tracker loses the object, and the next confident frame creates a brand new one.
+
+What helps:
+
+- **Improve the view**: even a small angle change that gets more of the car visible could lift scores enough to stabilize tracking.
+- **Use a more accurate model**: switching from `mobiledet` to `yolov9`, or stepping up to a larger variant like `yolov9-s` over `yolov9-t`, can help (at the cost of inference time, and still on the COCO dataset). The biggest gains usually come from fine-tuning a model on images from your own cameras so it learns your specific scene. [Frigate+](https://frigate.video/plus) is a paid option that does this - models are trained on security-camera footage and can be fine-tuned on images you submit from your own setup.
+- **Don't set `detect -> stationary -> max_frames` for `car`**: it artificially ends tracking and forces re-detection as a new object. See [Stationary Objects](../configuration/stationary_objects.md).
+- **Restrict alerts to the areas you care about** with `required_zones`. See [Zones](../configuration/zones.md#restricting-alerts-and-detections-to-specific-zones). Make sure those zones use the default `loitering_time: 0` unless you specifically want the review item to stay open until the car leaves.
+- **Filter impossible locations** with [object filter masks](../configuration/masks.md#object-filter-masks) if cars are being detected on rooftops, treetops, etc.
+
+See [Object Filters](../configuration/object_filters.md) for more on tuning `min_score` and `threshold`. Note that raising them too high will make this exact problem worse.
+
+### How do I correct Frigate when it detects something as the wrong object?
+
+Frigate's object detection relies on a machine learning [model](../frigate/glossary.md#model), and the free [COCO-trained](https://cocodataset.org/#explore) models that ship with Frigate can misidentify objects in scenes they weren't trained on. There are two ways to handle this, depending on whether you want to _teach_ the model or just _suppress_ the bad result.
+
+**Train or fine-tune a model with your own images.** The most durable fix is to improve the model itself. The biggest gains usually come from fine-tuning a model on images from your own cameras so it learns your specific scene. Some tools are freely available, and [Frigate+](https://frigate.video/plus) is a paid option that does this - models are trained on security-camera footage and can be fine-tuned on images you submit from your own setup. When Frigate mislabels something, open the tracked object in Explore, select the **Snapshot** tab, and use **Submit to Frigate+** to send the example with the correct label (or mark it as a [false positive](../frigate/glossary.md#false-positive)). Once you've submitted examples and [requested a model](../plus/first_model.md), the retrained model will be more accurate for your cameras. See [Submitting examples to Frigate+](../integrations/plus.md#submit-examples) for the full workflow.
+
+**Suppress the misidentification with filters.** You can use filters to stop a specific false positive from being tracked:
+
+- Tune `min_score` / `threshold`, or add `min_area` / `max_area` / `min_ratio` / `max_ratio` filters. See [Object Filters](../configuration/object_filters.md).
+- If the false positive is always in the same fixed spot (like a statue or mailbox that reads as a person), add an [object filter mask](../configuration/masks.md#object-filter-masks) over that location.
+
+Filters and masks only hide the incorrect result - they don't teach Frigate what the object actually is. For that, fine-tune your own model or use Frigate+.
diff --git a/docs/docs/troubleshooting/go2rtc.md b/docs/docs/troubleshooting/go2rtc.md
new file mode 100644
index 0000000000..b8f2102406
--- /dev/null
+++ b/docs/docs/troubleshooting/go2rtc.md
@@ -0,0 +1,235 @@
+---
+id: go2rtc
+title: Troubleshooting go2rtc
+---
+
+import ConfigTabs from "@site/src/components/ConfigTabs";
+import TabItem from "@theme/TabItem";
+import NavPath from "@site/src/components/NavPath";
+
+This page covers common problems with the bundled [go2rtc](/configuration/go2rtc) and how to resolve them, whether your cameras were added with the setup wizard or configured by hand.
+
+When a stream won't play or behaves oddly, the most important first step is to figure out **where** in the pipeline it breaks. Frigate's live view is a chain (_camera → go2rtc → your browser_), and each stage fails for different reasons. Work through the checks below in order, then jump to the matching problem category.
+
+## Start by isolating the problem
+
+### 1. Read the go2rtc logs
+
+Access the go2rtc logs in the Frigate UI under in the sidebar (select the **go2rtc** tab). If go2rtc cannot connect to your camera you will usually see a clear error here: `401 Unauthorized` (bad or incorrectly encoded credentials), `Connection refused` / `timeout` (wrong IP, port, or the camera is at its connection limit), or `404 Not Found` (wrong RTSP path, or the referenced stream name does not exist).
+
+### 2. Test the stream in the go2rtc web interface
+
+If the logs look clean, open go2rtc's own web interface on port `1984`. This is the single most useful diagnostic, because it takes Frigate's UI out of the equation entirely.
+
+- If using Frigate through Home Assistant, enable the web interface at port `1984` (it is disabled by default, see [Home Assistant ports](#home-assistant-and-port-access)).
+- If using Docker, forward port `1984` before accessing the web interface.
+
+Open the stream page for your camera (`http://:1984/stream.html?src=back`) and try each player link:
+
+- **If nothing plays here**, the problem is between the camera and go2rtc (codec, credentials, or transport), _not_ your browser. Fix it at the source before touching anything in Frigate.
+- **If a player works here but Frigate's live view does not**, the problem is browser/codec related. Compare the **MSE** and **WebRTC** links. Frigate prefers MSE and only attempts WebRTC when MSE fails (or for two-way talk). If `mode=mse` plays but `mode=webrtc` does not, you have a [WebRTC codec problem](#webrtc-and-two-way-talk); if neither plays, your browser cannot decode the codec (commonly H.265, see [H.265 / HEVC cameras](#h265--hevc-cameras)).
+
+### 3. Inspect the negotiated codecs
+
+You can view detailed stream info, including the exact video and audio codecs go2rtc negotiated with the camera, at `http://frigate_ip:5000/api/go2rtc/streams` (or `http://frigate_ip:5000/api/go2rtc/streams/back` for a single camera). This is the authoritative answer to "what is my camera actually sending?" and is far more reliable than guessing from the camera's web UI. It also shows whether the audio track is `sendonly`/`recvonly`, which matters for [two-way talk](#webrtc-and-two-way-talk).
+
+### 4. Fix the codec with the FFmpeg module
+
+If the camera plays in go2rtc but not in your browser, the video or audio codec is unsupported. Browsers can reliably play **H.264** video and **AAC** audio; many cannot play H.265/HEVC, and some camera audio (G.711/PCM, MJPEG containers, etc.) is not playable at all. The fix is to have go2rtc re-encode the stream on demand using its FFmpeg module.
+
+In the Frigate UI this is the **Use compatibility mode (ffmpeg)** toggle on a stream source; in YAML it is the `ffmpeg:` prefix on the source URL.
+
+
+
+
+1. Navigate to and expand your camera's stream.
+2. On the source you want to convert, click the **Use compatibility mode (ffmpeg)** button (the sliders icon next to the URL). This routes the source through go2rtc's FFmpeg module and reveals the transcoding options.
+3. Set **Video** to **Transcode to H.264** if your browser can't play the camera's video codec (e.g. H.265). Leave it on **Copy** to pass the video through untouched. This is much cheaper and should be your default whenever only the audio needs converting.
+4. Set **Audio** to **Transcode to AAC** (for MSE) or **Transcode to Opus** (for WebRTC) if the camera's audio codec is unsupported. Leave it on **Copy** to keep the original, or **Exclude** to drop audio entirely.
+5. When transcoding **video**, set **Hardware acceleration** to **Automatic (recommended)** so the encode runs on your GPU instead of the CPU. See [hardware-accelerated transcoding](#hardware-accelerated-transcoding-with-ffmpeg-8) for an important FFmpeg 8 caveat.
+6. **Save** the section, then reload the live view.
+
+
+
+
+```yaml
+go2rtc:
+ streams:
+ back:
+ - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
+ # transcode video to H.264 on the GPU; only needed if the browser can't play the source codec
+ - "ffmpeg:back#video=h264#hardware"
+```
+
+To convert audio only (leaving video untouched), or to convert both:
+
+```yaml
+go2rtc:
+ streams:
+ back:
+ - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
+ - "ffmpeg:back#audio=aac" # audio only, preferred when the video already plays
+ # or, to convert both video and audio:
+ # - "ffmpeg:back#video=h264#audio=aac#hardware"
+```
+
+
+
+
+:::warning
+
+The `#`-modifiers (`#video=`, `#audio=`, `#hardware`, `#backchannel=0`, …) **only take effect on a source that is prefixed with `ffmpeg:`**. Adding them to a bare `rtsp://…#audio=opus` source does nothing: go2rtc ignores them. Likewise, when a source references another stream by name (e.g. `ffmpeg:back#audio=aac`), the name must match the stream key **exactly** (it is case sensitive), or the transcode is silently never produced. This is the single most common configuration mistake. In the Frigate UI, the **Use compatibility mode (ffmpeg)** toggle adds the `ffmpeg:` prefix for you.
+
+:::
+
+Transcoding video is resource intensive. Always prefer `#video=copy` (the **Copy** option) and only convert the track that is actually unsupported. If you must transcode video and have no hardware encoder available, the built-in jsmpeg view may be the better option.
+
+## Live view is black, buffering, or stuck in "low-bandwidth mode"
+
+When the live view shows a black screen, spins forever, or repeatedly drops to the lower-quality jsmpeg player ("low-bandwidth mode"), the stream almost always contains something the browser cannot decode over MSE, usually H.265 video or a non-AAC audio track. Confirm this in the go2rtc web UI (port `1984`): if MSE won't play there, Frigate can't play it either, since it uses the same pipeline.
+
+The fix is to produce an **H.264 + AAC** stream, either by changing your camera's firmware codecs or by transcoding in go2rtc (see [Fix the codec with the FFmpeg module](#4-fix-the-codec-with-the-ffmpeg-module)). A few other things worth checking:
+
+- **Set the camera's I-frame (keyframe) interval to match its frame rate** (or "1x" on Reolink), and avoid "smart"/"+" codecs like _H.264+_ or _H.265+_. A long keyframe interval delays the first decodable frame past Frigate's startup timeout, which forces the fallback to jsmpeg. See [camera settings recommendations](/configuration/live#camera-settings-recommendations).
+- **A spinner that never clears, even though video plays in VLC**, is often an unplayable _audio_ track stalling playback. Drop or transcode the audio (see below).
+- **Remote/VPN viewing that buffers** while the LAN is fine is usually latency/jitter exceeding MSE's startup buffer. Set up [WebRTC](/configuration/live#webrtc-extra-configuration), which drops late frames instead of buffering.
+
+The general live-view behavior (smart streaming, the MSE → WebRTC → jsmpeg fallback chain, and how to read browser console errors) is documented in detail in the [Live view FAQ](/configuration/live#live-view-faq).
+
+## H.265 / HEVC cameras
+
+H.265/HEVC playback in the browser is unreliable and version-dependent. WebRTC does not support H.265 on some browsers, and MSE/HEVC support varies by browser, OS, and whether a hardware decoder is present. An H.265 stream that plays fine in VLC, the go2rtc web UI, and Frigate's recordings can still be blank in a live view.
+
+For dependable live viewing, use **H.264** for the stream the live view consumes:
+
+- Point the live view at the camera's H.264 **substream** and keep the H.265 main stream for recording only, or
+- Transcode H.265 → H.264 in go2rtc with the FFmpeg module and `#hardware` (software HEVC transcoding is very CPU heavy).
+
+Treat browser HEVC playback as best-effort. See also [H.265 cameras via Safari](/configuration/camera_specific#h265-cameras-via-safari).
+
+## No audio in Live view
+
+Live view audio has strict codec requirements that differ by player: **MSE requires AAC, PCMA, or PCMU**, and **WebRTC requires Opus, PCMA, or PCMU**. Many cameras default to a codec outside these sets (or to PCM/G.711), so the player loads video only and no audio control appears.
+
+The most robust approach is to provide both an AAC track (for MSE) and an Opus track (for WebRTC) on the same stream by transcoding audio with the FFmpeg module while copying the video:
+
+
+
+
+1. Navigate to and expand the camera's stream.
+2. Add a second **Source** that references the stream by name (e.g. the URL `ffmpeg:back`), enable **Use compatibility mode (ffmpeg)**, and set **Audio** to **Transcode to Opus** for WebRTC support.
+3. Keep the original source as **Source 1** so MSE can use the camera's AAC (or transcode the first source's audio to AAC if the camera doesn't provide it).
+4. **Save** the section.
+
+
+
+
+```yaml
+go2rtc:
+ streams:
+ back:
+ - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 # video + AAC for MSE
+ - "ffmpeg:back#audio=opus" # adds an Opus track for WebRTC
+```
+
+If the camera's native audio isn't AAC either, transcode both:
+
+```yaml
+go2rtc:
+ streams:
+ back:
+ - "ffmpeg:rtsp://user:password@10.0.10.10:554/live0#video=copy#audio=aac" # video copy + AAC for MSE
+ - "ffmpeg:back#audio=opus" # Opus for WebRTC
+```
+
+
+
+
+Setting the camera firmware to AAC (and H.264) avoids transcoding entirely and is always preferable when the camera supports it. For more detail and examples, see [Audio Support](/configuration/live#audio-support).
+
+## WebRTC and two-way talk
+
+WebRTC is only attempted when MSE fails or when using a camera's two-way talk feature; the "All Cameras" dashboard never uses it. When it doesn't work, the cause is almost always one of:
+
+- **Codec mismatch**: WebRTC cannot carry H.265 or AAC. The stream backing the WebRTC view must provide Opus (or PCMA/PCMU) audio and H.264 video. Add an `ffmpeg:back#audio=opus` source as shown above.
+- **Port `8555` not reachable, or no candidates set**: WebRTC needs port `8555` (both TCP and UDP) open and a reachable candidate advertised. On Docker installs running on a custom/overlay network, go2rtc may advertise unreachable container IPs as ICE candidates; setting `webrtc.filters.candidates: []` and supplying only your host's LAN IP resolves this. See [WebRTC extra configuration](/configuration/live#webrtc-extra-configuration).
+- **Two-way talk** additionally requires a secure context (HTTPS or the authenticated port `8971`, because browsers block microphone access on plain HTTP). The camera's RTSP backchannel must also be handled correctly: go2rtc seizes the backchannel by default, which blocks two-way audio for other consumers and can inject static. Disable it on the primary stream with `#backchannel=0` and use a separate dedicated stream for talk, as documented in [preventing go2rtc from blocking two-way audio](/configuration/restream#two-way-talk-restream).
+
+## High CPU usage
+
+If go2rtc is using a lot of CPU, it is almost always transcoding in software. An FFmpeg source with a codec modifier like `#video=h264` or `#audio=aac` but **no** `#hardware` re-encodes on the CPU. (Frigate's `ffmpeg.hwaccel_args` only applies to Frigate's own detect/record processes. It does _not_ accelerate go2rtc's transcodes.)
+
+To keep CPU usage down:
+
+- Only transcode the track that is genuinely unsupported, and use `#video=copy` to pass video through untouched whenever possible.
+- When you must transcode video, always add `#hardware` (the **Automatic** hardware option in the UI) so the encode runs on the GPU. Note the [FFmpeg 8 device requirement](#hardware-accelerated-transcoding-with-ffmpeg-8) below.
+- Don't restream a high-resolution main stream just to feed the live view: even with `#video=copy`, muxing a 4K/8MP+ stream is inherently expensive. Use the camera's lower-resolution substream for live and detect, and let Frigate pull the main stream directly for recording.
+
+## Connection, authentication, and complex passwords
+
+If go2rtc logs `401 Unauthorized` for a URL that works in VLC, the password almost certainly contains reserved URL characters. **Frigate URL-encodes passwords for its own `cameras.ffmpeg.inputs`, but it does not touch what you write under `go2rtc.streams`**: go2rtc parses that URL itself. You must URL-encode special characters yourself in the `go2rtc.streams` section (`@` → `%40`, `#` → `%23`, `?` → `%3F`, `%` → `%25`, etc.).
+
+Note the asymmetry: under `cameras.ffmpeg.inputs` you should use the **raw** password (Frigate encodes it for you). Pre-encoding it there causes a double-encode and fails. See [Handling Complex Passwords](/configuration/restream#handling-complex-passwords).
+
+Repeated `401`/`Connection refused` errors can also mean the camera hit its **concurrent connection limit** or triggered a login lockout. Routing all roles through a single [RTSP restream](/configuration/restream#reduce-connections-to-camera) means the camera only ever sees one connection from go2rtc.
+
+## Stream names must match everywhere
+
+A surprising number of "the better live options aren't available" or `404 Not Found` problems come down to a name mismatch. The same string must be used consistently:
+
+- the **go2rtc stream key** (`go2rtc.streams.`),
+- any `ffmpeg:#…` source that references it,
+- the camera's restream input path (`rtsp://127.0.0.1:8554/`), and
+- the camera name itself (so Frigate auto-maps it for MSE/WebRTC), or an explicit `live -> streams` mapping pointing at the go2rtc stream **name** (never a path).
+
+If you rename or remove a go2rtc stream while experimenting and the live stream selector then shows a blank entry, clear your browser's site data for the Frigate URL. The selected stream is cached per-device in local storage.
+
+## Camera-specific behavior
+
+Several camera brands have well-known quirks with go2rtc. Rather than repeat them here, see the [camera-specific configuration](/configuration/camera_specific) page, which covers them in detail. The highlights:
+
+- **Reolink**: RTSP is unreliable on many models; the **http-flv** stream through the FFmpeg module is recommended, and you must enable HTTP/RTMP in the camera and **reboot** it. 6MP+ models stream H.265 over http-flv-enhanced, which requires FFmpeg 8.0. See [Reolink Cameras](/configuration/camera_specific#reolink-cameras).
+- **TP-Link Tapo**: use go2rtc's native `tapo://` source for stability and two-way audio; a stale RTSP credential can often be revived by clicking play once in the go2rtc web UI.
+- **Ubiquiti/UniFi Protect**: use the `rtspx://` scheme (not `rtsps://…?enableSrtp`).
+- **Amcrest/Dahua**: use the `/cam/realmonitor?channel=1&subtype=N` scheme, where `subtype=0` is the main stream. See [Amcrest & Dahua](/configuration/camera_specific#amcrest--dahua).
+
+## Non-RTSP sources and the FFmpeg module
+
+go2rtc's native zero-copy handling only supports well-formed RTSP H.264/H.265. Anything else (MJPEG, HTTP/HTTP-FLV, RTMP, or unusual codecs) must be handed to the FFmpeg module by prefixing the source with `ffmpeg:`. This is also necessary for some camera streams to be parsed at all, at the cost of slightly slower startup. MJPEG and other non-H.264 sources additionally need `#video=h264` (with `#hardware`) before they can be used for the `record`, `detect`, or restream roles. See [MJPEG Cameras](/configuration/camera_specific#mjpeg-cameras) for a complete example.
+
+## Hardware-accelerated transcoding with FFmpeg 8
+
+Frigate 0.18 ships **FFmpeg 8.0** as the default, and FFmpeg 8 is stricter about hardware-accelerated filtering than earlier versions. Whenever go2rtc transcodes video with hardware acceleration (any source using `#hardware`, `#hardware=vaapi`, or the **Automatic** hardware option in the UI), it builds a filter chain that uploads frames to the GPU with the `hwupload` filter. FFmpeg 8 now refuses to do this unless it is told **which device** to use. Earlier versions selected one automatically. The result is that an otherwise-working transcode fails to start, the live view never loads, and go2rtc logs:
+
+```
+[hwupload] A hardware device reference is required to upload frames to.
+[AVFilterGraph] Error initializing filters
+Error opening output files: Invalid argument
+```
+
+The fix is to tell go2rtc's bundled FFmpeg which hardware device to use via the `go2rtc -> ffmpeg -> global` option. For **VAAPI**-based acceleration (which covers most Intel and AMD GPUs, and is what go2rtc selects automatically on that hardware), point it at your render device:
+
+```yaml
+go2rtc:
+ ffmpeg:
+ global: "-vaapi_device /dev/dri/renderD128"
+ streams:
+ back:
+ - "ffmpeg:rtsp://user:password@10.0.10.10:554/live0#video=h264#hardware"
+```
+
+`/dev/dri/renderD128` is the usual render node; on a system with more than one GPU you may need `renderD129` (or higher), and the device must be passed into the container (e.g. `devices: - /dev/dri:/dev/dri` in Docker Compose).
+
+If you use a **different hardware acceleration backend**, you will likely need to specify its device in the same way, using the option that matches that backend instead of `-vaapi_device`. See the [go2rtc FFmpeg source documentation](https://github.com/AlexxIT/go2rtc/tree/v1.9.14#source-ffmpeg) and the upstream report ([go2rtc issue #1984](https://github.com/AlexxIT/go2rtc/issues/1984)) for background and other examples.
+
+:::tip
+
+If you don't transcode in go2rtc with hardware acceleration, this does not affect you. If you want to avoid the change entirely, you can pin Frigate (and the go2rtc it bundles) back to FFmpeg 7.0 by setting `ffmpeg -> path: "7.0"` in your config.
+
+:::
+
+## Home Assistant and port access
+
+When running Frigate as a Home Assistant App, the go2rtc API (port `1984`), the RTSP restream (port `8554`), and WebRTC (port `8555`) are **disabled and hidden by default**. To use them (for example to reach the go2rtc web interface for troubleshooting, or to open a go2rtc stream externally in an app like VLC), go to , click **Show disabled ports**, enable the port you need, and save. Use the host's IP address rather than an mDNS name like `homeassistant.local`.
+
+If live view works in the Frigate UI but not in Home Assistant, the most common cause is the go2rtc stream name not matching the camera name: name the primary go2rtc stream exactly like the camera, or add a `live -> streams` mapping, so the integration can resolve the restream.
diff --git a/docs/docs/troubleshooting/gpu.md b/docs/docs/troubleshooting/gpu.md
index 6399f92d8b..a39386bfa9 100644
--- a/docs/docs/troubleshooting/gpu.md
+++ b/docs/docs/troubleshooting/gpu.md
@@ -10,4 +10,47 @@ title: GPU Errors
Some users have reported issues using some Intel iGPUs with OpenVINO, where the GPU would not be detected. This error can be caused by various problems, so it is important to ensure the configuration is setup correctly. Some solutions users have noted:
- In some cases users have noted that an HDMI dummy plug was necessary to be plugged into the motherboard's HDMI port.
-- When mixing an Intel iGPU with Nvidia GPU, the devices can be mixed up between `/dev/dri/renderD128` and `/dev/dri/renderD129` so it is important to confirm the correct device, or map the entire `/dev/dri` directory into the Frigate container.
\ No newline at end of file
+- When mixing an Intel iGPU with Nvidia GPU, the devices can be mixed up between `/dev/dri/renderD128` and `/dev/dri/renderD129` so it is important to confirm the correct device, or map the entire `/dev/dri` directory into the Frigate container.
+
+## Intel/AMD GPU
+
+### Hardware acceleration is not being used
+
+For VAAPI or QSV to work, the GPU's render device must be passed through to the Frigate container. Intel and AMD GPUs expose this as a render node under `/dev/dri`, usually `/dev/dri/renderD128`. If it is not passed through, hardware acceleration is unavailable: ffmpeg fails to initialize it (for example `Failed to open the drm device` or `No VA display found for device`) and GPU usage stays at zero while CPU usage remains high.
+
+Pass the render device through when starting the container. With `docker compose`:
+
+```yaml
+services:
+ frigate:
+ devices:
+ - /dev/dri/renderD128:/dev/dri/renderD128 # Intel / AMD GPU, update for your hardware
+```
+
+Or with `docker run`, add `--device /dev/dri/renderD128`. See the [installation docs](/frigate/installation) for a complete example.
+
+If it still isn't working after passing the device through:
+
+- **Confirm the render node exists and is the correct one.** Run `ls /dev/dri` on the host. You should see one or more `renderD12X` entries. Systems with more than one GPU (an Intel iGPU plus a discrete GPU) can expose both `/dev/dri/renderD128` and `/dev/dri/renderD129`, and the numbering is not guaranteed. Pass through the correct node, or map the entire directory (`/dev/dri:/dev/dri`, or `--device /dev/dri`) so all render nodes are available.
+- **Check device permissions.** The Frigate process must be able to access the render node. This is usually automatic when the container runs as root (the default), but nested setups such as an unprivileged Proxmox/LXC container often require making the device accessible on the host (for example, a world-readable render node) or running the container privileged. Note that running Frigate inside an LXC is not officially supported. See the [installation docs](/frigate/installation#proxmox) for details.
+
+### Failed to download frame: -5
+
+When using VAAPI or QSV hardware acceleration, ffmpeg may crash and restart periodically with a signature like this in the `ffmpeg..detect` log:
+
+```
+[AVHWFramesContext @ 0x...] Failed to sync surface ... (operation failed).
+[hwdownload @ 0x...] Failed to download frame: -5.
+[vf#0:0 @ 0x...] Error while filtering: Input/output error
+[vf#0:0 @ 0x...] Task finished with error code: -5 (Input/output error)
+[frigate.video] : Unable to read frames from ffmpeg process.
+```
+
+This is a hardware frame synchronization failure between ffmpeg and the GPU driver, not a Frigate bug. It comes from how a specific camera stream interacts with the GPU's decode and scaling path, so it is highly dependent on your hardware, driver, and stream. Frigate's automatic hardware acceleration detection is a best-guess effort, so the fix is usually to tune the configuration for your specific hardware and camera. The solutions below are ordered from most to least likely to help:
+
+- **Switch between the VAAPI and QSV presets.** On Intel Gen 12 and newer iGPUs, `preset-intel-qsv-h264` / `preset-intel-qsv-h265` is often more stable than the auto-detected `preset-vaapi`. See the [hardware acceleration docs](/configuration/hardware_acceleration_video.md#intel-based-cpus) for the recommended preset for your Intel generation.
+- **Try a different VAAPI driver.** The default driver is `iHD`. On older Intel CPUs, `LIBVA_DRIVER_NAME=i965` can be more stable; on AMD GPUs use `LIBVA_DRIVER_NAME=radeonsi`. See [the hardware acceleration docs](/configuration/hardware_acceleration_video.md#intel-based-cpus) for how to set the driver.
+- **Use a codec that decodes more reliably.** H.265/HEVC streams may trigger this error far more often than H.264 depending on your CPU generation. If your camera exposes a separate sub-stream, assign an H.264 stream to the `detect` role. Cameras that output full-range YUV (for example some Hikvision models) are especially prone to it.
+- **Match the detect resolution to the stream resolution.** When the `detect` resolution differs from the stream, Frigate inserts a GPU scaling filter (`scale_vaapi`), which is where these surface-sync failures can often originate. Set the `detect` `width` and `height` to match the exact resolution of the stream assigned the `detect` role.
+- **Match the detect `fps` to the camera stream.** Aggressively dropping frames (for example `detect` `fps: 1` on a stream that runs at 15 fps) can cause timing mismatches in the GPU's frame buffer. Lower the sub-stream's frame rate on the camera itself instead of dropping most frames in Frigate.
+- **Fall back to software decoding.** If none of the above resolve it, remove the preset for that camera (`hwaccel_args: []`). Hardware decoding is only an optimization. On a capable CPU, software-decoding a low-resolution sub-stream is inexpensive and gives a stable detect pipeline.
diff --git a/docs/docs/troubleshooting/recordings.md b/docs/docs/troubleshooting/recordings.md
index 2425e653a4..c4e8e72b25 100644
--- a/docs/docs/troubleshooting/recordings.md
+++ b/docs/docs/troubleshooting/recordings.md
@@ -3,17 +3,13 @@ id: recordings
title: Recordings Errors
---
-## I have Frigate configured for motion recording only, but it still seems to be recording even with no motion. Why?
+import FaqItem from "@site/src/components/FaqItem";
-You'll want to:
+## Why are my recordings not working? (empty Recordings, "No recordings found for this time")
-- Make sure your camera's timestamp is masked out with a motion mask. Even if there is no motion occurring in your scene, your motion settings may be sensitive enough to count your timestamp as motion.
-- If you have audio detection enabled, keep in mind that audio that is heard above `min_volume` is considered motion.
-- [Tune your motion detection settings](/configuration/motion_detection) either by editing your config file or by using the UI's Motion Tuner.
+If Frigate shows live video but the History view is empty, or you see "No recordings found for this time", the cause is almost always in one of the three categories below. Segments are first written to the RAM cache and are only moved to disk if they match a retention policy _and_ the camera's `record` stream is producing valid, storable video. Work through the categories in order: retention configuration is by far the most common cause.
-## I see the message: WARNING : Unable to keep up with recording segments in cache for camera. Keeping the 5 most recent segments out of 6 and discarding the rest...
-
-This error can be caused by a number of different issues. The first step in troubleshooting is to enable debug logging for recording. This will enable logging showing how long it takes for recordings to be moved from RAM cache to the disk.
+Before diving in, enable debug logging for the recording maintainer so you can see whether segments are being written to disk at all:
```yaml
logger:
@@ -21,31 +17,240 @@ logger:
frigate.record.maintainer: debug
```
-This will include logs like:
+A healthy camera logs lines like `Copied /media/frigate/recordings/{segment_path} in 0.2 seconds`. If you never see these, no segments are reaching disk, which points at the camera/stream or storage sections below.
+
+### Retention configuration issues
+
+
+
+This is the single most common cause. Setting `record.enabled: True` on its own does **not** keep any footage: **continuous recording is disabled by default**, and segments in the cache are only moved to disk if they match a configured retention policy. You must configure at least one of `continuous`, `motion`, `alerts`, or `detections` retention.
+
+To store all video (the most conservative option), configure continuous retention:
+
+```yaml
+record:
+ enabled: True
+ continuous:
+ days: 3 # keep all footage for 3 days
+```
+
+See [Recording](/configuration/record) for the full set of common configurations, including reduced-storage and alerts-only setups.
+
+
+
+
+
+If you only configured `motion`, `alerts`, or `detections` retention (with no `continuous`), Frigate keeps footage selectively based on the retention `mode`:
+
+- **`mode: motion`** (the default) only retains segments that contain motion. If your [motion masks](/configuration/motion_detection) cover the areas where activity happens, or your motion sensitivity is too low, nothing will be retained even though recording is "on".
+- **`mode: active_objects`** only retains segments where a tracked object was actively moving.
+- **`mode: all`** retains every segment in the window.
+
+If you expected continuous footage but only configured motion/event retention, add a `continuous` retention period as shown above. To verify motion is actually being detected, watch the motion boxes in the debug view or the Motion Tuner in the UI.
+
+
+
+
+
+`alerts` and `detections` retention only keep footage that overlaps a tracked object, so they depend on object detection running:
+
+- **Detection must be enabled.** If `detect: enabled: False`, no alerts or detections are ever created, so alert/detection retention keeps nothing. (Continuous and motion retention still work with detection disabled.)
+- **The object must be supported by your model.** If you track an object your model doesn't support (for example `deer` or `license_plate` on the default model), Frigate never detects it and never records for it. Check your logs for warnings such as `... is configured to track ['deer'] objects, which are not supported by the current model` and remove unsupported objects or switch to a model (e.g. [Frigate+](/plus/)) that includes them.
+
+
+
+
+
+Configuration keys change between major versions. The old `clips` config, for example, has not existed for a long time. If you copied a config from an old blog post or video, verify every key against the current [reference config](/configuration/advanced/reference).
+
+
+
+### Camera and stream issues
+
+
+
+Frigate stores recordings in an MP4 container, and some camera audio codecs (most commonly `pcm_alaw`, `pcm_mulaw`, or other G.711 variants) **cannot be placed in an MP4 container**. When this happens, ffmpeg fails to write the segment and no recording is saved, even though the live view works fine. This is a frequent cause on Tapo, TP-Link VIGI, and some Reolink cameras.
+
+Transcode the audio to AAC (or drop it entirely) using the appropriate [ffmpeg preset](/configuration/ffmpeg_presets):
+
+```yaml
+cameras:
+ your_camera:
+ ffmpeg:
+ output_args:
+ record: preset-record-generic-audio-aac # transcode audio to AAC
+ # or preset-record-generic to record with no audio
+```
+
+
+
+
+
+A message like `No new recording segments were created for in the last 120s` means ffmpeg cannot read the `record` stream. To diagnose:
+
+- Confirm a stream is actually assigned the `record` role in your camera's `ffmpeg.inputs`.
+- Open the go2rtc web interface on port `1984` and click each stream to confirm it plays. go2rtc errors such as `wrong response on DESCRIBE` or `start from CONN state` indicate the camera connection is failing.
+- Test the exact RTSP URL (with the correct path, port, and credentials) in VLC or `ffplay`.
+- If you restream through go2rtc, make sure the `record` input path points at the correct go2rtc stream name. Copying a config between cameras without updating the stream name is a common mistake.
+
+
+
+### Storage and mounting issues
+
+
+
+If the recordings volume (`/media/frigate`) points at the wrong location, isn't writable, or a network/encrypted mount failed to mount at boot, Frigate cannot save recordings, or it silently writes to the boot drive and then purges aggressively because the drive appears far smaller than expected.
+
+- Compare the host's real capacity (`df -h`) against what the **Storage** page in the Frigate UI reports. A mismatch (for example Frigate reporting ~220 GB when your storage drive is 4 TB) means the bind mount is resolving to the wrong filesystem.
+- Verify the host path in your Docker `volumes` mapping (`- /your/storage:/media/frigate`) exists and is writable by the container.
+- For a mount that may fail intermittently, protecting the mount point with `chattr +i` on an empty directory forces Frigate to error out (rather than silently writing to the boot drive) when the mount is missing.
+- Check `dmesg` and system logs for filesystem or I/O errors around the time recordings disappeared.
+
+If recordings _are_ being written but the copy is too slow to keep up, see the ["Unable to keep up with recording segments"](#i-see-the-message-warning--unable-to-keep-up-with-recording-segments-in-cache-for-camera-keeping-the-5-most-recent-segments-out-of-6-and-discarding-the-rest) question below.
+
+
+
+## Recordings won't play back
+
+
+
+When a recording refuses to play in the Frigate UI and you see an error like `Failed to play recordings (error 3): PIPELINE_ERROR_DECODE`, the message is coming from **your browser**, not from Frigate. `PIPELINE_ERROR_DECODE` is emitted exclusively by the media pipeline in **Chromium-based browsers** (Chrome, Edge, Brave, Vivaldi, Opera, Arc, and the Android WebView used by many in-app browsers) when the browser cannot decode a video or audio packet in the recording. WebKit browsers (Safari) report the same underlying problem with a different message, usually `Media failed to decode` or `DECODER_ERROR_NOT_SUPPORTED`.
+
+Frigate copies the `record` stream to disk **without re-encoding it**, so the browser must decode exactly what your camera produced, and Chromium's decoder is far stricter about malformed or nonstandard media than VLC or ffmpeg.
+
+:::warning
+
+The same recording playing perfectly in VLC, decoding cleanly with `ffprobe`/`ffmpeg`, or having a valid MP4 container does **not** mean the browser can decode it. VLC and ffmpeg are much more tolerant of codec quirks and damaged packets than a browser's media pipeline, so a "valid" file can still trigger `PIPELINE_ERROR_DECODE`. This is outside of Frigate's control, because Frigate never modifies the recording stream.
+
+:::
+
+#### Step 1: Confirm it is a browser issue
+
+Open the same recording in **Firefox** or **Safari**. Firefox and Safari both use a different media engine and cannot produce `PIPELINE_ERROR_DECODE`, so if playback works there you have confirmed a client-side codec or decoder problem rather than a bad recording. Switching browsers is a workaround, not a fix; the remaining steps address the root cause so that Chromium browsers work too.
+
+#### Step 2: Rule out H.265 / HEVC
+
+Browser support for H.265 (HEVC) is limited and depends on the operating system, GPU, hardware acceleration, and browser version, which makes it the most common cause of this error. Options, in order of reliability:
+
+- **Record H.264 instead.** Configure the camera's `record`/main stream to output H.264, the most compatible codec across all browsers. See [camera settings recommendations](/configuration/live#camera-settings-recommendations).
+- **Transcode to H.264 with go2rtc.** If you must keep HEVC on the camera, have go2rtc re-encode the recording stream. This increases CPU usage; add `#hardware` to use the GPU where available:
+
+ ```yaml
+ go2rtc:
+ streams:
+ your_camera:
+ # transcode video to h264 and audio to aac; #hardware uses the GPU if available
+ - "ffmpeg:rtsp://user:password@CAMERA_IP:554/stream#video=h264#audio=aac#hardware"
+ cameras:
+ your_camera:
+ ffmpeg:
+ inputs:
+ - path: rtsp://127.0.0.1:8554/your_camera
+ input_args: preset-rtsp-restream
+ roles:
+ - record
+ ```
+
+ The `#video=h264` parameter only takes effect with the `ffmpeg:` source module; adding it to a plain `rtsp://` go2rtc source does nothing.
+
+- **Keep HEVC but improve compatibility.** If your browser and OS do support HEVC, set [`apple_compatibility`](/configuration/camera_specific#h265-cameras-via-safari) on the camera. Some players (Safari and other clients) require a specific HEVC stream format that this option corrects:
+
+ ```yaml
+ cameras:
+ your_camera:
+ ffmpeg:
+ apple_compatibility: true
+ ```
+
+ You may also need to enable HEVC and hardware decoding in the browser itself (for example, Chrome's Settings → System → "Use hardware acceleration when available"). HEVC hardware support varies widely by GPU, OS, and browser version.
+
+#### Step 3: Clean up damaged packets from the camera
+
+If the error is **intermittent** (the same recording plays after a page refresh, or fails only after playing for a while), the camera is most likely emitting occasional corrupt or malformed packets. Some camera models are more prone to this than others. Routing the stream through go2rtc's `ffmpeg` module often "cleans up" the stream enough for the browser to decode it, even without changing the codec:
+
+```yaml
+go2rtc:
+ streams:
+ your_camera:
+ - "ffmpeg:rtsp://user:password@CAMERA_IP:554/stream#video=h264#audio=aac"
+```
+
+#### Step 4: Fix incompatible or corrupt audio
+
+Audio is one of the most common culprits, and a decode failure on the audio track fails the whole recording. Make sure the camera outputs **AAC** audio, transcode the audio to AAC with go2rtc (`#audio=aac`), or drop audio entirely. See [Incompatible audio codec](#incompatible-audio-codec-recordings-silently-fail-to-save) for a preset-based way to do this.
+
+#### Step 5: Avoid "smart" / "+" codecs and check the keyframe interval
+
+- Disable any **"Smart Codec"**, **"H.264+"**, or **"H.265+"** feature in the camera. These nonstandard modes drop keyframes and change encoding parameters mid-stream, producing exactly the kind of packets a browser refuses to decode. (They also cause [short recording segments](#segments-are-only-1-second-long).)
+- Set the camera's **I-frame (keyframe) interval equal to the frame rate** (for example `20` for a 20 fps stream). Long keyframe intervals slow the start of playback and make decode errors more likely.
+
+#### Step 6: Consider bitrate and the client hardware
+
+The browser decodes the video locally, so a stream that is too demanding can fail on one device while playing on another:
+
+- A **very high bitrate or resolution** (for example a 4K/8MP HEVC main stream) can overwhelm a low-power tablet, phone, or SBC and stall the decoder. Test the same recording on a desktop; if it plays there, lower the camera's bitrate or record a lower-resolution profile.
+- Errors that name the client's GPU decoder, such as `VaapiVideoDecoder: failed Initialize()ing the frame pool`, indicate a browser hardware-decode problem. Toggling the browser's "Use hardware acceleration" setting (on or off) often resolves these.
+
+
+
+
+
+Frigate copies the `record` stream directly without re-encoding, so playback depends on your browser supporting the camera's codec. H265/HEVC recordings may not be playable in some browsers. If recordings appear as audio-only or a black screen, your camera is likely sending a codec your browser can't decode. Configure the camera to output **H264** for maximum compatibility.
+
+If playback instead fails with an explicit `PIPELINE_ERROR_DECODE` or `Media failed to decode` error, see [Recordings won't play back with "PIPELINE_ERROR_DECODE"](#pipeline-error-decode) above.
+
+
+
+## Recording cache warnings and errors
+
+
+
+If the record stream uses a "Smart Codec"/H.264+ mode or changes encoding parameters mid-stream, corrupt timestamps cause segments to be split far too frequently and fill the cache. This produces the "Too many unprocessed recording segments" warning. See [that question below](#i-see-the-message-warning--too-many-unprocessed-recording-segments-in-cache-for-camera-this-likely-indicates-an-issue-with-the-detect-stream) for the full diagnosis.
+
+
+
+
+
+This warning means the recording maintainer cannot move recording segments from the RAM cache to disk fast enough. When the cache fills up, Frigate discards the oldest segments to avoid running out of memory and crashing, so you lose recorded footage. This is almost always a storage throughput or system resource problem. Work through the steps below to identify which.
+
+#### Step 1: Enable recording debug logging
+
+The first step is to measure how long each segment takes to move from the RAM cache to disk. Enable debug logging for the recording maintainer:
+
+```yaml
+logger:
+ logs:
+ frigate.record.maintainer: debug
+```
+
+This adds log lines showing the copy duration for each segment:
```
DEBUG : Copied /media/frigate/recordings/{segment_path} in 0.2 seconds.
```
-It is important to let this run until the errors begin to happen, to confirm that there is not a slow down in the disk at the time of the error.
+Let this run until the warnings begin to appear, so you can confirm whether the disk is actually slowing down at the moment the error occurs.
-#### Copy Times > 1 second
+#### Step 2: Interpret the copy times
-If the storage is too slow to keep up with the recordings then the maintainer will fall behind and purge the oldest recordings to ensure the cache does not fill up causing a crash. In this case it is important to diagnose why the copy times are slow.
+The copy duration tells you which direction to investigate:
-##### Check RAM, swap, cache utilization, and disk utilization
+- **Consistently longer than ~1 second**: your storage cannot keep up with the incoming recordings. Continue with Steps 3–5 to diagnose the slow storage.
+- **Consistently well under 1 second**: storage is fast enough, and the problem is more likely CPU or resource contention. Skip to Step 6.
-If CPU, RAM, disk throughput, or bus I/O is insufficient, nothing inside frigate will help. It is important to review each aspect of available system resources.
+#### Step 3: Check RAM, swap, cache, and disk utilization
-On linux, some helpful tools/commands in diagnosing would be:
+If CPU, RAM, disk throughput, or bus I/O is insufficient, nothing inside Frigate will help. Review each aspect of available system resources while the warnings are occurring.
-- docker stats
-- htop
-- iotop -o
-- iostat -sxy --human 1 1
-- vmstat 1
+On Linux, some helpful tools/commands for diagnosing this are:
-On modern linux kernels, the system will utilize some swap if enabled. Setting vm.swappiness=1 no longer means that the kernel will only swap in order to avoid OOM. To prevent any swapping inside a container, set allocations memory and memory+swap to be the same and disable swapping by setting the following docker/podman run parameters:
+- `docker stats`
+- `htop`
+- `iotop -o`
+- `iostat -sxy --human 1 1`
+- `vmstat 1`
+
+On modern Linux kernels, the system will use some swap if it is enabled. Setting `vm.swappiness=1` no longer means the kernel will only swap in order to avoid OOM. To prevent any swapping inside the container, set the memory and memory+swap allocations to the same value and disable swapping by setting the following docker/podman run parameters:
**Docker Compose example**
@@ -67,23 +272,25 @@ services:
--memory= --memory-swap= --memory-swappiness=0
```
-NOTE: These are hard-limits for the container, be sure there is enough headroom above what is shown by `docker stats` for your container. It will immediately halt if it hits ``. In general, running all cache and tmp filespace in RAM is preferable to disk I/O where possible.
+NOTE: These are hard limits for the container, so be sure there is enough headroom above what `docker stats` shows for your container. It will immediately halt if it hits ``. In general, keeping all cache and tmp filespace in RAM is preferable to disk I/O where possible.
-##### Check Storage Type
+#### Step 4: Check your storage type
-Mounting a network share is a popular option for storing Recordings, but this can lead to reduced copy times and cause problems. Some users have found that using `NFS` instead of `SMB` considerably decreased the copy times and fixed the issue. It is also important to ensure that the network connection between the device running Frigate and the network share is stable and fast.
+Mounting a network share is a popular option for storing recordings, but it can lead to reduced copy times and cause problems. Some users have found that using `NFS` instead of `SMB` considerably decreased copy times and fixed the issue. It is also important to ensure that the network connection between the device running Frigate and the network share is stable and fast. A saturated or unreliable link will stall copies.
-##### Check mount options
+#### Step 5: Check your mount options
-Some users found that mounting a drive via `fstab` with the `sync` option caused dramatically reduce performance and led to this issue. Using `async` instead greatly reduced copy times.
+Some users found that mounting a drive via `fstab` with the `sync` option dramatically reduced performance and led to this issue. Using `async` instead greatly reduced copy times.
-#### Copy Times < 1 second
+#### Step 6: Rule out CPU load
-If the storage is working quickly then this error may be caused by CPU load on the machine being too high for Frigate to have the resources to keep up. Try temporarily shutting down other services to see if the issue improves.
+If the copy times are consistently under 1 second but you still see the warning, the machine's CPU load is likely too high for Frigate to have the resources to keep up. Try temporarily shutting down other services, and any resource-intensive Frigate features, to see if the issue improves.
-## I see the message: WARNING : Too many unprocessed recording segments in cache for camera. This likely indicates an issue with the detect stream...
+
-This warning means that the detect stream for the affected camera has fallen behind or stopped processing frames. Frigate's recording cache holds segments waiting to be analyzed by the detector — when more than 6 segments pile up without being processed, Frigate discards the oldest ones to prevent the cache from filling up.
+
+
+This warning means that the detect stream for the affected camera has fallen behind or stopped processing frames. Frigate's recording cache holds segments waiting to be analyzed by the detector. When more than 6 segments pile up without being processed, Frigate discards the oldest ones to prevent the cache from filling up.
:::warning
@@ -91,11 +298,11 @@ This error is a **symptom**, not the root cause. The actual cause is always logg
:::
-### Step 1: Get the full logs
+#### Step 1: Get the full logs
-Collect complete Frigate logs from startup through the first occurrence of the error. Look for errors or warnings that appear **before** the "Too many unprocessed" messages begin — that is where the root cause will be found.
+Collect complete Frigate logs from startup through the first occurrence of the error. Look for errors or warnings that appear **before** the "Too many unprocessed" messages begin. That is where the root cause will be found.
-### Step 2: Check the cache directory
+#### Step 2: Check the cache directory
Exec into the Frigate container and inspect the recording cache:
@@ -105,7 +312,7 @@ docker exec -it frigate ls -la /tmp/cache
Each camera should have a small number of `.mp4` segment files. If one camera has significantly more files than others, that camera is the source of the problem. A problem with a single camera can cascade and cause all cameras to show this error.
-### Step 3: Verify segment duration
+#### Step 3: Verify segment duration
Recording segments should be approximately 10 seconds long. Run `ffprobe` on segments in the cache to check:
@@ -117,20 +324,26 @@ If segments are only ~1 second instead of ~10 seconds, the camera is sending cor
**Common causes of short segments:**
-- **"Smart Codec" or "Smart+" enabled on the camera** — These features dynamically change encoding parameters mid-stream, which corrupts timestamps. Disable them in your camera's settings.
-- **Changing codec, bitrate, or resolution mid-stream** — Any encoding changes during an active stream can cause unpredictable segment splitting.
-- **Camera firmware bugs** — Check for firmware updates from your camera manufacturer.
+- **"Smart Codec" or "Smart+" enabled on the camera**: These features dynamically change encoding parameters mid-stream, which corrupts timestamps. Disable them in your camera's settings.
+- **Changing codec, bitrate, or resolution mid-stream**: Any encoding changes during an active stream can cause unpredictable segment splitting.
+- **Camera firmware bugs**: Check for firmware updates from your camera manufacturer.
-### Step 4: Check for a stuck detector
+:::tip
+
+You don't have to run `ffprobe` by hand to catch this. Open a camera's **Camera Probe Info** dialog (the info icon on the System → Metrics → Cameras page) and check the **Keyframe analysis** section. It probes the record stream and flags sparse or variable keyframes, which is what smart/"+" codecs (H.264+/H.265+) and long keyframe intervals produce.
+
+:::
+
+#### Step 4: Check for a stuck detector
If the detect stream is not processing frames, segments will accumulate. Common causes:
-- **Detection resolution too high** — Use a substream for detection, not the full resolution main stream.
-- **Detection FPS too high** — 5 fps is the recommended maximum for detection.
-- **Model too large** — Use smaller model variants (e.g., YOLO `s` or `t` size, not `e` or `x`). Use 320x320 input size rather than 640x640 unless you have a powerful dedicated detector.
-- **Virtualization** — Running Frigate in a VM (especially Proxmox) can cause the detector to hang or stall. This is a known issue with GPU/TPU passthrough in virtualized environments and is not something Frigate can fix. Running Frigate in Docker on bare metal is recommended.
+- **Detection resolution too high**: Use a substream for detection, not the full resolution main stream.
+- **Detection FPS too high**: 5 fps is the recommended maximum for detection.
+- **Model too large**: Use smaller model variants (e.g., YOLO `s` or `t` size, not `e` or `x`). Use 320x320 input size rather than 640x640 unless you have a powerful dedicated detector.
+- **Virtualization**: Running Frigate in a VM (especially Proxmox) can cause the detector to hang or stall. This is a known issue with GPU/TPU passthrough in virtualized environments and is not something Frigate can fix. Running Frigate in Docker on bare metal is recommended.
-### Step 5: Check for GPU hangs
+#### Step 5: Check for GPU hangs
On the host machine, check `dmesg` for GPU-related errors:
@@ -140,25 +353,78 @@ dmesg | grep -i -E "gpu|drm|reset|hang"
Messages like `trying reset from guc_exec_queue_timedout_job` or similar GPU reset/hang messages indicate a driver or hardware issue. Ensure your kernel and GPU drivers (especially Intel) are up to date.
-### Step 6: Verify hardware acceleration configuration
+#### Step 6: Verify hardware acceleration configuration
An incorrect `hwaccel_args` preset can cause ffmpeg to fail silently or consume excessive CPU, starving the detector of resources.
- After upgrading Frigate, verify your preset matches your hardware (e.g., `preset-intel-qsv-h264` instead of the deprecated `preset-vaapi`).
- For h265 cameras, use the corresponding h265 preset (e.g., `preset-intel-qsv-h265`).
-- Note that `hwaccel_args` are only relevant for the detect stream — Frigate does not decode the record stream.
+- Note that `hwaccel_args` are only relevant for the detect stream. Frigate does not decode the record stream.
-### Step 7: Verify go2rtc stream configuration
+#### Step 7: Verify go2rtc stream configuration
Ensure that the ffmpeg source names in your go2rtc configuration match the correct camera stream. A misconfigured stream name (e.g., copying a config from one camera to another without updating the stream reference) will cause the wrong stream to be used or the stream to fail entirely.
-### Step 8: Check system resources
+#### Step 8: Check system resources
If none of the above apply, the issue may be a general resource constraint. Monitor the following on your host:
-- **CPU usage** — An overloaded CPU can prevent the detector from keeping up.
-- **RAM and swap** — Excessive swapping dramatically slows all I/O operations.
-- **Disk I/O** — Use `iotop` or `iostat` to check for saturation.
-- **Storage space** — Verify you have free space on the Frigate storage volume (check the Storage page in the Frigate UI).
+- **CPU usage**: An overloaded CPU can prevent the detector from keeping up.
+- **RAM and swap**: Excessive swapping dramatically slows all I/O operations.
+- **Disk I/O**: Use `iotop` or `iostat` to check for saturation.
+- **Storage space**: Verify you have free space on the Frigate storage volume (check the Storage page in the Frigate UI).
Try temporarily disabling resource-intensive features like `genai` and `face_recognition` to see if the issue resolves. This can help isolate whether the detector is being starved of resources.
+
+
+
+
+
+This message means the recording maintainer hit an error while moving segments from the cache to disk. It is a **generic wrapper**: the actual cause is always logged on the **very next line**. Frigate usually recovers and keeps running, but any affected segments are lost, so it is worth resolving.
+
+:::warning
+
+Always read the line immediately following this message. `Error occurred when attempting to maintain recording cache` on its own tells you nothing; the exception on the next line (for example `[Errno 28] No space left on device` or `[Errno 17] File exists`) is the real problem.
+
+:::
+
+Because these are operating-system-level errors, they must be resolved on the **host**, not within Frigate's configuration. The most common underlying errors are below.
+
+#### [Errno 28] No space left on device
+
+The filesystem Frigate is writing to is full. Things to check:
+
+- **The recordings volume is genuinely full.** Check free space on the host with `df -h` for the path mapped to `/media/frigate`, and review the **Storage** page in the Frigate UI.
+- **The disk shows free space but is still "full".** This usually means the filesystem has run out of **inodes** (check with `df -i`), or recordings are landing on a different, smaller filesystem than you expect because of an incorrect bind mount. See [The storage volume isn't mounted correctly](#the-storage-volume-isnt-mounted-correctly) above.
+- **`/tmp/cache` is full.** If you mounted `/tmp/cache` as a small `tmpfs`, a backlog of segments can fill it. Increase the tmpfs size, or address whatever is causing segments to pile up (see the [Too many unprocessed recording segments](#i-see-the-message-warning--too-many-unprocessed-recording-segments-in-cache-for-camera-this-likely-indicates-an-issue-with-the-detect-stream) question above).
+- **The host blocks writes before Frigate can purge.** On some systems (for example Unraid with a fill-up threshold), the host stops writes before Frigate's emergency cleanup can run. Leave more headroom on the volume, or lower your retention so Frigate purges sooner.
+
+#### [Errno 17] File exists (with ffmpeg "Error writing trailer" or "unable to re-open output file")
+
+Errors like `[Errno 17] File exists: '/media/frigate/recordings/.../'`, often alongside ffmpeg errors such as `Unable to re-open ... output file for shifting data` or `Error writing trailer: No such file or directory`, are a hallmark of an **unreliable network share** (NFS or SMB). The mount is dropping, serving stale directory entries, or mishandling file locking.
+
+- Confirm the network connection to the NAS is stable and fast. An intermittent link produces these errors sporadically.
+- Prefer **NFS over SMB** for the recordings mount; several users have found NFS more reliable and faster.
+- Review your `fstab`/mount options for settings that hurt consistency or performance (see the `sync` vs `async` note in the [Unable to keep up with recording segments](#i-see-the-message-warning--unable-to-keep-up-with-recording-segments-in-cache-for-camera-keeping-the-5-most-recent-segments-out-of-6-and-discarding-the-rest) question above).
+- Enable `frigate.record.maintainer` debug logging to confirm whether the errors line up with the share becoming unavailable.
+
+#### Errors referencing a camera you manually renamed or removed
+
+If the next-line error references a camera name that no longer exists in your config, orphaned data is left over from a rename or removal in a persistent `/tmp/cache` volume.
+
+- Using a `tmpfs` mount for `/tmp/cache` as recommended in the [installation docs](/frigate/installation#storage) prevents stale cache files under the old camera name from surviving a restart, which avoids this issue entirely.
+- If errors persist, stop Frigate and remove any leftover segments for the old camera name from `/tmp/cache`.
+
+
+
+## Other recording questions
+
+
+
+You'll want to:
+
+- Make sure your camera's timestamp is masked out with a motion mask. Even if there is no motion occurring in your scene, your motion settings may be sensitive enough to count your timestamp as motion.
+- If you have audio detection enabled, keep in mind that audio that is heard above `min_volume` is considered motion.
+- [Tune your motion detection settings](/configuration/motion_detection) either by editing your config file or by using the UI's Motion Tuner.
+
+
diff --git a/docs/docs/usage/explore.md b/docs/docs/usage/explore.md
new file mode 100644
index 0000000000..49cdc82023
--- /dev/null
+++ b/docs/docs/usage/explore.md
@@ -0,0 +1,101 @@
+---
+id: explore
+title: Explore
+---
+
+import NavPath from "@site/src/components/NavPath";
+
+**Explore** is where you browse and search every **tracked object** Frigate has saved. By default it groups recent objects by label; when [Semantic Search](/configuration/semantic_search) is enabled, you can also search by natural-language description or visual similarity. Selecting any object opens a detail pane with its snapshot, lifecycle, and metadata.
+
+This page describes how to _use_ the Explore view. For how the underlying features are _configured_, see [Semantic Search](/configuration/semantic_search) and [Generative AI descriptions](/configuration/genai/genai_objects).
+
+:::tip
+
+If you just want to quickly see what happened on your cameras, it's recommended to use [Review](/usage/review) rather than Explore. Review groups overlapping and adjacent activity on a camera into **review items** and sorts them into Alerts, Detections, and Motion, so you can scan and play back footage in a few clicks instead of sifting through individual objects. Reach for Explore when you need to find a _specific_ tracked object after the fact: by label, time, zone, or description.
+
+:::
+
+## Browsing tracked objects
+
+The default view shows your most recent tracked objects grouped into rows by label (_Person_, _Car_, _Dog_, and so on), each row labeled with the object type and a count. The arrow at the end of a row opens the full, filterable grid for that label.
+
+Clicking a thumbnail opens its [detail dialog](#tracked-object-details); right-clicking or long-pressing a thumbnail opens an [actions menu](#actions-and-bulk-selection). You can switch to a denser grid layout and adjust the number of columns from the view's settings.
+
+## Searching
+
+When [Semantic Search](/configuration/semantic_search) is enabled, a search bar appears that combines two things in one input:
+
+- **Natural-language search**: type a free-text query and press Enter to run a semantic search over your tracked objects.
+- **Filter tokens**: type a `key:` to get suggestions, then a value, to add a structured filter. Each filter becomes a removable chip, and you can chain several together.
+
+You can save a search with the star icon and reload it later, and clear everything with the clear-search icon. A help popover explains the token syntax, for example:
+
+```
+cameras:front_door label:person before:01012024 time_range:3:00PM-4:00PM
+```
+
+### Filter reference
+
+The most common filter tokens are:
+
+| Filter | Description |
+| ---------------------------- | ---------------------------------------------------------------------------------- |
+| **Cameras** | Limit to one or more cameras. |
+| **Labels** | Object labels (person, car, etc.). |
+| **Sub Labels** | Recognized sub labels (e.g. a recognized face or name). |
+| **Attributes** | Classification attributes applied to the object. |
+| **Recognized License Plate** | Match a recognized plate. |
+| **Zones** | Objects that entered specific zones. |
+| **Before / After** | Restrict to a date range. |
+| **Time Range** | Restrict to a time of day (`HH:MM-HH:MM`). |
+| **Min / Max Score** | Restrict by the object's confidence score. |
+| **Min / Max Speed** | Restrict by estimated speed (when speed estimation is configured). |
+| **Has Snapshot / Has Clip** | Only objects that saved a snapshot or recording. |
+| **Submitted to Frigate+** | Only objects already submitted (when Frigate+ is enabled). |
+| **Search Type** | Whether semantic search matches the object's **Thumbnail** or its **Description**. |
+
+### Sorting
+
+When a filter or search is active, a **Sort** control lets you order results by **date**, **object score**, or **estimated speed** (ascending or descending). When a semantic query or similarity search is active, results can also be ordered by **relevance**.
+
+### Thumbnail and description search
+
+- The **Search Type** setting controls whether a text query is matched against each object's **thumbnail** or its **description**. Each result indicates which one it matched and the confidence.
+
+Natural-language search, thumbnail search, and description search all require [Semantic Search](/configuration/semantic_search) to be enabled.
+
+## Tracked Object Details
+
+Selecting an object opens the **Tracked Object Details** dialog. Use the arrows (or the left/right keys) to step to the previous or next object. The dialog has two tabs:
+
+- **Snapshot** or **Thumbnail**: the saved snapshot (or thumbnail).
+- **Tracking Details**: the object's lifecycle, available when the object has a recording. It lists each significant moment (detected, entered a zone, became active or stationary, left, and so on); clicking a moment plays that part of the recording with the bounding box overlaid. A settings popover lets you show all zones and adjust the annotation offset.
+
+The details pane shows the object's **label**, **scores**, **camera**, **timestamp**, estimated **speed**, any **recognized license plate** and **classification attributes**, and its **description**. Admins can edit the sub label, license plate, and attributes inline.
+
+The **description** can be edited by hand, and, when [Generative AI descriptions](/configuration/genai/genai_objects) are enabled and the object's lifecycle has ended, regenerated from the snapshot or from thumbnails. For `speech` objects, a **Transcribe** action is available when audio transcription is enabled. When [Frigate+](/integrations/plus) is enabled, admins can submit a snapshot to improve their model directly from this pane.
+
+## Actions and bulk selection
+
+Right-clicking or long-pressing an object (in the grid or its thumbnail) opens an actions menu with options to **download** the video, snapshot, or a clean snapshot; **view tracking details**; **find similar**; **add a trigger**; **view in History**; and **delete the tracked object**.
+
+:::note
+
+Deleting a tracked object removes its snapshot, embeddings, and tracking-details entries, but the recorded footage of that object in [History](/usage/history) is **not** deleted.
+
+:::
+
+To act on many objects at once, Ctrl/Cmd-click or right-click to start a selection (selected tiles gain a blue ring), then use the toolbar to select all, clear the selection, or delete (admins).
+
+## Semantic Search - Usage and best practices {#usage-and-best-practices}
+
+1. Semantic Search is used in conjunction with the other filters available on the Explore page. Use a combination of traditional filtering and Semantic Search for the best results.
+2. Use the thumbnail search type when searching for particular objects in the scene. Use the description search type when attempting to discern the intent of your object.
+3. Because of how the AI models Frigate uses have been trained, the comparison between text and image embedding distances generally means that with multi-modal (`thumbnail` and `description`) searches, results matching `description` will appear first, even if a `thumbnail` embedding may be a better match. Play with the "Search Type" setting to help find what you are looking for. Note that if you are generating descriptions for specific objects or zones only, this may cause search results to prioritize the objects with descriptions even if the ones without them are more relevant.
+4. Make your search language and tone closely match exactly what you're looking for. If you are using thumbnail search, **phrase your query as an image caption**. Searching for "red car" may not work as well as "red sedan driving down a residential street on a sunny day".
+5. Semantic search on thumbnails tends to return better results when matching large subjects that take up most of the frame. Small things like "cat" tend to not work well.
+6. Experiment! Find a tracked object you want to test and start typing keywords and phrases to see what works for you.
+
+## Triggers
+
+From an object's actions menu, **Add trigger** sets up a per-camera trigger that uses Semantic Search to automate an action (a notification, sub label, or attribute) whenever a similar object appears. Triggers require Semantic Search and are managed under . See [Triggers](/configuration/semantic_search#triggers) for full configuration and best practices.
diff --git a/docs/docs/usage/exports.md b/docs/docs/usage/exports.md
new file mode 100644
index 0000000000..a593374b35
--- /dev/null
+++ b/docs/docs/usage/exports.md
@@ -0,0 +1,43 @@
+---
+id: exports
+title: Exports
+---
+
+**Exports** are how you keep a specific piece of footage permanently.
+
+Frigate's recordings are governed by your [retention settings](/configuration/record): once footage ages past its retention window (or, depending on your configuration, once it is only kept where motion, alerts, or detections occurred), it is deleted to free up disk space. An **export** saves a copy of a chosen time range to a separate location that is **never removed by retention**, so it stays available until you delete it yourself.
+
+This is the answer to the common question _"how do I stop Frigate from deleting an important clip?"_ Instead of increasing retention for an entire camera (which uses far more storage to protect a single moment), export just the footage you want to keep.
+
+:::tip
+
+Exports are stored under `/media/frigate/exports`, separate from your recordings, and are not counted against or removed by recording retention. They remain on disk until you delete them, so be aware that they accumulate over time.
+
+:::
+
+## Creating an export
+
+There are a few ways to create an export:
+
+- **From Review**: select (right click or long-press) an individual review item directly, and choose Export from the header menu. You can also select multiple review items and export them all at once, optionally grouping them into a [case](#cases).
+- **From History**: open the **Actions** menu and choose **Export**. You can export a preset duration (the last 1, 4, 8, 12, or 24 hours), enter a custom start and end time, or select a range directly on the timeline. A **multi-camera** option lets you export the same time range across several cameras at once.
+
+In every case you can give the export a name. Frigate then saves the footage from your recordings as a single video file. Larger ranges take time to process; the export is marked _in progress_ until it finishes, and you can keep using Frigate while it runs.
+
+## Managing exports
+
+All of your exports live on the **Exports** page, reachable from the main navigation, where you can search for one by name. Each export offers the following actions:
+
+- **Play** it in the browser,
+- **Download** it to save the footage outside of Frigate,
+- **Share** it: copies a direct link to the export (or uses your device's share sheet),
+- **Rename** it, and
+- **Delete** it: deleting is the only way an export is removed.
+
+You can also select multiple exports at once to **delete** them in bulk, or to **add them to** (or **remove them from**) a [case](#cases).
+
+## Cases
+
+A **case** groups related exports together: for example, all the clips from a single incident across multiple cameras. On the **Exports** page you can create a case with a name and description, add existing exports to it (or create a new case while exporting), and **download the entire case as a single archive** to hand off as one package.
+
+Exports that don't belong to a case appear under **Uncategorized Exports**. Deleting a case lets you either keep its exports (they move back to uncategorized) or delete them along with the case.
diff --git a/docs/docs/usage/history.md b/docs/docs/usage/history.md
new file mode 100644
index 0000000000..5ed99f440a
--- /dev/null
+++ b/docs/docs/usage/history.md
@@ -0,0 +1,75 @@
+---
+id: history
+title: History
+---
+
+import NavPath from "@site/src/components/NavPath";
+
+**History** is Frigate's full-resolution recording viewer. Unlike Live, Review, and Explore, there is no menu item for it. You reach it from within another view, then scrub the timeline, switch cameras, inspect a tracked object's lifecycle, and export or share any moment.
+
+This page describes how to _use_ the History view. For how recordings are _configured_ (retention, pre/post capture), see [Recording](/configuration/record).
+
+## Opening History
+
+You can open History from several places:
+
+- **From [Review](/usage/review):** clicking a review item opens its recording, scrubbed to just before the activity on that camera.
+- **From [Live](/usage/live):** the **History** button in a camera's single-camera view opens that camera about 30 seconds in the past.
+- **From a share link:** opening a shared timestamp link (see [Share Timestamp](#the-actions-menu) below) jumps straight to that camera and moment.
+
+Use the **Back** button to return where you came from, or the **Live** button to jump to the current camera's live view.
+
+:::tip
+
+If you see **"No recordings found for this time"**, the most common causes are: recording was not enabled for that camera at the time of the event; the retention window has since expired and those segments were removed; or storage ran low and Frigate deleted them early to free space. See [Recording](/configuration/record) to verify your retention settings.
+
+:::
+
+## Timeline, Events, and Detail
+
+A toggle (a drawer on mobile) switches the side panel between three modes:
+
+- **Timeline**: a scrubbable vertical timeline of the selected camera. Horizontal lines down the center represent motion, with longer lines indicating more motion at that moment. Review items are marked as shaded areas (**red** for alerts, **orange** for detections), and sections with no colored background are times when no recording exists.
+- **Events**: a scrollable list of the camera's review items for the time range; clicking one seeks the player to it.
+- **Detail**: the [tracking details inspector](#the-detail-view) for the objects in view.
+
+While you are selecting a range to export, the panel temporarily switches to Timeline.
+
+## Scrubbing and previews
+
+Drag the timeline handlebar to move through time; the main player and any secondary camera previews scrub together so everything stays in sync. Press the zoom buttons on the timeline to change its zoom level (from coarse to fine segments). Sections of the timeline with no recordings are shown as gaps.
+
+On desktop, when more than one camera is available, a **row of secondary previews** shows the other cameras at the same moment. Clicking one of them makes it the main camera at the current timestamp, so you can follow activity across cameras without losing your place. On mobile, use the camera drawer to switch cameras.
+
+## Filtering and the calendar
+
+You can filter History by **cameras** and **date**. The calendar behaves the same as it does in [Review](/usage/review#filtering-and-the-calendar): an **underline** under a day means recordings exist for that day, and a **colored dot** (red for unreviewed alerts, orange for unreviewed detections) marks days with unreviewed activity.
+
+## The Detail view
+
+The **Detail** mode turns the side panel into a tracking details inspector. It lists one card per review item, each showing the item's severity, start time, the object labels involved, a count of tracked objects, and the duration. The active card is highlighted as the video plays, and clicking a card seeks to it.
+
+Expanding a card reveals the **lifecycle** of each tracked object: a row for each significant moment (detected, entered a zone, became active, became stationary, left, and so on), with a progress line that follows the current playback position. Hovering a row shows that moment's score, ratio, and area, and clicking a row seeks the video to that exact timestamp.
+
+The **Detail View Settings** at the bottom let you toggle whether the active item's objects expand automatically, and adjust the **annotation offset**: a fine timing correction that aligns the bounding-box overlays with the recorded video when your camera's snapshot and recording timestamps drift. Admins can save the offset to the camera's configuration.
+
+## The Actions menu
+
+On desktop, the **Actions** menu (the film icon) collects the things you can do with the footage you are viewing:
+
+- **Export**: save a clip of a chosen time range so it is never removed by retention. The dialog pre-selects the last hour; adjust the range or drag the timeline handles, then export. See [Exports](/usage/exports) for managing and downloading exports.
+- **Share Timestamp**: generate a link to the current moment (or a custom timestamp) to share with another Frigate user. This is an internal link, not a public share URL.
+- **Motion Search**: scan this camera's recordings for changes in a region you draw. This is the same tool documented under [Reviewing Motion](/usage/review#motion-search).
+- **Debug Replay** (admins): replay a recorded range back through Frigate's detection pipeline to see how it would be processed.
+
+You can also capture an instant snapshot of the current frame, and submit a frame to [Frigate+](/integrations/plus) directly from the player (admins only).
+
+## AI review summaries
+
+When [Generative AI review](/configuration/genai/genai_review) is configured, Frigate can generate a title, description, and threat classification for review items and surface them as you scrub through History. A review item that has an AI summary exposes its details in a few places:
+
+- **Over the video**: when the item is on screen, a popup appears over the player.
+- **In the Events side panel**: items with a summary show the title below the thumbnail.
+- **In the Detail side panel**: the item's card shows the title alongside its tracking details.
+
+Clicking any of these opens the **AI Analysis** dialog with the generated detail and any flagged concerns for that item.
diff --git a/docs/docs/usage/live.md b/docs/docs/usage/live.md
new file mode 100644
index 0000000000..9366add9de
--- /dev/null
+++ b/docs/docs/usage/live.md
@@ -0,0 +1,118 @@
+---
+id: live
+title: Live View
+---
+
+import NavPath from "@site/src/components/NavPath";
+
+**Live view** is Frigate's real-time dashboard and the page you land on by default. It shows all of your cameras at a glance, streams your most recent alerts across the top, and lets you open any camera in a full-resolution single-camera view with audio, two-way talk, PTZ, and on-demand recording controls.
+
+This page describes how to _use_ the Live view. For how to _configure_ live streaming (go2rtc, stream selection, smart streaming, WebRTC, and audio), see the [Live View configuration](/configuration/live) docs.
+
+## The dashboard at a glance
+
+The default **All Cameras** dashboard shows every camera, with a filmstrip of recent **alerts** scrolling across the top. Clicking an alert opens it in [Review](/usage/review); each card also has a check button to mark it reviewed without leaving the dashboard. Only **alerts** appear in the filmstrip. To suppress a label or zone from showing there, configure it as a detection instead (see [Alerts and Detections](/configuration/review#alerts-and-detections)).
+
+By default Frigate uses **smart streaming**: a camera's image updates roughly once per minute while nothing is happening, and switches to a full live stream the moment activity is detected. This conserves bandwidth and resources. You can change this for each camera when using a camera group (see [Streaming settings](#streaming-settings-and-the-right-click-menu) below), and the behavior is explained in detail under [Live view technologies](/configuration/live#live-view-technologies).
+
+On mobile, a toggle in the header switches between a **grid** layout and a single-column **list** layout. On desktop a **fullscreen** button is available in the lower-right corner.
+
+## Switching dashboards and camera groups
+
+The icon rail (top-left on desktop, a horizontal strip on mobile) switches between dashboards:
+
+- The **home** icon is the **All Cameras** dashboard, which shows every camera enabled for the dashboard.
+- Each **camera group** you create appears as its own icon. Selecting a group shows only that group's cameras.
+
+Camera groups are useful for organizing cameras by location (for example, _Front of House_ or _Backyard_) and for giving each group its own dashboard layout and camera streaming preferences.
+
+You can also view [Birdseye](/configuration/birdseye) on the dashboard, or open it directly at `http://:5000/#birdseye`. Clicking a camera inside the Birdseye view jumps to that camera's live feed.
+
+## Creating and editing camera groups
+
+Admins can manage groups from the pencil icon next to the group rail, which opens the **Camera Groups** dialog. From there you can add a group, or edit and delete existing ones. When creating a group you choose:
+
+- a **Name** (spaces are converted to underscores),
+- the **cameras** to include (each camera has a toggle and a gear that opens its [streaming settings](#streaming-settings-and-the-right-click-menu)), and
+- an **icon** used for the group's button in the rail.
+
+Deleting a group also clears any custom layout you saved for it.
+
+## Rearranging a camera group layout
+
+On desktop and tablet, each camera group has its own freely-arrangeable grid. Enter **Edit Layout** mode from the layout button in the lower-right corner: camera tiles gain a drag handle and corner resize handles. Drag a tile to reposition it and drag a corner to resize it (the aspect ratio is preserved). Exit edit mode to save. The layout is stored in your browser per device, so each device can have its own arrangement.
+
+The default **All Cameras** dashboard is not manually arrangeable. It automatically sizes tiles based on each camera's aspect ratio (wide cameras span two columns, tall cameras span two rows).
+
+## Reading the tile indicators
+
+Each camera tile surfaces its current state with a few overlays:
+
+- A **pulsing red dot** in the corner means **motion is currently detected** on that camera.
+- A **red outline** around the tile means an **active tracked object** is on that camera.
+- A small **label chip** lists the object types currently detected (for example, _Person_, _Car_).
+- A **camera-name label** appears when you have enabled always-on camera names, or when a camera is offline or disabled.
+- A **Stream Offline** or **Camera is off** placeholder appears when no frames are being received or the camera has been turned off.
+
+You can optionally overlay live streaming statistics (stream type, bandwidth, latency, and frame counts) on a tile to diagnose playback issues.
+
+## Streaming settings and the right-click menu
+
+Right-clicking (or long-pressing) a camera tile opens a context menu with quick controls: an **audio volume** control for streams that support audio, **Mute / Unmute all cameras**, **show or hide streaming statistics**, the **debug view**, **notification** options, and, for admins, turning the camera on or off. If the audio control doesn't appear, see [Audio Support](/configuration/live#audio-support). Audio requires go2rtc configured with a compatible codec.
+
+A **Low-bandwidth mode** notice may also appear in the context menu with a **Reset** option when Frigate has fallen back to the lower-quality jsmpeg stream. See the [Live view FAQ](/configuration/live#live-view-faq) for why this happens.
+
+For non-default groups, the context menu also exposes **Streaming Settings** for that camera, which let you choose:
+
+- the **stream** to display (the dropdown lists the streams you configured under [`live -> streams`](/configuration/live#setting-streams-for-live-ui), and indicates whether audio is available),
+- the **streaming method**: **No Streaming**, **Smart Streaming** (recommended), or **Continuous Streaming** (higher bandwidth), and
+- **compatibility mode**, for devices that have trouble rendering the default player.
+
+These settings are saved per group and per device in your browser, not in your config file.
+
+## The single-camera view
+
+Clicking a camera tile opens its full-resolution single-camera view. The top bar provides:
+
+- **Back** (also the `Esc` key) to return to the dashboard,
+- **History** to jump to the [recordings](/usage/history) for this camera, starting about 30 seconds in the past,
+- **Fullscreen** and **Picture-in-Picture** (if supported by your browser),
+- **Two-way talk** (the microphone button, which requires a supported camera and WebRTC; keyboard shortcut `t`), and
+- **Camera audio muting** (the speaker button; keyboard shortcut `m`).
+
+You can pinch or scroll to zoom into the feed. A **settings** gear provides a **stream** selector (with audio and two-way-talk availability indicators), **Play in background**, **Show stats**, and a **Debug view** that overlays Frigate's detection regions and bounding boxes.
+
+:::tip
+
+Two-way talk and camera audio have specific codec and port requirements. See [Audio Support](/configuration/live#audio-support) and [WebRTC](/configuration/live#webrtc-extra-configuration) for setup details.
+
+:::
+
+## Camera controls
+
+Admins get a row of toggles in the single-camera view (a settings drawer on mobile) to turn camera features on and off in real time:
+
+- **Camera** on/off,
+- **Object detection**,
+- **Recording** (only available when recording is enabled in the camera's config),
+- **Snapshots**,
+- **Audio detection**,
+- **Live audio transcription** (when audio detection is enabled), and
+- **Autotracking** (for [autotracking-capable PTZ cameras](/configuration/autotracking)).
+
+These toggles change runtime behavior immediately. Whether a change persists across a restart depends on the feature. See the relevant configuration page.
+
+## On-demand recording and snapshots
+
+The single-camera view can capture footage on demand:
+
+- **Start on-demand recording** begins a manual recording based on the camera's recording retention settings (the button pulses while active). If recording is disabled for the camera, only a snapshot is saved. Use **End on-demand recording** to stop.
+- **Download instant snapshot** saves a still image of the current frame.
+
+See [Recording](/configuration/record) and [Snapshots](/configuration/snapshots) for how retention is configured, and [Exports](/usage/exports) for keeping a clip permanently.
+
+## PTZ controls
+
+For ONVIF cameras that support it, a control panel provides pan/tilt arrows, **zoom**, **focus**, and saved **presets**. You can also enable a **click-to-move / drag-to-zoom** overlay: click a point in the frame to center the camera there, or drag a box to pan and zoom to that area (dragging top-left to bottom-right zooms in, the reverse zooms out).
+
+For continuous, automatic tracking of a moving object, see [Autotracking](/configuration/autotracking).
diff --git a/docs/docs/usage/review.md b/docs/docs/usage/review.md
new file mode 100644
index 0000000000..6416b500e1
--- /dev/null
+++ b/docs/docs/usage/review.md
@@ -0,0 +1,140 @@
+---
+id: review
+title: Review
+---
+
+import NavPath from "@site/src/components/NavPath";
+
+**Review** is where you triage what happened on your cameras. It groups activity into **review items**, segments of time on a single camera that bundle together the objects and audio that were active at once, and sorts them into **Alerts**, **Detections**, and **Motion**. From here you can scrub through activity, mark items as reviewed, filter, export, and jump to the full recording in [History](/usage/history).
+
+This page describes how to _use_ the Review view. For how alerts and detections are _configured_ (labels, zones, required zones, retention), see the [Review configuration](/configuration/review) docs.
+
+:::info
+
+Review items are only created for a camera when **object tracking and recording are enabled** for that camera. See [Recording](/configuration/record).
+
+:::
+
+## Alerts, Detections, and Motion
+
+Not every segment of video captured by Frigate is of the same level of interest. The people who enter your property may be a higher priority than those just walking by on the sidewalk. For this reason, Frigate sorts **review items** by importance into **alerts** and **detections**, with a separate **Motion** category for significant motion.
+
+The toggle at the top of the page switches between these three severities. One is always selected.
+
+| Tab | Indicator color | What it shows |
+| -------------- | --------------- | ---------------------------------------------------------------------------------------------------------------- |
+| **Alerts** | dark red | The activity you most want to see. By default, all `person` and `car` tracked objects are alerts. |
+| **Detections** | orange | Everything else Frigate tracked that wasn't promoted to an alert. |
+| **Motion** | yellow | Periods of significant motion, with the ability to filter to periods which did **not** produce a tracked object. |
+
+This same color coding is used for the ring around a selected item and the dots on the calendar. How an object is categorized as an alert vs. a detection, and how required zones refine that, is covered in [Alerts and Detections](/configuration/review#alerts-and-detections).
+
+The **Alerts** and **Detections** tabs show a count next to their label. With **Show Reviewed** turned off (the default), this is the number of items still left to review; with it on, the count reflects every item in the selected time range.
+
+## Marking items as reviewed
+
+Review items are shown as a grid of thumbnail cards next to a vertical activity timeline. Hovering a card (desktop) or swiping to the right (mobile) plays a short preview inline.
+
+- **Clicking** a card opens its recording in [History](/usage/history) and marks the item as reviewed.
+- The object chip on each card is **gray** when the item is unreviewed and turns **green** once it has been reviewed.
+- The **Mark these items as reviewed** button marks everything currently shown as reviewed at once.
+
+Reviewed state is tracked per user, so marking an item reviewed does not hide it for other users. Marking an item reviewed does not delete anything: the footage and the review item itself remain until they expire via retention.
+
+## Selecting and acting on multiple items
+
+To act on several items at once, start a selection by **Ctrl/Cmd-clicking** a card (desktop) or **long-pressing** one (mobile). Selected cards gain a colored ring matching their severity. Keyboard shortcuts speed this up: `Ctrl+A` selects all, `R` marks the selection reviewed, and `Esc` clears it.
+
+With items selected, an action bar appears with options to:
+
+- **Export** the selected items (a single item exports directly; multiple items open the batch [export](/usage/exports) dialog),
+- **Mark as reviewed** or **Mark as unreviewed**, and
+- **Delete** them (admins only).
+
+## Filtering and the calendar
+
+Use the filter controls in the header to narrow what's shown. The available filters depend on the tab: Alerts and Detections can be filtered by **cameras**, **date**, **labels**, **zones**, and whether items are already reviewed; the Motion tab can be filtered by **cameras**, **date**, and **motion only**.
+
+The **calendar** filter lets you jump to a specific day (it shows **Last 24 Hours** until you pick one). On each day:
+
+- An **underline** under the day number means **recordings exist** for that day. Days without recordings are dimmed.
+- A **colored dot** under the day number means there is **unreviewed activity** that day: a **red dot** for unreviewed alerts, or an **orange dot** for unreviewed detections when there are no unreviewed alerts. Motion is not represented by a dot.
+
+Future dates are disabled, and the week start and time zone follow your configuration.
+
+## Reviewing Motion
+
+The Review page also can show periods of motion that didn't produce a tracked object, and provides a way to search past recordings for motion in a specific region. These tools complement the alerts and detections workflow above. See [Tuning Motion Detection](/configuration/motion_detection) for how the underlying motion detector is configured.
+
+The **Motion** tab itself shows a multi-camera grid scrubbed to a shared point in time, with a draggable timeline and a playback-speed selector. A camera tile gains a colored ring when a review item or significant motion overlaps the current time, and clicking a tile opens that camera's recording at that moment. Each camera's options menu (the kebab in the corner of its tile) is where you open **Motion Previews** and **Motion Search**, described below.
+
+### Motion Previews
+
+The Motion Previews pane shows preview clips for periods of significant motion that did not produce a tracked object. It is useful for spotting things that motion detection picked up but object detection did not, which can help validate tuning or catch missed objects.
+
+On the page, click the kebab menu on a camera and choose **Motion Previews**. Each card represents a continuous range of motion-only activity and plays back the recorded preview for that range. A heatmap overlay dims areas of the frame with no motion so the moving regions stand out.
+
+The pane provides a few controls:
+
+- **Speed**: speeds up or slows down all of the preview clips at once.
+- **Dim**: controls how strongly non-motion areas are darkened by the heatmap overlay. Higher values increase motion area visibility.
+- **Filter**: opens a 16×16 grid overlaid on a snapshot of the camera. Select one or more cells to only show clips with motion in those regions. This is helpful for filtering out motion in areas like a busy street while keeping motion in your driveway.
+
+Clicking a preview clip seeks the recording player to that timestamp so you can review the full footage.
+
+### Motion Search
+
+Motion Search lets you scan recorded footage for changes inside a region of interest you draw on the camera. Unlike Motion Previews, which surfaces what Frigate's motion detector flagged in real time, Motion Search re-analyzes the saved recordings, so it can find changes that were missed (for example, an object that appeared while motion detection was paused by `lightning_threshold`, or in a region that is normally motion-masked).
+
+To start a search, open the Actions menu in [History](/usage/history) or click the kebab menu on a camera in the page and choose **Motion Search**. In the dialog:
+
+1. Pick the camera and time range to scan. In the date pickers, days that have recordings available are underlined.
+2. Draw a polygon on the camera frame to define the region of interest.
+3. Adjust the search parameters if needed:
+
+| Field | Description |
+| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Sensitivity Threshold** | Per-pixel luminance change required to count as motion inside the ROI. Behaves like Frigate's motion detection `threshold` setting. |
+| **Minimum Change Area** | Minimum size of a single moving region, as a percentage of the ROI, for a frame to count as significant. Raise it to ignore small movements (leaves, distant motion); lower it when your subject covers only a small slice of the ROI. Every result shows the percentage it scored, so you can use those values to tune this. |
+| **Maximum Results** | Maximum number of matching timestamps to return. The search stops once it reaches this many results, so a lower value finishes sooner while a higher value scans further into the range. |
+| **Parallel mode** | Decode multiple recording ranges at the same time. Speeds up large time ranges at the cost of higher decoding and CPU usage. |
+
+Motion Search samples each recording's keyframes automatically, so there is no frame-rate or sampling setting to tune.
+
+Once running, Frigate scans the recording segments that overlap the time range and reports timestamps where changes were detected inside the polygon, along with the percentage of the ROI that changed. Clicking a result seeks the player to that moment so you can review what happened.
+
+The results panel shows the time range being scanned, a live progress bar with the timestamp currently being analyzed, and the running result count. A collapsible **Search Metrics** section reports how many segments were scanned and processed, how many were skipped because no motion was recorded in the ROI (using the stored motion heatmap), how many frames were decoded, and the total search time. Skipping segments with no recorded motion in the selected ROI is what makes searching long time ranges practical.
+
+#### Common use cases
+
+Frigate's main use case is to record and surface tracked objects, so Motion Search is most useful for the cases where object detection produced nothing: there is no object to find in Explore, but you suspect something happened.
+
+- **Locating an unattributed change.** You know something appeared, disappeared, or moved in a window of footage (a package now gone, a gate left open), but no detection points to it. A search returns the candidate timestamps instead of scrubbing the timeline by hand.
+- **An object that was never detected.** Something Frigate doesn't have a model label for, an object too small or distant to be detected, or movement in a region where detection isn't running. The activity left no tracked object but did change the pixels, so a search can still find it.
+- **Activity while detection was effectively paused.** Changes that occurred while object detection was disabled, motion was suppressed by `skip_motion_threshold`, or inside an area covered by a motion mask, won't appear as review items or tracked objects but can be recovered by searching the recordings directly.
+
+#### Examples
+
+These show how to choose the ROI and **Minimum Change Area** for two common goals. Minimum Change Area is the size of a single moving region as a percentage of the ROI you draw, so the right value depends on how much of the ROI your subject, and its movement between samples, covers.
+
+Because samples are a second or more apart, a moving subject usually appears in two places at once in the comparison, so even ordinary motion often scores tens of percent and a low threshold lets in almost everything. The most reliable approach is to **run a search, look at the percentage each result scored, and set Minimum Change Area just below the values for the events you care about.** The default is 20%; the suggestions below are starting points.
+
+- **When did this item first appear (or disappear)?** A package was dropped off, a car parked, or a trash can was moved, and you want the exact moment. Draw a **tight ROI** around the spot the item occupies and **raise Minimum Change Area** (start around 40–60%). Because the item fills most of a tight ROI, its arrival or removal is a large change, while smaller nearby motion (shadows, a passing pedestrian) stays below the threshold. The **earliest result** is when it appeared; if you only care about that moment, a low Maximum Results finishes faster. If you get no hits, the ROI is probably looser than the item: lower the threshold or tighten the ROI.
+- **What's been getting into the garden?** Something has been trampling a flower bed overnight and no object was ever tracked. Draw a **looser ROI** covering the whole bed and use a **lower Minimum Change Area than the case above**: start near the 20% default and lower it (toward 5–10%) only if a small or distant subject is missed, since it covers just a slice of a large region. Expect more results to scan through: step through the timestamps and jump to each to see what triggered it. If wind-blown plants add noise, raise Minimum Change Area or the Sensitivity Threshold.
+
+#### Expected performance
+
+Motion Search analyzes the saved recordings on demand rather than reading a pre-built index, so a search over a long range takes longer than browsing Motion Previews. Cost scales mainly with how much footage has to be examined: segments with no recorded motion in your ROI are skipped using the stored motion heatmap (shown as "segments skipped" in the status panel), so a quiet range finishes quickly while a busy one takes longer.
+
+To increase the speed of searches:
+
+- Draw a tight ROI. Because **Minimum Change Area** is measured as a percentage of the region you draw, a tight ROI around where you expect the change makes the object fill a larger share of the area, so it clears the threshold more easily. A loose ROI makes the same object a small fraction of the region, so it can fall below the threshold and be missed, forcing you to lower Minimum Change Area, which lets in more noise.
+- Narrow the time range to the window you care about, so there is less footage to examine.
+- Lower **Maximum Results** when you only need the first few hits. Because the search stops once it reaches that many results, a smaller value lets a busy range finish early instead of scanning the whole window.
+- Use Parallel mode to shorten wall-clock time on multi-core systems, at the cost of higher decoding and CPU usage while it runs.
+
+## AI review summaries
+
+When [Generative AI review](/configuration/genai/genai_review) is configured, Frigate can generate a title, description, and threat classification for review items and surface them automatically in Review and History. Clicking the summary chip opens an **AI Analysis** dialog with the generated detail and any flagged concerns.
+
+In Review, an additional icon appears on unreviewed items that the AI classified as **suspicious** (Level 1) or **critical** (Level 2), so the activity that most warrants attention stands out before you open it. The icon goes away once the item has been reviewed.
diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts
index e11cdd5555..69ab7c3f70 100644
--- a/docs/docusaurus.config.ts
+++ b/docs/docusaurus.config.ts
@@ -186,6 +186,7 @@ const config: Config = {
},
plugins: [
path.resolve(__dirname, "plugins", "raw-loader"),
+ path.resolve(__dirname, "plugins", "yaml-loader"),
[
"docusaurus-plugin-openapi-docs",
{
diff --git a/docs/package-lock.json b/docs/package-lock.json
index 626d71dfda..2310274651 100644
--- a/docs/package-lock.json
+++ b/docs/package-lock.json
@@ -14,9 +14,11 @@
"@docusaurus/theme-mermaid": "^3.7.0",
"@inkeep/docusaurus": "^2.0.16",
"@mdx-js/react": "^3.1.0",
+ "@types/js-yaml": "^4.0.9",
"clsx": "^2.1.1",
"docusaurus-plugin-openapi-docs": "^4.5.1",
"docusaurus-theme-openapi-docs": "^4.5.1",
+ "js-yaml": "^4.1.1",
"prism-react-renderer": "^2.4.1",
"raw-loader": "^4.0.2",
"react": "^18.3.1",
@@ -5747,6 +5749,11 @@
"@types/istanbul-lib-report": "*"
}
},
+ "node_modules/@types/js-yaml": {
+ "version": "4.0.9",
+ "resolved": "https://mirrors.tencent.com/npm/@types/js-yaml/-/js-yaml-4.0.9.tgz",
+ "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="
+ },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -10897,9 +10904,9 @@
"license": "MIT"
},
"node_modules/express/node_modules/path-to-regexp": {
- "version": "0.1.12",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
- "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
"license": "MIT"
},
"node_modules/express/node_modules/range-parser": {
@@ -10964,9 +10971,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
- "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
+ "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
"funding": [
{
"type": "github",
@@ -12883,7 +12890,7 @@
},
"node_modules/js-yaml": {
"version": "4.1.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "resolved": "https://mirrors.tencent.com/npm/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"license": "MIT",
"dependencies": {
diff --git a/docs/package.json b/docs/package.json
index 0ff76c4739..1649df7e0c 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -3,9 +3,10 @@
"version": "0.0.0",
"private": true,
"scripts": {
+ "build:config": "node scripts/build-config.mjs",
"docusaurus": "docusaurus",
- "start": "npm run regen-docs && docusaurus start --host 0.0.0.0",
- "build": "npm run regen-docs && docusaurus build",
+ "start": "npm run build:config && npm run regen-docs && docusaurus start --host 0.0.0.0",
+ "build": "npm run build:config && npm run regen-docs && docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
@@ -23,9 +24,12 @@
"@docusaurus/theme-mermaid": "^3.7.0",
"@inkeep/docusaurus": "^2.0.16",
"@mdx-js/react": "^3.1.0",
+ "@types/js-yaml": "^4.0.9",
"clsx": "^2.1.1",
"docusaurus-plugin-openapi-docs": "^4.5.1",
"docusaurus-theme-openapi-docs": "^4.5.1",
+ "js-yaml": "^4.1.1",
+ "marked": "^16.4.2",
"prism-react-renderer": "^2.4.1",
"raw-loader": "^4.0.2",
"react": "^18.3.1",
diff --git a/docs/plugins/js-yaml-loader.js b/docs/plugins/js-yaml-loader.js
new file mode 100644
index 0000000000..66451bd3a9
--- /dev/null
+++ b/docs/plugins/js-yaml-loader.js
@@ -0,0 +1,9 @@
+const yaml = require("js-yaml");
+
+// Webpack loader that compiles a YAML file into a default-exported JS object,
+// so docs data can be authored in YAML (block scalars, no quote/newline
+// escaping) but imported exactly like the JSON it replaces.
+module.exports = function (source) {
+ const data = yaml.load(source);
+ return `export default ${JSON.stringify(data)};`;
+};
diff --git a/docs/plugins/yaml-loader.js b/docs/plugins/yaml-loader.js
new file mode 100644
index 0000000000..475940e6f2
--- /dev/null
+++ b/docs/plugins/yaml-loader.js
@@ -0,0 +1,23 @@
+const path = require("node:path");
+
+// Enables importing YAML data files from docs/data as plain JS objects.
+// Scoped to the data directory so it never intercepts other .yaml files
+// (e.g. the OpenAPI spec under static/).
+module.exports = function (context, options) {
+ return {
+ name: "yaml-data-loader",
+ configureWebpack(config, isServer, utils) {
+ return {
+ module: {
+ rules: [
+ {
+ test: /\.ya?ml$/,
+ include: path.resolve(__dirname, "..", "data"),
+ use: path.resolve(__dirname, "js-yaml-loader.js"),
+ },
+ ],
+ },
+ };
+ },
+ };
+};
diff --git a/docs/scripts/build-config.mjs b/docs/scripts/build-config.mjs
new file mode 100644
index 0000000000..78926bed5b
--- /dev/null
+++ b/docs/scripts/build-config.mjs
@@ -0,0 +1,64 @@
+#!/usr/bin/env node
+
+/**
+ * Build script: reads config.yaml and generates TypeScript files
+ * for the Docker Compose Generator.
+ *
+ * Usage: node scripts/build-config.mjs
+ */
+
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import yaml from "js-yaml";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const CONFIG_DIR = path.resolve(__dirname, "../src/components/DockerComposeGenerator/config");
+const YAML_PATH = path.join(CONFIG_DIR, "config.yaml");
+
+// Read & parse YAML
+const raw = fs.readFileSync(YAML_PATH, "utf8");
+const config = yaml.load(raw);
+
+if (!config.devices || !config.hardware || !config.ports) {
+ console.error("config.yaml must contain 'devices', 'hardware', and 'ports' sections.");
+ process.exit(1);
+}
+
+/**
+ * Generate a .ts file from a section of the YAML config.
+ */
+function generateTsFile(sectionName, items, typeName, varName, mapVarName, yamlFilename) {
+ const jsonItems = JSON.stringify(items, null, 2);
+ // Indent JSON to fit inside the array literal
+ const indented = jsonItems
+ .split("\n")
+ .map((line, i) => (i === 0 ? line : " " + line))
+ .join("\n");
+
+ const content = `/**
+ * AUTO-GENERATED FILE — do not edit directly.
+ * Source: ${yamlFilename}
+ * To update, edit the YAML file and run: npm run build:config
+ */
+
+import type { ${typeName} } from "./types";
+
+export const ${varName}: ${typeName}[] = ${indented};
+
+/** Lookup map for quick access by ID */
+export const ${mapVarName}: Map = new Map(${varName}.map((item) => [item.id, item]));
+`;
+
+ const outPath = path.join(CONFIG_DIR, `${sectionName}.ts`);
+ fs.writeFileSync(outPath, content, "utf8");
+ console.log(` ✓ Generated ${sectionName}.ts (${items.length} items)`);
+}
+
+console.log("Building config from config.yaml...");
+
+generateTsFile("devices", config.devices, "DeviceConfig", "devices", "deviceMap", "config.yaml");
+generateTsFile("hardware", config.hardware, "HardwareOption", "hardwareOptions", "hardwareMap", "config.yaml");
+generateTsFile("ports", config.ports, "PortConfig", "ports", "portMap", "config.yaml");
+
+console.log("Done!");
diff --git a/docs/scripts/lib/nav_map.py b/docs/scripts/lib/nav_map.py
index 80f13d65b7..0fddf40e00 100644
--- a/docs/scripts/lib/nav_map.py
+++ b/docs/scripts/lib/nav_map.py
@@ -63,8 +63,8 @@ SYSTEM_NAV: dict[str, tuple[str, str]] = {
"environment_vars": ("System", "Environment variables"),
"telemetry": ("System", "Telemetry"),
"birdseye": ("System", "Birdseye"),
- "detectors": ("System", "Detector hardware"),
- "model": ("System", "Detection model"),
+ "detectors": ("System", "Detectors and model"),
+ "model": ("System", "Detectors and model"),
}
# All known top-level config section keys
diff --git a/docs/sidebars.ts b/docs/sidebars.ts
index adc3bc1e19..ce6c41c133 100644
--- a/docs/sidebars.ts
+++ b/docs/sidebars.ts
@@ -17,91 +17,126 @@ const sidebars: SidebarsConfig = {
],
Guides: [
"guides/getting_started",
- "guides/configuring_go2rtc",
"guides/ha_notifications",
"guides/ha_network_storage",
"guides/reverse_proxy",
],
- Configuration: {
- "Configuration Files": [
- "configuration/index",
- "configuration/reference",
- {
- type: "link",
- label: "Go2RTC Configuration Reference",
- href: "https://github.com/AlexxIT/go2rtc/tree/v1.9.13#configuration",
- } as PropSidebarItemLink,
- ],
- Detectors: [
- "configuration/object_detectors",
- "configuration/audio_detectors",
- ],
- Enrichments: [
- "configuration/semantic_search",
- "configuration/face_recognition",
- "configuration/license_plate_recognition",
- "configuration/bird_classification",
- {
- type: "category",
- label: "Custom Classification",
- link: {
- type: "generated-index",
- title: "Custom Classification",
- description: "Configuration for custom classification models",
+ Usage: [
+ "usage/live",
+ "usage/review",
+ "usage/history",
+ "usage/explore",
+ "usage/exports",
+ ],
+ Configuration: [
+ "configuration/config",
+ {
+ type: "category",
+ label: "Detectors",
+ items: [
+ "configuration/object_detectors",
+ "configuration/audio_detectors",
+ ],
+ },
+ {
+ type: "category",
+ label: "Enrichments",
+ items: [
+ "configuration/semantic_search",
+ "configuration/face_recognition",
+ "configuration/license_plate_recognition",
+ "configuration/bird_classification",
+ {
+ type: "category",
+ label: "Custom Classification",
+ link: {
+ type: "generated-index",
+ title: "Custom Classification",
+ description: "Configuration for custom classification models",
+ },
+ items: [
+ "configuration/custom_classification/state_classification",
+ "configuration/custom_classification/object_classification",
+ ],
},
- items: [
- "configuration/custom_classification/state_classification",
- "configuration/custom_classification/object_classification",
- ],
- },
- {
- type: "category",
- label: "Generative AI",
- link: {
- type: "generated-index",
- title: "Generative AI",
- description: "Generative AI Features",
+ {
+ type: "category",
+ label: "Generative AI",
+ link: {
+ type: "generated-index",
+ title: "Generative AI",
+ description: "Generative AI Features",
+ },
+ items: [
+ "configuration/genai/genai_config",
+ "configuration/genai/genai_review",
+ "configuration/genai/genai_objects",
+ ],
},
- items: [
- "configuration/genai/genai_config",
- "configuration/genai/genai_review",
- "configuration/genai/genai_objects",
- ],
- },
- ],
- Cameras: [
- "configuration/cameras",
- "configuration/review",
- "configuration/record",
- "configuration/snapshots",
- "configuration/motion_detection",
- "configuration/birdseye",
- "configuration/live",
- "configuration/restream",
- "configuration/autotracking",
- "configuration/camera_specific",
- ],
- Objects: [
- "configuration/object_filters",
- "configuration/masks",
- "configuration/zones",
- "configuration/objects",
- "configuration/stationary_objects",
- ],
- "Hardware Acceleration": [
- "configuration/hardware_acceleration_video",
- "configuration/hardware_acceleration_enrichments",
- ],
- "Extra Configuration": [
- "configuration/authentication",
- "configuration/notifications",
- "configuration/profiles",
- "configuration/ffmpeg_presets",
- "configuration/pwa",
- "configuration/tls",
- "configuration/advanced",
- ],
- },
+ ],
+ },
+ {
+ type: "category",
+ label: "Cameras",
+ items: [
+ "configuration/cameras",
+ "configuration/review",
+ "configuration/record",
+ "configuration/snapshots",
+ "configuration/motion_detection",
+ "configuration/birdseye",
+ "configuration/live",
+ "configuration/restream",
+ "configuration/autotracking",
+ "configuration/camera_specific",
+ ],
+ },
+ {
+ type: "category",
+ label: "Objects",
+ items: [
+ "configuration/object_filters",
+ "configuration/masks",
+ "configuration/zones",
+ "configuration/objects",
+ "configuration/stationary_objects",
+ ],
+ },
+ {
+ type: "category",
+ label: "Hardware Acceleration",
+ items: [
+ "configuration/hardware_acceleration_video",
+ "configuration/hardware_acceleration_enrichments",
+ ],
+ },
+ {
+ type: "category",
+ label: "Extra Configuration",
+ items: [
+ "configuration/authentication",
+ "configuration/notifications",
+ "configuration/profiles",
+ "configuration/go2rtc",
+ "configuration/ffmpeg_presets",
+ "configuration/pwa",
+ "configuration/tls",
+ ],
+ },
+ {
+ type: "category",
+ label: "Advanced Configuration",
+ items: [
+ "configuration/advanced/system",
+ "configuration/advanced/reference",
+ {
+ type: "link",
+ label: "Go2RTC Configuration Reference",
+ href: "https://github.com/AlexxIT/go2rtc/tree/v1.9.14#configuration",
+ } as PropSidebarItemLink,
+ ],
+ },
+ ],
Integrations: [
"integrations/plus",
"integrations/home-assistant",
@@ -130,6 +165,7 @@ const sidebars: SidebarsConfig = {
],
Troubleshooting: [
"troubleshooting/faqs",
+ "troubleshooting/go2rtc",
"troubleshooting/recordings",
"troubleshooting/dummy-camera",
{
diff --git a/docs/src/components/DockerComposeGenerator/DockerComposeGenerator.tsx b/docs/src/components/DockerComposeGenerator/DockerComposeGenerator.tsx
new file mode 100644
index 0000000000..b8a8a8fc85
--- /dev/null
+++ b/docs/src/components/DockerComposeGenerator/DockerComposeGenerator.tsx
@@ -0,0 +1,108 @@
+import React from "react";
+import Admonition from "@theme/Admonition";
+import DeviceSelector from "./components/DeviceSelector";
+import HardwareOptions from "./components/HardwareOptions";
+import PortConfigSection from "./components/PortConfig";
+import StoragePaths from "./components/StoragePaths";
+import NvidiaGpuConfig from "./components/NvidiaGpuConfig";
+import OtherOptions from "./components/OtherOptions";
+import GeneratedOutput from "./components/GeneratedOutput";
+import { useConfigGenerator } from "./hooks/useConfigGenerator";
+import styles from "./styles.module.css";
+
+/**
+ * Simple markdown-link-to-React renderer for help text.
+ * Only supports [text](url) syntax — no nested brackets.
+ */
+function renderHelpText(text: string): React.ReactNode {
+ const parts = text.split(/(\[[^\]]+\]\([^)]+\))/g);
+ return parts.map((part, i) => {
+ const match = part.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
+ if (match) {
+ return (
+
+ {match[1]}
+
+ );
+ }
+ return {part} ;
+ });
+}
+
+export default function DockerComposeGenerator() {
+ const {
+ deviceId, device, hardwareEnabled,
+ portEnabled,
+ nvidiaGpuCount, nvidiaGpuDeviceId,
+ configPath, mediaPath, rtspPassword, timezone, shmSize,
+ shmSizeError, gpuDeviceIdError, configPathError, mediaPathError,
+ hasAnyHardware, generatedYaml,
+ selectDevice, toggleHardware, togglePort,
+ handleShmSizeChange, handleConfigPathChange, handleMediaPathChange,
+ handleNvidiaGpuCountChange, handleNvidiaGpuDeviceIdChange,
+ setRtspPassword, setTimezone, isHardwareDisabled,
+ } = useConfigGenerator();
+
+ return (
+
+
+
+
+ {device.helpText && (
+
+ {renderHelpText(device.helpText)}
+
+ )}
+
+ {device.needsNvidiaConfig && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/docs/src/components/DockerComposeGenerator/components/DeviceSelector.tsx b/docs/src/components/DockerComposeGenerator/components/DeviceSelector.tsx
new file mode 100644
index 0000000000..ddad160502
--- /dev/null
+++ b/docs/src/components/DockerComposeGenerator/components/DeviceSelector.tsx
@@ -0,0 +1,147 @@
+import React from "react";
+import { useColorMode } from "@docusaurus/theme-common";
+import { devices } from "../config";
+import type { DeviceConfig } from "../config";
+import styles from "../styles.module.css";
+
+interface Props {
+ selectedId: string;
+ onSelect: (id: string) => void;
+}
+
+/**
+ * Determine the icon type from the icon string:
+ * - Starts with " tag.
+ */
+function hasBackgroundProps(style: React.CSSProperties | undefined): boolean {
+ if (!style) return false;
+ return Object.keys(style).some((key) => {
+ const k = key.toLowerCase().replace(/-/g, "");
+ return k === "backgroundsize" || k === "backgroundposition" || k === "backgroundrepeat" || k === "backgroundimage";
+ });
+}
+
+/**
+ * Convert a style object to CSS custom properties (e.g. { width: "24px" } → { "--svg-width": "24px" })
+ * so they can be consumed by CSS rules targeting child elements like .
+ */
+function toCssVars(style: React.CSSProperties | undefined, prefix: string): React.CSSProperties {
+ if (!style) return {};
+ const vars: Record = {};
+ for (const [key, value] of Object.entries(style)) {
+ const cssKey = key.replace(/([A-Z])/g, "-$1").toLowerCase();
+ vars[`--${prefix}-${cssKey}`] = value;
+ }
+ return vars as React.CSSProperties;
+}
+
+function DeviceIcon({ device }: { device: DeviceConfig }) {
+ const { isDarkTheme } = useColorMode();
+ const iconStr = isDarkTheme && device.iconDark ? device.iconDark : device.icon;
+ const iconStyle = (isDarkTheme && device.iconDarkStyle
+ ? device.iconDarkStyle
+ : device.iconStyle) as React.CSSProperties | undefined;
+ const svgStyle = (isDarkTheme && device.svgDarkStyle
+ ? device.svgDarkStyle
+ : device.svgStyle) as React.CSSProperties | undefined;
+
+ const iconType = getIconType(iconStr);
+
+ if (iconType === "svg") {
+ return (
+
+ );
+ }
+
+ if (iconType === "image") {
+ // When iconStyle contains background-* properties, render as background-image
+ // on the container div instead of an tag, enabling background-size/position control.
+ if (hasBackgroundProps(iconStyle)) {
+ return (
+
+ );
+ }
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {iconStr}
+
+ );
+}
+
+function DeviceCard({
+ device,
+ active,
+ onClick,
+}: {
+ device: DeviceConfig;
+ active: boolean;
+ onClick: () => void;
+}) {
+ return (
+ {
+ if (e.key === "Enter" || e.key === " ") onClick();
+ }}
+ >
+
+
{device.name}
+
{device.description}
+
+ );
+}
+
+export default function DeviceSelector({ selectedId, onSelect }: Props) {
+ return (
+
+
Device Type
+
+ {devices.map((d) => (
+ onSelect(d.id)}
+ />
+ ))}
+
+
+ );
+}
diff --git a/docs/src/components/DockerComposeGenerator/components/GeneratedOutput.tsx b/docs/src/components/DockerComposeGenerator/components/GeneratedOutput.tsx
new file mode 100644
index 0000000000..f170637aa0
--- /dev/null
+++ b/docs/src/components/DockerComposeGenerator/components/GeneratedOutput.tsx
@@ -0,0 +1,60 @@
+import React, { useState, useCallback } from "react";
+import CodeBlock from "@theme/CodeBlock";
+import Admonition from "@theme/Admonition";
+import styles from "../styles.module.css";
+
+interface Props {
+ yaml: string;
+ configPath: string;
+ mediaPath: string;
+ hasAnyHardware: boolean;
+ deviceId: string;
+}
+
+export default function GeneratedOutput({
+ yaml,
+ configPath,
+ mediaPath,
+ hasAnyHardware,
+ deviceId,
+}: Props) {
+ const [copied, setCopied] = useState(false);
+
+ const handleCopy = useCallback(() => {
+ navigator.clipboard.writeText(yaml).then(() => {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ });
+ }, [yaml]);
+
+ return (
+
+
+
Generated Configuration
+
+ {copied ? "Copied!" : "Copy"}
+
+
+
+ {!configPath && (
+
+ You haven't specified a config file directory. You may want to modify the default path.
+
+ )}
+ {!mediaPath && (
+
+ You haven't specified a recording storage directory. You may want to modify the default path.
+
+ )}
+ {deviceId === "stable" && !hasAnyHardware && (
+
+ You haven't selected any hardware acceleration. Please check if you have supported hardware available.
+
+ )}
+
+
+ {yaml}
+
+
+ );
+}
diff --git a/docs/src/components/DockerComposeGenerator/components/HardwareOptions.tsx b/docs/src/components/DockerComposeGenerator/components/HardwareOptions.tsx
new file mode 100644
index 0000000000..9c261ed419
--- /dev/null
+++ b/docs/src/components/DockerComposeGenerator/components/HardwareOptions.tsx
@@ -0,0 +1,62 @@
+import React from "react";
+import { hardwareOptions } from "../config";
+import type { HardwareOption } from "../config";
+import styles from "../styles.module.css";
+
+interface Props {
+ deviceId: string;
+ hardwareEnabled: Record;
+ onToggle: (hwId: string) => void;
+ isDisabled: (hwId: string) => boolean;
+}
+
+function renderDescription(text: string): React.ReactNode {
+ const parts = text.split(/(\[[^\]]+\]\([^)]+\))/g);
+ return parts.map((part, i) => {
+ const match = part.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
+ if (match) {
+ return {match[1]} ;
+ }
+ return {part} ;
+ });
+}
+
+function HardwareCheckbox({
+ hw, disabled, checked, onToggle,
+}: {
+ hw: HardwareOption; disabled: boolean; checked: boolean; onToggle: () => void;
+}) {
+ return (
+
+
+
+ {hw.label}
+
+ {checked && hw.description && (
+
{renderDescription(hw.description)}
+ )}
+
+ );
+}
+
+export default function HardwareOptions({ deviceId, hardwareEnabled, onToggle, isDisabled }: Props) {
+ return (
+
+
Generic Hardware Devices
+ {deviceId !== "stable" && (
+
+ Some options have been auto-configured based on your device type.
+
+ )}
+
+ {hardwareOptions.map((hw) => {
+ const disabled = isDisabled(hw.id);
+ const checked = disabled ? false : !!hardwareEnabled[hw.id];
+ return (
+ onToggle(hw.id)} />
+ );
+ })}
+
+
+ );
+}
diff --git a/docs/src/components/DockerComposeGenerator/components/NvidiaGpuConfig.tsx b/docs/src/components/DockerComposeGenerator/components/NvidiaGpuConfig.tsx
new file mode 100644
index 0000000000..9c9be5e6a9
--- /dev/null
+++ b/docs/src/components/DockerComposeGenerator/components/NvidiaGpuConfig.tsx
@@ -0,0 +1,64 @@
+import React from "react";
+import styles from "../styles.module.css";
+
+interface Props {
+ gpuCount: string;
+ gpuDeviceId: string;
+ gpuDeviceIdError: boolean;
+ onGpuCountChange: (value: string) => void;
+ onGpuDeviceIdChange: (value: string) => void;
+}
+
+export default function NvidiaGpuConfig({
+ gpuCount,
+ gpuDeviceId,
+ gpuDeviceIdError,
+ onGpuCountChange,
+ onGpuDeviceIdChange,
+}: Props) {
+ const showDeviceId = gpuCount !== "";
+
+ return (
+
+
+
+ GPU count:
+
+ onGpuCountChange(e.target.value.replace(/\D/g, ""))}
+ />
+
+ {showDeviceId && (
+
+
+ GPU device IDs (required, comma-separated):
+
+
onGpuDeviceIdChange(e.target.value)}
+ />
+ {gpuDeviceIdError ? (
+
+ ⚠️ GPU device IDs are required when GPU count is a number
+
+ ) : (
+
+ Single GPU: 0 | Multiple GPUs: 0,1,2
+
+ )}
+
+ )}
+
+ );
+}
diff --git a/docs/src/components/DockerComposeGenerator/components/OtherOptions.tsx b/docs/src/components/DockerComposeGenerator/components/OtherOptions.tsx
new file mode 100644
index 0000000000..8d1efef0fc
--- /dev/null
+++ b/docs/src/components/DockerComposeGenerator/components/OtherOptions.tsx
@@ -0,0 +1,122 @@
+import React, { useMemo } from "react";
+import CodeInline from "@theme/CodeInline";
+import styles from "../styles.module.css";
+
+const AUTO_TIMEZONE_VALUE = "__auto__";
+
+function getTimezoneList(): string[] {
+ if (typeof Intl !== "undefined") {
+ const intl = Intl as typeof Intl & {
+ supportedValuesOf?: (key: string) => string[];
+ };
+ const supported = intl.supportedValuesOf?.("timeZone");
+ if (supported && supported.length > 0) {
+ return [...supported].sort();
+ }
+ }
+
+ const fallback = Intl.DateTimeFormat().resolvedOptions().timeZone;
+ return fallback ? [fallback] : ["UTC"];
+}
+
+interface Props {
+ rtspPassword: string;
+ timezone: string;
+ shmSize: string;
+ shmSizeError: boolean;
+ onRtspPasswordChange: (value: string) => void;
+ onTimezoneChange: (value: string) => void;
+ onShmSizeChange: (value: string) => void;
+}
+
+export default function OtherOptions({
+ rtspPassword,
+ timezone,
+ shmSize,
+ shmSizeError,
+ onRtspPasswordChange,
+ onTimezoneChange,
+ onShmSizeChange,
+}: Props) {
+ const timezones = useMemo(() => getTimezoneList(), []);
+ const systemTimezone =
+ Intl.DateTimeFormat().resolvedOptions().timeZone || "Etc/UTC";
+ const selectedValue = timezone || AUTO_TIMEZONE_VALUE;
+
+ return (
+
+
Other Options
+
+
+
+ Timezone:
+
+
+ onTimezoneChange(
+ e.target.value === AUTO_TIMEZONE_VALUE ? "" : e.target.value
+ )
+ }
+ >
+
+ Use browser timezone ({systemTimezone})
+
+ {timezones.map((tz) => (
+
+ {tz}
+
+ ))}
+
+
+
+
+ Shared memory (SHM):
+
+
onShmSizeChange(e.target.value)}
+ />
+ {shmSizeError ? (
+
+ ⚠️ Invalid format. Use a number followed by a unit (e.g. 512mb, 1gb)
+
+ ) : (
+
+ See{" "}
+
+ calculating required SHM size
+ {" "}
+ for the correct value.
+
+ )}
+
+
+
+ RTSP password:
+
+
onRtspPasswordChange(e.target.value)}
+ />
+
+ Optional. You can specify{" "}
+ {"{FRIGATE_RTSP_PASSWORD}"} {" "}
+ in the config file to reference camera stream passwords. This is NOT
+ the Frigate login password.
+
+
+
+
+ );
+}
diff --git a/docs/src/components/DockerComposeGenerator/components/PortConfig.tsx b/docs/src/components/DockerComposeGenerator/components/PortConfig.tsx
new file mode 100644
index 0000000000..c4e5acf713
--- /dev/null
+++ b/docs/src/components/DockerComposeGenerator/components/PortConfig.tsx
@@ -0,0 +1,71 @@
+import React from "react";
+import Admonition from "@theme/Admonition";
+import { ports } from "../config";
+import styles from "../styles.module.css";
+
+interface Props {
+ portEnabled: Record;
+ onTogglePort: (portId: string) => void;
+}
+
+function PortItem({
+ port,
+ enabled,
+ onToggle,
+}: {
+ port: typeof ports[number];
+ enabled: boolean;
+ onToggle: () => void;
+}) {
+ const showWarning = port.warningContent && (
+ port.warningWhen === "checked" ? enabled :
+ port.warningWhen === "unchecked" ? !enabled : enabled
+ );
+
+ return (
+
+
+
+
+ {port.locked && "🔒 "}
+ Port {port.host}
+ {port.protocol !== "tcp" && `/${port.protocol}`}
+
+
+ {port.description && (
+
{port.description}
+ )}
+ {showWarning && (
+
+ {port.warningContent}
+
+ )}
+
+ );
+}
+
+export default function PortConfigSection({
+ portEnabled,
+ onTogglePort,
+}: Props) {
+ return (
+
+
Port Configuration
+
+ {ports.map((port) => (
+
onTogglePort(port.id)}
+ />
+ ))}
+
+
+ );
+}
diff --git a/docs/src/components/DockerComposeGenerator/components/StoragePaths.tsx b/docs/src/components/DockerComposeGenerator/components/StoragePaths.tsx
new file mode 100644
index 0000000000..1e20189cea
--- /dev/null
+++ b/docs/src/components/DockerComposeGenerator/components/StoragePaths.tsx
@@ -0,0 +1,66 @@
+import React from "react";
+import styles from "../styles.module.css";
+
+interface Props {
+ configPath: string;
+ mediaPath: string;
+ configPathError: boolean;
+ mediaPathError: boolean;
+ onConfigPathChange: (value: string) => void;
+ onMediaPathChange: (value: string) => void;
+}
+
+export default function StoragePaths({
+ configPath,
+ mediaPath,
+ configPathError,
+ mediaPathError,
+ onConfigPathChange,
+ onMediaPathChange,
+}: Props) {
+ return (
+
+
Storage Paths
+
+
+
+ Config / DB / model cache directory (on your host):
+
+
onConfigPathChange(e.target.value)}
+ />
+ {configPathError && (
+
+ ⚠️ Path contains invalid characters. Only letters, numbers,
+ underscores, hyphens, slashes, and dots are allowed.
+
+ )}
+
+
+
+ Recording storage directory (on your host):
+
+
onMediaPathChange(e.target.value)}
+ />
+ {mediaPathError && (
+
+ ⚠️ Path contains invalid characters. Only letters, numbers,
+ underscores, hyphens, slashes, and dots are allowed.
+
+ )}
+
+
+
+ );
+}
diff --git a/docs/src/components/DockerComposeGenerator/config/config.yaml b/docs/src/components/DockerComposeGenerator/config/config.yaml
new file mode 100644
index 0000000000..42199ffca1
--- /dev/null
+++ b/docs/src/components/DockerComposeGenerator/config/config.yaml
@@ -0,0 +1,297 @@
+# Unified configuration for Docker Compose Generator
+# This file defines all devices, hardware options, and ports for Frigate Docker Compose generation
+
+devices:
+ - id: "stable"
+ name: "Standard x86_64"
+ description: "Generic PC / server"
+ icon: "💻"
+ imageTag: "stable"
+ autoHardware: []
+
+ - id: "intel"
+ name: "Intel Device"
+ description: "Intel GPU / NPU"
+ icon: ' '
+ imageTag: "stable"
+ autoHardware:
+ - "gpu"
+ - "intelNpu"
+ helpText: "Intel Device automatically configures /dev/dri and /dev/accel device mappings."
+ helpType: "info"
+
+ - id: "stable-tensorrt"
+ name: "NVIDIA GPU"
+ description: "NVIDIA acceleration"
+ icon: ' '
+ svgStyle:
+ width: 50px
+ height: 50px
+ iconStyle:
+ padding-bottom: 15px
+ imageTag: "stable-tensorrt"
+ autoHardware: []
+ helpText: "Requires the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html#docker) to be installed. GPU deploy resources are configured automatically."
+ helpType: "warning"
+ needsNvidiaConfig: true
+
+ - id: "stable-tensorrt-jp6"
+ name: "NVIDIA Jetson"
+ description: "Jetson development board"
+ icon: ' '
+ svgStyle:
+ width: 50px
+ height: 50px
+ iconStyle:
+ padding-bottom: 15px
+ imageTag: "stable-tensorrt-jp6"
+ autoHardware: []
+ helpText: "NVIDIA Jetson devices automatically configure runtime: nvidia."
+ helpType: "info"
+ runtime: "nvidia"
+
+ - id: "stable-rocm"
+ name: "AMD GPU"
+ description: "ROCm acceleration"
+ icon: "https://www.amd.com/content/dam/code/images/header/amd-header-logo.svg"
+ iconStyle:
+ filter: invert(1)
+ background-repeat: no-repeat
+ background-position: right center
+ background-size: 338% 90%
+ iconDark: "https://www.amd.com/content/dam/code/images/header/amd-header-logo.svg"
+ iconDarkStyle:
+ filter: invert(0)
+ background-repeat: no-repeat
+ background-position: right center
+ background-size: 338% 90%
+ imageTag: "stable-rocm"
+ autoHardware:
+ - "gpu"
+ helpText: "AMD GPU automatically configures LIBVA_DRIVER_NAME environment variable and /dev/dri device mapping."
+ helpType: "info"
+ env:
+ LIBVA_DRIVER_NAME: "radeonsi"
+
+ - id: "apple-silicon"
+ name: "Apple Silicon"
+ description: "Mac M-series processor"
+ icon: ' '
+ svgStyle:
+ width: 90px
+ height: 90px
+ svgDarkStyle:
+ width: 90px
+ height: 90px
+ fill: white
+ imageTag: "stable"
+ imageTagSuffix: "-standard-arm64"
+ autoHardware: []
+ helpText: "Apple Silicon (M-series) requires an [external detector](/configuration/object_detectors#apple-silicon-detector) running on the host."
+ helpType: "warning"
+ extraHosts:
+ - "host.docker.internal:host-gateway"
+
+ - id: "raspberry-pi"
+ name: "Raspberry Pi"
+ description: "ARM device"
+ icon: ' '
+ svgStyle:
+ width: 40px
+ height: 40px
+ transform: translateX(-3px)
+ imageTag: "stable"
+ imageTagSuffix: "-standard-arm64"
+ autoHardware:
+ - "video11"
+ helpText: "Raspberry Pi automatically configures the video11 device (RPi 4) and uses the arm64 image."
+ helpType: "info"
+
+ - id: "stable-rk"
+ name: "Rockchip"
+ description: "Rockchip SoC board"
+ icon: "https://www.rock-chips.com/favicon.ico"
+ imageTag: "stable-rk"
+ autoHardware:
+ - "gpu"
+ helpText: "Rockchip devices automatically configure /dev/dri device mapping."
+ helpType: "info"
+ devices:
+ - host: "/dev/dma_heap"
+ comment: "Rockchip DMA heap"
+ - host: "/dev/rga"
+ comment: "Rockchip RGA"
+ - host: "/dev/mpp_service"
+ comment: "Rockchip MPP service"
+ volumes:
+ - host: "/sys/"
+ container: "/sys/"
+ readOnly: true
+ comment: "Rockchip system info"
+ securityOpt:
+ - "apparmor=unconfined"
+ - "systempaths=unconfined"
+
+ - id: "stable-synaptics"
+ name: "Synaptics"
+ description: "Synaptics NPU"
+ icon: "🔷"
+ imageTag: "stable-synaptics"
+ autoHardware: []
+ helpText: "Synaptics devices automatically configure /dev/synap and video devices."
+ helpType: "info"
+ devices:
+ - host: "/dev/synap"
+ comment: "Synaptics NPU"
+ - host: "/dev/video0"
+ comment: "Video device 0"
+ - host: "/dev/video1"
+ comment: "Video device 1"
+
+hardware:
+ - id: "usbCoral"
+ label: "USB Coral (TPU)"
+ description: "Enable this if you have a Google Coral USB TPU. Other Coral versions require different device paths."
+ disabledWhen:
+ - "apple-silicon"
+ - "stable-synaptics"
+ devices:
+ - host: "/dev/bus/usb"
+ container: "/dev/bus/usb"
+ comment: "USB Coral — modify for other versions"
+
+ - id: "pcieCoral"
+ label: "PCIe Coral (TPU)"
+ description: "Enable this if you have a Google Coral PCIe/M.2 TPU. You also need to [install the driver](https://github.com/jnicolson/gasket-builder)."
+ disabledWhen:
+ - "apple-silicon"
+ - "stable-synaptics"
+ devices:
+ - host: "/dev/apex_0"
+ container: "/dev/apex_0"
+ comment: "PCIe Coral — follow driver instructions at https://github.com/jnicolson/gasket-builder"
+
+ - id: "gpu"
+ label: "Intel/AMD GPU (/dev/dri)"
+ description: "Pass through /dev/dri for GPU hardware acceleration (Intel/AMD)."
+ disabledWhen:
+ - "stable-tensorrt-jp6"
+ - "apple-silicon"
+ devices:
+ - host: "/dev/dri"
+ container: "/dev/dri"
+ comment: "Intel/AMD GPU hardware acceleration"
+
+ - id: "intelNpu"
+ label: "Intel NPU (/dev/accel)"
+ description: "Pass through /dev/accel for Intel NPU acceleration."
+ disabledWhen:
+ - "stable-tensorrt-jp6"
+ - "apple-silicon"
+ - "stable-rocm"
+ - "stable-rk"
+ - "stable-synaptics"
+ devices:
+ - host: "/dev/accel"
+ container: "/dev/accel"
+ comment: "Intel NPU"
+
+ - id: "hailo"
+ label: "Hailo NPU (/dev/hailo0)"
+ description: "Pass through /dev/hailo0 for Hailo-8 / Hailo-8L NPU acceleration. You also need to [install the driver](#hailo-8)."
+ disabledWhen:
+ - "apple-silicon"
+ - "stable-synaptics"
+ devices:
+ - host: "/dev/hailo0"
+ comment: "Hailo NPU"
+
+ - id: "memryx"
+ label: "MemryX MX3 (/dev/memx0)"
+ description: "Pass through /dev/memx0 for MemryX MX3 NPU acceleration. You also need to [install the driver](#memryx-mx3)."
+ disabledWhen:
+ - "apple-silicon"
+ - "stable-synaptics"
+ devices:
+ - host: "/dev/memx0"
+ comment: "MemryX MX3 NPU"
+ volumes:
+ - host: "/run/mxa_manager"
+ container: "/run/mxa_manager"
+ comment: "MemryX manager"
+
+ - id: "axera"
+ label: "AXERA Accelerator"
+ description: "Pass through AXERA accelerator devices. Requires the [AXCL driver](#axera) to be installed first."
+ disabledWhen:
+ - "apple-silicon"
+ - "stable-synaptics"
+ devices:
+ - host: "/dev/axcl_host"
+ comment: "AXERA accelerator device"
+ - host: "/dev/ax_mmb_dev"
+ comment: "AXERA MMB device"
+ - host: "/dev/msg_userdev"
+ comment: "AXERA message device"
+ volumes:
+ - host: "/usr/bin/axcl"
+ container: "/usr/bin/axcl"
+ comment: "AXERA binaries"
+ - host: "/usr/lib/axcl"
+ container: "/usr/lib/axcl"
+ comment: "AXERA libraries"
+
+ - id: "video11"
+ label: "Raspberry Pi (/dev/video11)"
+ description: "Pass through /dev/video11 for Raspberry Pi 4B hardware acceleration."
+ disabledWhen:
+ - "stable-tensorrt"
+ - "stable-tensorrt-jp6"
+ - "stable-rocm"
+ - "stable-rk"
+ - "stable-synaptics"
+ - "intel"
+ - "apple-silicon"
+ - "stable"
+ devices:
+ - host: "/dev/video11"
+ container: "/dev/video11"
+ comment: "Raspberry Pi 4B"
+
+ports:
+ - id: "8971"
+ host: 8971
+ container: 8971
+ protocol: "tcp"
+ description: "Authenticated UI and API access (default HTTPS)"
+ defaultEnabled: true
+ warningContent: "This is the access port for Frigate. Closing it means you will no longer be able to access the instance."
+ warningWhen: "unchecked"
+
+ - id: "8554"
+ host: 8554
+ container: 8554
+ protocol: "tcp"
+ description: "Access RTSP feeds from go2rtc"
+ defaultEnabled: true
+
+ - id: "8555-tcp"
+ host: 8555
+ container: 8555
+ protocol: "tcp"
+ description: "WebRTC over TCP"
+ defaultEnabled: true
+
+ - id: "8555-udp"
+ host: 8555
+ container: 8555
+ protocol: "udp"
+ description: "WebRTC over UDP"
+ defaultEnabled: true
+
+ - id: "1984"
+ host: 1984
+ container: 1984
+ protocol: "tcp"
+ description: "Go2RTC Web UI"
+ defaultEnabled: false
diff --git a/docs/src/components/DockerComposeGenerator/config/index.ts b/docs/src/components/DockerComposeGenerator/config/index.ts
new file mode 100644
index 0000000000..5acaba9f1b
--- /dev/null
+++ b/docs/src/components/DockerComposeGenerator/config/index.ts
@@ -0,0 +1,12 @@
+export { devices, deviceMap } from "./devices";
+export { hardwareOptions, hardwareMap } from "./hardware";
+export { ports, portMap } from "./ports";
+
+export type {
+ DeviceConfig,
+ DeviceMapping,
+ VolumeMapping,
+ HardwareOption,
+ PortConfig,
+ NvidiaDeployConfig,
+} from "./types";
diff --git a/docs/src/components/DockerComposeGenerator/config/types.ts b/docs/src/components/DockerComposeGenerator/config/types.ts
new file mode 100644
index 0000000000..87bcb608dd
--- /dev/null
+++ b/docs/src/components/DockerComposeGenerator/config/types.ts
@@ -0,0 +1,154 @@
+/**
+ * Type definitions for the Docker Compose Generator configuration.
+ * All device, hardware, and port options are declaratively defined
+ * so that adding a new device only requires editing config files.
+ */
+
+/** A single device mapping entry (e.g. /dev/dri:/dev/dri) */
+export interface DeviceMapping {
+ /** Host device path */
+ host: string;
+ /** Container device path (defaults to host if omitted) */
+ container?: string;
+ /** Inline comment for this device line */
+ comment?: string;
+}
+
+/** A single volume mapping entry */
+export interface VolumeMapping {
+ /** Host path */
+ host: string;
+ /** Container path */
+ container: string;
+ /** Whether the mount is read-only */
+ readOnly?: boolean;
+ /** Inline comment */
+ comment?: string;
+}
+
+/** NVIDIA deploy configuration for docker-compose */
+export interface NvidiaDeployConfig {
+ /** "all" or a specific number */
+ count: string;
+ /** Specific GPU device IDs (when count is a number) */
+ deviceIds?: string[];
+}
+
+/** Full device type definition */
+export interface DeviceConfig {
+ /** Unique identifier, e.g. "intel" */
+ id: string;
+ /** Display name, e.g. "Intel GPU" */
+ name: string;
+ /** Short description */
+ description: string;
+ /**
+ * Icon for the device card. Supports:
+ * - Emoji string (e.g. "🖥️")
+ * - Image URL or static path (e.g. "/img/intel.svg", "https://example.com/icon.png")
+ * - Inline SVG markup (e.g. "... ")
+ */
+ icon: string;
+ /**
+ * Additional CSS properties applied to the icon element.
+ * - For image-type icons: if any `background-*` property (e.g. `background-size`,
+ * `background-position`) is present, the image is rendered as a CSS `background-image`
+ * on the container div, enabling full background positioning control.
+ * Otherwise the image is rendered as an ` ` tag and styles apply to it.
+ * - For emoji/SVG icons: styles apply to the container div.
+ */
+ iconStyle?: Record;
+ /**
+ * Additional CSS properties applied directly to the inner `` element
+ * when the icon is an inline SVG. Use this to override the default
+ * `width: 100%; height: 100%` or set `fill`, `transform`, etc.
+ * Ignored for emoji and image-type icons.
+ */
+ svgStyle?: Record;
+ /**
+ * Icon for dark mode. Same format as `icon`. When provided, this icon
+ * replaces `icon` when the user is in dark mode.
+ */
+ iconDark?: string;
+ /** Additional CSS properties for the dark mode icon container */
+ iconDarkStyle?: Record;
+ /**
+ * SVG-specific styles for dark mode. Same as `svgStyle` but applied
+ * when dark mode is active. Merged over `svgStyle` in dark mode.
+ */
+ svgDarkStyle?: Record;
+ /** Docker image tag, e.g. "stable" */
+ imageTag: string;
+ /**
+ * Image tag suffix appended to the base tag.
+ * e.g. "-standard-arm64" produces "stable-standard-arm64"
+ */
+ imageTagSuffix?: string;
+ /** Hardware option IDs to auto-enable when this device is selected */
+ autoHardware: string[];
+ /** Help text shown as an admonition when this device is selected */
+ helpText?: string;
+ /** Admonition type for help text */
+ helpType?: "info" | "warning" | "danger";
+ /** Device mappings always added for this device type */
+ devices?: DeviceMapping[];
+ /** Volume mappings always added for this device type */
+ volumes?: VolumeMapping[];
+ /** Extra environment variables for this device type */
+ env?: Record;
+ /** NVIDIA deploy config (only for tensorrt) */
+ nvidiaDeploy?: NvidiaDeployConfig;
+ /** Runtime setting, e.g. "nvidia" for Jetson */
+ runtime?: string;
+ /** Extra hosts entries, e.g. "host.docker.internal:host-gateway" */
+ extraHosts?: string[];
+ /** Security options, e.g. ["apparmor=unconfined"] */
+ securityOpt?: string[];
+ /** Whether this device type needs the NVIDIA GPU config UI */
+ needsNvidiaConfig?: boolean;
+}
+
+/** Generic hardware acceleration option definition */
+export interface HardwareOption {
+ /** Unique identifier, e.g. "usbCoral" */
+ id: string;
+ /** Display label */
+ label: string;
+ /**
+ * Description shown below the checkbox when this option is enabled.
+ * Supports markdown link syntax: [text](url)
+ */
+ description?: string;
+ /** Device IDs that disable this option */
+ disabledWhen?: string[];
+ /** Device mappings added when this option is enabled */
+ devices?: DeviceMapping[];
+ /** Volume mappings added when this option is enabled */
+ volumes?: VolumeMapping[];
+ /** Extra environment variables */
+ env?: Record;
+}
+
+/** Port definition */
+export interface PortConfig {
+ /** Unique identifier (also the default host port as string) */
+ id: string;
+ /** Host port number */
+ host: number;
+ /** Container port number */
+ container: number;
+ /** Protocol */
+ protocol?: "tcp" | "udp";
+ /** Description of the port's purpose */
+ description: string;
+ /** Whether enabled by default */
+ defaultEnabled: boolean;
+ /** Whether this port is locked (always enabled, cannot be toggled off) */
+ locked?: boolean;
+ /** Admonition type for the warning */
+ warningType?: "warning" | "danger";
+ /** Warning content (markdown) */
+ warningContent?: string;
+ /** When to show the warning: when the port is checked or unchecked */
+ warningWhen?: "checked" | "unchecked";
+}
diff --git a/docs/src/components/DockerComposeGenerator/generator/index.ts b/docs/src/components/DockerComposeGenerator/generator/index.ts
new file mode 100644
index 0000000000..f6091f7c01
--- /dev/null
+++ b/docs/src/components/DockerComposeGenerator/generator/index.ts
@@ -0,0 +1,250 @@
+import type {
+ DeviceConfig,
+ DeviceMapping,
+ VolumeMapping,
+} from "../config/types";
+import { hardwareMap } from "../config";
+
+// ---------------------------------------------------------------------------
+// Input type
+// ---------------------------------------------------------------------------
+
+export interface GeneratorInput {
+ device: DeviceConfig;
+ selectedHardware: string[];
+ enabledPorts: string[];
+ configPath: string;
+ mediaPath: string;
+ rtspPassword?: string;
+ timezone: string;
+ shmSize: string;
+ nvidiaGpuCount?: string;
+ nvidiaGpuDeviceId?: string;
+}
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+function deviceLine(dm: DeviceMapping): string {
+ const host = dm.host;
+ const container = dm.container ?? dm.host;
+ const mapping = host === container ? host : `${host}:${container}`;
+ const comment = dm.comment ? ` # ${dm.comment}` : "";
+ return ` - ${mapping}${comment}`;
+}
+
+function volumeLine(vm: VolumeMapping): string {
+ const ro = vm.readOnly ? ":ro" : "";
+ const comment = vm.comment ? ` # ${vm.comment}` : "";
+ return ` - ${vm.host}:${vm.container}${ro}${comment}`;
+}
+
+// ---------------------------------------------------------------------------
+// YAML builder — each section returns an array of lines
+// ---------------------------------------------------------------------------
+
+function buildImage(device: DeviceConfig): string[] {
+ const tag = device.imageTagSuffix
+ ? `${device.imageTag}${device.imageTagSuffix}`
+ : device.imageTag;
+ return [` image: ghcr.io/blakeblackshear/frigate:${tag}`];
+}
+
+function buildDevices(
+ device: DeviceConfig,
+ hwDevices: DeviceMapping[]
+): string[] {
+ const all: DeviceMapping[] = [
+ ...(device.devices ?? []),
+ ...hwDevices,
+ ];
+ if (all.length === 0) return [];
+ return [
+ " devices:",
+ ...all.map(deviceLine),
+ ];
+}
+
+function buildVolumes(
+ device: DeviceConfig,
+ hwVolumes: VolumeMapping[],
+ configPath: string,
+ mediaPath: string
+): string[] {
+ const all: VolumeMapping[] = [
+ ...(device.volumes ?? []),
+ ...hwVolumes,
+ ];
+ return [
+ " volumes:",
+ " - /etc/localtime:/etc/localtime:ro # Sync host time",
+ ` - ${configPath}:/config # Config file directory`,
+ ` - ${mediaPath}:/media/frigate # Recording storage directory`,
+ " - type: tmpfs # 1GB in-memory filesystem for recording segment storage",
+ " target: /tmp/cache",
+ " tmpfs:",
+ " size: 1000000000",
+ ...all.map(volumeLine),
+ ];
+}
+
+function buildPorts(enabledPorts: string[]): string[] {
+ return [
+ " ports:",
+ ...enabledPorts,
+ ];
+}
+
+function buildEnvironment(
+ device: DeviceConfig,
+ hwEnv: Record,
+ rtspPassword: string | undefined,
+ timezone: string
+): string[] {
+ const allEnv: Record = {
+ ...hwEnv,
+ ...(device.env ?? {}),
+ };
+
+ const lines: string[] = [" environment:"];
+
+ if (rtspPassword) {
+ lines.push(
+ ` FRIGATE_RTSP_PASSWORD: "${rtspPassword}" # RTSP password — change to your own`
+ );
+ }
+
+ lines.push(` TZ: "${timezone}" # Timezone`);
+
+ for (const [key, value] of Object.entries(allEnv)) {
+ lines.push(` ${key}: "${value}"`);
+ }
+
+ return lines;
+}
+
+function buildDeploy(device: DeviceConfig, input: GeneratorInput): string[] {
+ if (device.id === "stable-tensorrt") {
+ const count = input.nvidiaGpuCount || "all";
+ const isAll = count === "all";
+ const deviceId = input.nvidiaGpuDeviceId?.trim();
+
+ if (isAll) {
+ return [
+ " deploy:",
+ " resources:",
+ " reservations:",
+ " devices:",
+ " - driver: nvidia",
+ " count: all # Use all GPUs",
+ " capabilities: [gpu]",
+ ];
+ }
+
+ if (deviceId) {
+ const ids = deviceId
+ .split(",")
+ .map((s) => s.trim())
+ .filter(Boolean)
+ .map((s) => `'${s}'`)
+ .join(", ");
+ return [
+ " deploy:",
+ " resources:",
+ " reservations:",
+ " devices:",
+ " - driver: nvidia",
+ ` device_ids: [${ids}] # GPU device IDs`,
+ ` count: ${count} # GPU count`,
+ " capabilities: [gpu]",
+ ];
+ }
+
+ return [
+ " deploy:",
+ " resources:",
+ " reservations:",
+ " devices:",
+ " - driver: nvidia",
+ ` count: ${count} # GPU count`,
+ " capabilities: [gpu]",
+ ];
+ }
+
+ return [];
+}
+
+function buildRuntime(device: DeviceConfig): string[] {
+ if (device.runtime) {
+ return [` runtime: ${device.runtime}`];
+ }
+ return [];
+}
+
+function buildExtraHosts(device: DeviceConfig): string[] {
+ if (!device.extraHosts?.length) return [];
+ return [
+ " extra_hosts:",
+ ...device.extraHosts.map(
+ (h, i) =>
+ ` - "${h}"${i === 0 ? " # Required to talk to the NPU detector" : ""}`
+ ),
+ ];
+}
+
+function buildSecurityOpt(device: DeviceConfig): string[] {
+ if (!device.securityOpt?.length) return [];
+ return [
+ " security_opt:",
+ ...device.securityOpt.map((s) => ` - ${s}`),
+ ];
+}
+
+// ---------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------
+
+/**
+ * Generate a docker-compose YAML string from the given input.
+ * The output is pure YAML with inline comments (no Shiki annotations).
+ */
+export function generateDockerCompose(input: GeneratorInput): string {
+ const { device } = input;
+
+ // Collect hardware-level devices, volumes, and env
+ const hwDevices: DeviceMapping[] = [];
+ const hwVolumes: VolumeMapping[] = [];
+ const hwEnv: Record = {};
+
+ for (const hwId of input.selectedHardware) {
+ const hw = hardwareMap.get(hwId);
+ if (!hw) continue;
+ // Skip GPU device mapping for tensorrt images (it uses deploy instead)
+ if (hw.id === "gpu" && device.imageTag === "stable-tensorrt") continue;
+ hwDevices.push(...(hw.devices ?? []));
+ hwVolumes.push(...(hw.volumes ?? []));
+ Object.assign(hwEnv, hw.env ?? {});
+ }
+
+ const lines: string[] = [
+ "services:",
+ " frigate:",
+ " container_name: frigate",
+ " privileged: true # This may not be necessary for all setups",
+ " restart: unless-stopped",
+ " stop_grace_period: 30s # Allow enough time to shut down the various services",
+ ...buildImage(device),
+ ` shm_size: "${input.shmSize || "512mb"}" # Update for your cameras based on SHM calculation`,
+ ...buildRuntime(device),
+ ...buildDeploy(device, input),
+ ...buildExtraHosts(device),
+ ...buildSecurityOpt(device),
+ ...buildDevices(device, hwDevices),
+ ...buildVolumes(device, hwVolumes, input.configPath, input.mediaPath),
+ ...buildPorts(input.enabledPorts),
+ ...buildEnvironment(device, hwEnv, input.rtspPassword, input.timezone),
+ ];
+
+ return lines.join("\n");
+}
diff --git a/docs/src/components/DockerComposeGenerator/hooks/useConfigGenerator.ts b/docs/src/components/DockerComposeGenerator/hooks/useConfigGenerator.ts
new file mode 100644
index 0000000000..19c3976d8d
--- /dev/null
+++ b/docs/src/components/DockerComposeGenerator/hooks/useConfigGenerator.ts
@@ -0,0 +1,195 @@
+import { useState, useCallback, useMemo } from "react";
+import { deviceMap, hardwareMap, portMap } from "../config";
+import { generateDockerCompose } from "../generator";
+import type { GeneratorInput } from "../generator";
+
+/**
+ * Main hook that holds all form state and generates the Docker Compose output.
+ * Configuration is loaded synchronously from build-time generated .ts files.
+ */
+export function useConfigGenerator() {
+ const [deviceId, setDeviceId] = useState("stable");
+
+ const [hardwareEnabled, setHardwareEnabled] = useState>(() => {
+ const defaultDevice = deviceMap.get("stable");
+ const initial: Record = {};
+ if (defaultDevice) {
+ for (const hwId of defaultDevice.autoHardware) {
+ initial[hwId] = true;
+ }
+ }
+ return initial;
+ });
+
+ const [portEnabled, setPortEnabled] = useState>(() => {
+ const initial: Record = {};
+ for (const p of portMap.values()) {
+ initial[p.id] = p.defaultEnabled;
+ }
+ return initial;
+ });
+
+ const [nvidiaGpuCount, setNvidiaGpuCount] = useState("");
+ const [nvidiaGpuDeviceId, setNvidiaGpuDeviceId] = useState("");
+ const [configPath, setConfigPath] = useState("");
+ const [mediaPath, setMediaPath] = useState("");
+ const [rtspPassword, setRtspPassword] = useState("");
+ const [timezone, setTimezone] = useState("");
+ const [shmSize, setShmSize] = useState("512mb");
+ const [shmSizeError, setShmSizeError] = useState(false);
+ const [gpuDeviceIdError, setGpuDeviceIdError] = useState(false);
+ const [configPathError, setConfigPathError] = useState(false);
+ const [mediaPathError, setMediaPathError] = useState(false);
+
+ const device = useMemo(() => deviceMap.get(deviceId)!, [deviceId]);
+
+ const selectDevice = useCallback((id: string) => {
+ const newDevice = deviceMap.get(id);
+ if (!newDevice) return;
+ setDeviceId(id);
+ setHardwareEnabled(() => {
+ const next: Record = {};
+ for (const hwId of newDevice.autoHardware) {
+ next[hwId] = true;
+ }
+ return next;
+ });
+ setNvidiaGpuCount("");
+ setNvidiaGpuDeviceId("");
+ setGpuDeviceIdError(false);
+ }, []);
+
+ const toggleHardware = useCallback((hwId: string) => {
+ setHardwareEnabled((prev) => ({ ...prev, [hwId]: !prev[hwId] }));
+ }, []);
+
+ const togglePort = useCallback((portId: string) => {
+ const port = portMap.get(portId);
+ if (port?.locked) return;
+ setPortEnabled((prev) => ({ ...prev, [portId]: !prev[portId] }));
+ }, []);
+
+ const isHardwareDisabled = useCallback(
+ (hwId: string): boolean => {
+ const hw = hardwareMap.get(hwId);
+ if (!hw) return false;
+ return hw.disabledWhen?.includes(deviceId) ?? false;
+ },
+ [deviceId]
+ );
+
+ const validateShmSize = useCallback((value: string): boolean => {
+ if (!value) return true;
+ return /^\d+(\.\d+)?[bkmgBKMG]{1,2}$/.test(value);
+ }, []);
+
+ const validatePath = useCallback((value: string): boolean => {
+ if (!value) return true;
+ return /^[a-zA-Z0-9_\-/./]+$/.test(value);
+ }, []);
+
+ const handleShmSizeChange = useCallback(
+ (value: string) => {
+ const filtered = value.replace(/[^0-9.bkmgBKMG]/g, "");
+ const valid = validateShmSize(filtered);
+ setShmSize(filtered);
+ setShmSizeError(!valid && filtered !== "");
+ },
+ [validateShmSize]
+ );
+
+ const handleConfigPathChange = useCallback(
+ (value: string) => {
+ const filtered = value.replace(/[^a-zA-Z0-9_\-/./]/g, "");
+ const valid = validatePath(filtered);
+ setConfigPath(filtered);
+ setConfigPathError(!valid && filtered !== "");
+ },
+ [validatePath]
+ );
+
+ const handleMediaPathChange = useCallback(
+ (value: string) => {
+ const filtered = value.replace(/[^a-zA-Z0-9_\-/./]/g, "");
+ const valid = validatePath(filtered);
+ setMediaPath(filtered);
+ setMediaPathError(!valid && filtered !== "");
+ },
+ [validatePath]
+ );
+
+ const handleNvidiaGpuCountChange = useCallback((value: string) => {
+ // Only allow digits
+ setNvidiaGpuCount(value);
+ if (value === "") {
+ setNvidiaGpuDeviceId("");
+ setGpuDeviceIdError(false);
+ } else {
+ setGpuDeviceIdError(false);
+ }
+ }, []);
+
+ const handleNvidiaGpuDeviceIdChange = useCallback((value: string) => {
+ setNvidiaGpuDeviceId(value.trim());
+ setGpuDeviceIdError(false);
+ }, []);
+
+ const enabledPortLines = useMemo(() => {
+ const lines: string[] = [];
+ for (const [id, enabled] of Object.entries(portEnabled)) {
+ if (!enabled) continue;
+ const p = portMap.get(id);
+ if (!p) continue;
+ const proto = p.protocol && p.protocol !== "tcp" ? `/${p.protocol}` : "";
+ const comment = p.description ? ` # ${p.description}` : "";
+ lines.push(` - "${p.host}:${p.container}${proto}"${comment}`);
+ }
+ return lines;
+ }, [portEnabled]);
+
+ const selectedHardwareIds = useMemo(() => {
+ return Object.entries(hardwareEnabled)
+ .filter(([id, enabled]) => {
+ if (!enabled) return false;
+ const hw = hardwareMap.get(id);
+ if (!hw) return false;
+ if (hw.disabledWhen?.includes(deviceId)) return false;
+ return true;
+ })
+ .map(([id]) => id);
+ }, [hardwareEnabled, deviceId]);
+
+ const generatedYaml = useMemo(() => {
+ const input: GeneratorInput = {
+ device,
+ selectedHardware: selectedHardwareIds,
+ enabledPorts: enabledPortLines,
+ configPath: configPath || "/path/to/your/config",
+ mediaPath: mediaPath || "/path/to/your/storage",
+ rtspPassword,
+ timezone: timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || "Etc/UTC",
+ shmSize: shmSize || "512mb",
+ nvidiaGpuCount,
+ nvidiaGpuDeviceId,
+ };
+ return generateDockerCompose(input);
+ }, [
+ device, selectedHardwareIds, enabledPortLines,
+ configPath, mediaPath, rtspPassword, timezone, shmSize,
+ nvidiaGpuCount, nvidiaGpuDeviceId,
+ ]);
+
+ const hasAnyHardware = selectedHardwareIds.length > 0 || !!device?.devices?.length;
+
+ return {
+ deviceId, device, hardwareEnabled, portEnabled,
+ nvidiaGpuCount, nvidiaGpuDeviceId,
+ configPath, mediaPath, rtspPassword, timezone, shmSize,
+ shmSizeError, gpuDeviceIdError, configPathError, mediaPathError,
+ hasAnyHardware, generatedYaml,
+ selectDevice, toggleHardware, togglePort,
+ handleShmSizeChange, handleConfigPathChange, handleMediaPathChange,
+ handleNvidiaGpuCountChange, handleNvidiaGpuDeviceIdChange,
+ setRtspPassword, setTimezone, isHardwareDisabled,
+ };
+}
diff --git a/docs/src/components/DockerComposeGenerator/index.ts b/docs/src/components/DockerComposeGenerator/index.ts
new file mode 100644
index 0000000000..76dd587560
--- /dev/null
+++ b/docs/src/components/DockerComposeGenerator/index.ts
@@ -0,0 +1 @@
+export { default } from "./DockerComposeGenerator";
diff --git a/docs/src/components/DockerComposeGenerator/styles.module.css b/docs/src/components/DockerComposeGenerator/styles.module.css
new file mode 100644
index 0000000000..d2e1b62dd8
--- /dev/null
+++ b/docs/src/components/DockerComposeGenerator/styles.module.css
@@ -0,0 +1,381 @@
+/* ===================================================================
+ Docker Compose Generator — styles
+ Uses Docusaurus / Infima CSS variables for theme compatibility.
+ =================================================================== */
+
+.generator {
+ margin: 2rem 0;
+}
+
+.card {
+ background: var(--ifm-background-surface-color);
+ border: 1px solid var(--ifm-color-emphasis-400);
+ border-radius: 12px;
+ padding: 2rem;
+ box-shadow: var(--ifm-global-shadow-lw);
+}
+
+[data-theme="light"] .card {
+ background: var(--ifm-color-emphasis-100);
+ border: 1px solid var(--ifm-color-emphasis-300);
+}
+
+/* --- Form sections --- */
+
+.formSection {
+ margin-bottom: 1.5rem;
+ padding-bottom: 1.5rem;
+ border-bottom: 1px solid var(--ifm-color-emphasis-400);
+}
+
+.formSection:last-child {
+ border-bottom: none;
+ margin-bottom: 0;
+ padding-bottom: 0;
+}
+
+.formSection h4 {
+ margin: 0 0 1rem 0;
+ color: var(--ifm-font-color-base);
+ font-size: 1.1rem;
+ font-weight: var(--ifm-font-weight-semibold);
+}
+
+/* --- Form controls --- */
+
+.formGroup {
+ margin-bottom: 1rem;
+}
+
+.formGroup:last-child {
+ margin-bottom: 0;
+}
+
+.label {
+ display: block;
+ margin-bottom: 0.25rem;
+ color: var(--ifm-font-color-base);
+ font-weight: var(--ifm-font-weight-semibold);
+ font-size: 0.9rem;
+}
+
+.input {
+ width: 100%;
+ padding: 0.5rem 0.75rem;
+ border: 1px solid var(--ifm-color-emphasis-400);
+ border-radius: 6px;
+ background: var(--ifm-background-color);
+ color: var(--ifm-font-color-base);
+ font-size: 0.95rem;
+ transition: border-color 0.2s, box-shadow 0.2s;
+}
+
+[data-theme="light"] .input {
+ background: #fff;
+ border: 1px solid #d0d7de;
+}
+
+.input:focus {
+ outline: none;
+ border-color: var(--ifm-color-primary);
+ box-shadow: 0 0 0 3px var(--ifm-color-primary-lightest);
+}
+
+[data-theme="dark"] .input {
+ border-color: var(--ifm-color-emphasis-300);
+}
+
+.inputError {
+ border-color: #e74c3c;
+ animation: shake 0.3s ease-in-out;
+}
+
+@keyframes shake {
+ 0%,
+ 100% {
+ transform: translateX(0);
+ }
+ 25% {
+ transform: translateX(-5px);
+ }
+ 75% {
+ transform: translateX(5px);
+ }
+}
+
+/* --- Select dropdown --- */
+
+.select {
+ cursor: pointer;
+ appearance: none;
+ -moz-appearance: none;
+ -webkit-appearance: none;
+ background: var(--ifm-background-color)
+ url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 8L1 3h10z'/%3E%3C/svg%3E")
+ no-repeat right 0.75rem center / 12px 12px;
+ padding-right: 2rem;
+}
+
+[data-theme="light"] .select {
+ background: #fff
+ url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23555' d='M6 8L1 3h10z'/%3E%3C/svg%3E")
+ no-repeat right 0.75rem center / 12px 12px;
+}
+
+.helpText {
+ margin: 0.5rem 0 0 0;
+ font-size: 0.85rem;
+ color: var(--ifm-font-color-secondary);
+ line-height: 1.5;
+}
+
+.helpText a {
+ color: var(--ifm-color-primary);
+}
+
+/* --- Device grid --- */
+
+.deviceGrid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
+ gap: 0.75rem;
+ margin-top: 0.5rem;
+}
+
+.deviceCard {
+ padding: 0.75rem;
+ border: 2px solid var(--ifm-color-emphasis-400);
+ border-radius: 12px;
+ cursor: pointer;
+ transition: all 0.2s;
+ text-align: center;
+ background: var(--ifm-background-color);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+[data-theme="light"] .deviceCard {
+ border: 2px solid #d0d7de;
+ background: #fff;
+}
+
+.deviceCard:hover {
+ border-color: var(--ifm-color-primary);
+ background: var(--ifm-color-emphasis-100);
+ transform: translateY(-2px);
+}
+
+.deviceCardActive {
+ border-color: var(--ifm-color-primary);
+ background: var(--ifm-color-primary-lightest);
+ box-shadow: 0 0 0 1px var(--ifm-color-primary);
+}
+
+[data-theme="light"] .deviceCardActive {
+ background: color-mix(in srgb, var(--ifm-color-primary) 12%, #fff);
+}
+
+[data-theme="dark"] .deviceCardActive {
+ background: color-mix(in srgb, var(--ifm-color-primary) 25%, #1b1b1b);
+}
+
+[data-theme="dark"] .deviceCardActive .deviceName {
+ color: var(--ifm-color-primary-light);
+}
+
+[data-theme="dark"] .deviceCardActive .deviceDesc {
+ color: var(--ifm-color-primary-light);
+ opacity: 0.85;
+}
+
+.deviceIcon {
+ font-size: 2rem;
+ margin-bottom: 0.25rem;
+ height: 40px;
+ width: 50px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.deviceIconSvg {
+ margin-bottom: 0.25rem;
+ height: 40px;
+ width: 50px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: visible;
+ /* Allow iconStyle width/height to override */
+ flex-shrink: 0;
+}
+
+.deviceIconSvg svg {
+ width: var(--svg-width, 100%);
+ height: var(--svg-height, 100%);
+ fill: var(--svg-fill, currentColor);
+ transform: var(--svg-transform, none);
+}
+
+.deviceIconImage {
+ margin-bottom: 0.25rem;
+ height: 40px;
+ width: 50px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.deviceIconImage img {
+ max-width: 100%;
+ max-height: 100%;
+ object-fit: contain;
+}
+
+.deviceName {
+ font-weight: var(--ifm-font-weight-semibold);
+ color: var(--ifm-font-color-base);
+ margin-bottom: 0.15rem;
+ font-size: 0.9rem;
+}
+
+.deviceDesc {
+ font-size: 0.75rem;
+ color: var(--ifm-font-color-secondary);
+ line-height: 1.3;
+}
+
+/* --- Checkbox grid --- */
+
+.checkboxGrid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 0.5rem;
+}
+
+@media (max-width: 576px) {
+ .checkboxGrid {
+ grid-template-columns: 1fr;
+ }
+}
+
+.hardwareItem {
+ margin-bottom: 0;
+}
+
+.hardwareDescription {
+ margin: 0.15rem 0 0.4rem 1.6rem;
+ font-size: 0.8rem;
+ color: var(--ifm-font-color-secondary);
+ line-height: 1.5;
+}
+
+.hardwareDescription a {
+ color: var(--ifm-color-primary);
+ text-decoration: underline;
+ text-underline-offset: 2px;
+}
+
+.checkboxLabel {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ cursor: pointer;
+ padding: 0.4rem 0.5rem;
+ border-radius: 6px;
+ transition: background-color 0.2s;
+ font-size: 0.9rem;
+}
+
+.checkboxLabel:hover {
+ background: var(--ifm-color-emphasis-100);
+}
+
+.checkboxLabel input[type="checkbox"] {
+ width: 1.1rem;
+ height: 1.1rem;
+ cursor: pointer;
+ flex-shrink: 0;
+}
+
+.checkboxLabel span {
+ color: var(--ifm-font-color-base);
+}
+
+.checkboxDisabled {
+ cursor: not-allowed;
+}
+
+.checkboxDisabled:hover {
+ background: transparent;
+}
+
+.checkboxDisabled input[type="checkbox"] {
+ cursor: not-allowed;
+ opacity: 0.5;
+}
+
+/* --- Form grid (side-by-side) --- */
+
+.formGrid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 1rem;
+}
+
+@media (max-width: 576px) {
+ .formGrid {
+ grid-template-columns: 1fr;
+ }
+}
+
+.formGrid .formGroup {
+ margin-bottom: 0;
+}
+
+/* --- Port section --- */
+
+.portSection {
+ margin-bottom: 0.75rem;
+}
+
+.warningBadge {
+ margin-left: auto;
+ color: #e67e22;
+ font-size: 0.85rem;
+}
+
+/* --- NVIDIA config --- */
+
+.nvidiaConfig {
+ margin-top: 1rem;
+ margin-bottom: 1.5rem;
+ padding: 1rem;
+ background: var(--ifm-background-color);
+ border-radius: 8px;
+ border-left: 3px solid var(--ifm-color-primary);
+}
+
+[data-theme="light"] .nvidiaConfig {
+ background: #f6f8fa;
+ border-left: 3px solid var(--ifm-color-primary);
+}
+
+/* --- Result section --- */
+
+.resultSection {
+ margin-top: 2rem;
+}
+
+.resultHeader {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1rem;
+}
+
+.resultHeader h4 {
+ margin: 0;
+ color: var(--ifm-font-color-base);
+}
diff --git a/docs/src/components/FaqItem/index.jsx b/docs/src/components/FaqItem/index.jsx
new file mode 100644
index 0000000000..af70d1100f
--- /dev/null
+++ b/docs/src/components/FaqItem/index.jsx
@@ -0,0 +1,66 @@
+import React, { useState, useEffect } from "react";
+import Heading from "@theme/Heading";
+import styles from "./styles.module.css";
+
+// A single FAQ entry.
+//
+// The question is a real anchored heading (via @theme/Heading), so on desktop
+// it gets the standard hover "#" hash link and the answer is always shown. On
+// mobile the heading text is a button that toggles its answer, keeping long
+// FAQ pages short. The desktop/mobile split is pure CSS (Docusaurus breakpoint:
+// 996px), so there is no hydration flash. The answer is always rendered into
+// the DOM, so search engines and the docs AI bot can read it regardless of
+// layout or collapsed state. The heading id resolves deep links on both layouts
+// and auto-expands the entry on mobile when it is the link target.
+export default function FaqItem({ id, question, children }) {
+ const [open, setOpen] = useState(false);
+
+ useEffect(() => {
+ const openIfTargeted = () => {
+ if (window.location.hash === `#${id}`) {
+ setOpen(true);
+ }
+ };
+ openIfTargeted();
+ window.addEventListener("hashchange", openIfTargeted);
+ return () => window.removeEventListener("hashchange", openIfTargeted);
+ }, [id]);
+
+ const toggle = () => {
+ const next = !open;
+ setOpen(next);
+ // Reflect the entry in the URL like clicking the heading anchor, so an
+ // opened answer is shareable. Use replaceState to avoid history spam and
+ // an abrupt scroll. Clear it on close if it currently points here.
+ if (next) {
+ if (window.location.hash !== `#${id}`) {
+ window.history.replaceState(null, "", `#${id}`);
+ }
+ } else if (window.location.hash === `#${id}`) {
+ window.history.replaceState(
+ null,
+ "",
+ window.location.pathname + window.location.search,
+ );
+ }
+ };
+
+ return (
+
+
+
+ {question}
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/docs/src/components/FaqItem/styles.module.css b/docs/src/components/FaqItem/styles.module.css
new file mode 100644
index 0000000000..bf348dc88a
--- /dev/null
+++ b/docs/src/components/FaqItem/styles.module.css
@@ -0,0 +1,93 @@
+/*
+ * FAQ entry: collapsible on mobile, static heading + expanded answer on
+ * desktop. The split is pure CSS (Docusaurus breakpoint: 996px) so there is
+ * no hydration flash. The answer is always rendered into the DOM, so search
+ * engines and the docs AI bot can read it regardless of layout or state.
+ */
+
+.item {
+ scroll-margin-top: calc(var(--ifm-navbar-height) + 1rem);
+}
+
+.heading {
+ margin: 0;
+}
+
+/* Mobile: the heading text is a full-width clickable toggle row. */
+.toggle {
+ display: flex;
+ align-items: center;
+ gap: 0.6rem;
+ width: 100%;
+ padding: 0.85rem 0;
+ border: none;
+ border-bottom: 1px solid var(--ifm-color-emphasis-200);
+ background: none;
+ color: inherit;
+ font: inherit;
+ text-align: left;
+ cursor: pointer;
+}
+
+.toggle::before {
+ content: "";
+ flex: 0 0 auto;
+ width: 0.5rem;
+ height: 0.5rem;
+ border-right: 2px solid currentColor;
+ border-bottom: 2px solid currentColor;
+ transform: rotate(-45deg);
+ transition: transform var(--ifm-transition-fast, 200ms) ease;
+}
+
+.item[data-open] .toggle::before {
+ transform: rotate(45deg);
+}
+
+.question {
+ flex: 1;
+ min-width: 0;
+}
+
+.content {
+ display: none;
+ padding: 0 0 0.85rem;
+}
+
+.item[data-open] .content {
+ display: block;
+}
+
+/* Hide the hover hash link on mobile (no hover; avoids a stray empty line). */
+.heading :global(.hash-link) {
+ display: none;
+}
+
+/* Desktop: render as a normal expanded heading + answer. */
+@media (min-width: 997px) {
+ .heading {
+ margin: 1.75rem 0 0.85rem;
+ }
+
+ .toggle {
+ display: inline;
+ width: auto;
+ padding: 0;
+ border: none;
+ cursor: default;
+ }
+
+ .toggle::before {
+ display: none;
+ }
+
+ .content {
+ display: block;
+ padding: 0 0 0.5rem 1rem;
+ border-left: 2px solid var(--ifm-color-emphasis-200);
+ }
+
+ .heading :global(.hash-link) {
+ display: inline;
+ }
+}
diff --git a/docs/src/components/ModelConfigDropdown/index.jsx b/docs/src/components/ModelConfigDropdown/index.jsx
new file mode 100644
index 0000000000..737f47ec23
--- /dev/null
+++ b/docs/src/components/ModelConfigDropdown/index.jsx
@@ -0,0 +1,171 @@
+import React, { useState } from "react";
+import CodeBlock from "@theme/CodeBlock";
+import ConfigTabs from "@site/src/components/ConfigTabs";
+import TabItem from "@theme/TabItem";
+import { marked } from "marked";
+import styles from "./styles.module.css";
+
+marked.setOptions({ gfm: true });
+
+/**
+ * @typedef {Object} Model
+ * @property {string} key
+ * @property {string} label
+ * @property {boolean} recommended
+ * @property {string} download Markdown for the "download the model" step.
+ * @property {string} ui Markdown for the Frigate UI configuration step.
+ * @property {string} yaml Raw YAML for the configuration step.
+ */
+
+// Render a markdown string to React nodes. Fenced code blocks become Docusaurus
+// CodeBlock components (so they get syntax highlighting and a copy button);
+// everything else is marked-parsed to HTML.
+function renderBlocks(md, keyPrefix) {
+ if (!md.trim()) return [];
+ const tokens = marked.lexer(md);
+ const nodes = [];
+ let buffer = [];
+ let idx = 0;
+
+ const flush = () => {
+ if (buffer.length) {
+ buffer.links = tokens.links;
+ nodes.push(
+
,
+ );
+ buffer = [];
+ }
+ };
+
+ tokens.forEach((token) => {
+ if (token.type === "code") {
+ flush();
+ const language = (token.lang || "text").split(/\s+/)[0];
+ nodes.push(
+
+ {token.text}
+ ,
+ );
+ } else {
+ buffer.push(token);
+ }
+ });
+ flush();
+ return nodes;
+}
+
+// marked does not understand Docusaurus admonitions (:::warning ... :::), so
+// render those blocks ourselves and render everything around them normally.
+function renderMarkdown(md) {
+ if (!md) return null;
+ const admonition = /:::(\w+)[ \t]*([^\n]*)\n([\s\S]*?)\n:::/g;
+ const nodes = [];
+ let lastIndex = 0;
+ let match;
+ let k = 0;
+ while ((match = admonition.exec(md)) !== null) {
+ nodes.push(...renderBlocks(md.slice(lastIndex, match.index), `seg${k}`));
+ const [, type, title, body] = match;
+ const heading = (title || type).trim();
+ nodes.push(
+
+
{heading}
+ {renderBlocks(body, `adm${k}`)}
+
,
+ );
+ lastIndex = admonition.lastIndex;
+ k++;
+ }
+ nodes.push(...renderBlocks(md.slice(lastIndex), `seg${k}`));
+ return nodes;
+}
+
+function Markdown({ children }) {
+ return {renderMarkdown(children)}
;
+}
+
+function RecommendedBadge() {
+ return Recommended ;
+}
+
+/**
+ * @param {{ models: Model[] }} props
+ */
+export default function ModelConfigDropdown({ models }) {
+ const [selectedModelIndex, setSelectedModelIndex] = useState(0);
+ const [isOpen, setIsOpen] = useState(false);
+
+ const selectedModel = models[selectedModelIndex];
+ const hasChoices = models.length > 1;
+
+ const handleModelSelect = (index) => {
+ setSelectedModelIndex(index);
+ setIsOpen(false);
+ };
+
+ return (
+
+
+
+
Step 1 — Choose a model
+
setIsOpen(!isOpen) : undefined}
+ >
+
+
+ {selectedModel.label}
+ {selectedModel.recommended && }
+
+ {hasChoices && (
+ {isOpen ? "▲" : "▼"}
+ )}
+
+
+
+ {isOpen && hasChoices && (
+
+ {models.map((model, index) => (
+
handleModelSelect(index)}
+ >
+ {model.label}
+ {model.recommended && }
+
+ ))}
+
+ )}
+
+
+
+
Step 2 — Download the model
+ {selectedModel.download}
+
+
+
+
Step 3 — Configure the detector
+
+
+ {selectedModel.ui}
+
+
+ {selectedModel.yaml}
+
+
+
+
+
+ );
+}
diff --git a/docs/src/components/ModelConfigDropdown/styles.module.css b/docs/src/components/ModelConfigDropdown/styles.module.css
new file mode 100644
index 0000000000..06f0333344
--- /dev/null
+++ b/docs/src/components/ModelConfigDropdown/styles.module.css
@@ -0,0 +1,275 @@
+/* ===================================================================
+ ModelConfigDropdown — styles
+ =================================================================== */
+
+.wrapper {
+ margin: 1.5rem 0;
+}
+
+/* --- Dropdown button --- */
+
+.dropdown {
+ display: inline-block;
+ width: 360px;
+ max-width: 100%;
+ text-align: left;
+ border: 1px solid var(--ifm-color-emphasis-400);
+ border-radius: 8px;
+ background: var(--ifm-background-color);
+ cursor: pointer;
+ transition:
+ border-color 0.2s,
+ box-shadow 0.2s;
+}
+
+[data-theme="light"] .dropdown {
+ border: 1px solid #d0d7de;
+ background: #fff;
+}
+
+[data-theme="dark"] .dropdown {
+ border: 1px solid var(--ifm-color-emphasis-300);
+ background: #21262d;
+}
+
+.dropdown:hover {
+ border-color: var(--ifm-color-primary);
+ box-shadow: 0 0 0 3px var(--ifm-color-primary-lightest);
+}
+
+[data-theme="dark"] .dropdown:hover {
+ box-shadow: 0 0 0 3px var(--ifm-color-primary-lightest);
+}
+
+.dropdown.open {
+ border-color: var(--ifm-color-primary);
+ box-shadow: 0 0 0 3px var(--ifm-color-primary-lightest);
+}
+
+[data-theme="dark"] .dropdown.open {
+ border-color: var(--ifm-color-primary);
+}
+
+/* Single-model detectors render the label without a clickable menu. */
+.dropdown.static {
+ cursor: default;
+}
+
+.dropdown.static:hover {
+ border-color: var(--ifm-color-emphasis-400);
+ box-shadow: none;
+}
+
+[data-theme="light"] .dropdown.static:hover {
+ border-color: #d0d7de;
+}
+
+[data-theme="dark"] .dropdown.static:hover {
+ border-color: var(--ifm-color-emphasis-300);
+}
+
+.dropdownContent {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 1rem;
+ padding: 0.8rem 1rem;
+}
+
+/* --- Model menu --- */
+
+.menu {
+ margin-top: 0.25rem;
+ width: 360px;
+ max-width: 100%;
+ border: 1px solid var(--ifm-color-emphasis-400);
+ border-radius: 8px;
+ overflow: hidden;
+ background: var(--ifm-background-color);
+}
+
+[data-theme="light"] .menu {
+ border: 1px solid #d0d7de;
+ background: #fff;
+}
+
+[data-theme="dark"] .menu {
+ border: 1px solid var(--ifm-color-emphasis-300);
+ background: #21262d;
+}
+
+.menuItem {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.6rem 1rem;
+ cursor: pointer;
+ font-size: 0.95rem;
+ color: var(--ifm-font-color-base);
+ transition: background 0.15s;
+}
+
+.menuItem:not(:last-child) {
+ border-bottom: 1px solid var(--ifm-color-emphasis-200);
+}
+
+.menuItem:hover {
+ background: var(--ifm-color-emphasis-100);
+}
+
+.menuItemActive {
+ font-weight: var(--ifm-font-weight-semibold);
+ background: var(--ifm-color-primary-lightest);
+}
+
+[data-theme="dark"] .menuItem:hover {
+ background: #2b3139;
+}
+
+[data-theme="dark"] .menuItemActive {
+ background: #2b3139;
+}
+
+.modelName {
+ font-weight: var(--ifm-font-weight-semibold);
+ color: var(--ifm-font-color-base);
+ font-size: 1rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ white-space: nowrap;
+}
+
+.recommendedBadge {
+ display: inline-block;
+ background: var(--ifm-color-success);
+ color: #fff;
+ font-size: 0.7rem;
+ font-weight: 600;
+ padding: 2px 8px;
+ border-radius: 12px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.arrow {
+ font-size: 0.7rem;
+ color: var(--ifm-font-color-secondary);
+ transition: transform 0.2s;
+}
+
+.dropdown.open .arrow {
+ transform: rotate(180deg);
+}
+
+/* --- Panel --- */
+
+.panel {
+ margin-top: 0.5rem;
+ border: 1px solid var(--ifm-color-emphasis-400);
+ border-radius: 8px;
+ overflow: hidden;
+ background: var(--ifm-background-color);
+}
+
+[data-theme="light"] .panel {
+ border: 1px solid #d0d7de;
+ background: #fff;
+}
+
+[data-theme="dark"] .panel {
+ border: 1px solid var(--ifm-color-emphasis-300);
+ background: #21262d;
+}
+
+/* --- Steps --- */
+
+.step {
+ padding: 1rem;
+}
+
+.step:not(:last-child) {
+ border-bottom: 1px solid var(--ifm-color-emphasis-200);
+}
+
+.stepTitle {
+ margin: 0 0 0.75rem 0;
+ font-size: 1rem;
+ font-weight: 600;
+ color: var(--ifm-font-color-base);
+}
+
+/* Rendered markdown (download + Frigate UI instructions). */
+
+.markdown {
+ font-size: 0.9rem;
+ line-height: 1.6;
+}
+
+.markdown > :last-child {
+ margin-bottom: 0;
+}
+
+.markdown a {
+ color: var(--ifm-color-primary);
+ text-decoration: underline;
+ text-underline-offset: 2px;
+}
+
+.markdown table {
+ display: table;
+ width: 100%;
+ margin: 0.75rem 0;
+ font-size: 0.85rem;
+}
+
+/* Docusaurus-style admonitions rendered from markdown. */
+
+.admonition {
+ margin: 0.75rem 0;
+ padding: 0.75rem 1rem;
+ border-left: 4px solid var(--ifm-color-info);
+ border-radius: 4px;
+ background: var(--ifm-color-info-contrast-background);
+ font-size: 0.85rem;
+}
+
+.admonition > :last-child {
+ margin-bottom: 0;
+}
+
+.admonitionTitle {
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ font-size: 0.75rem;
+ margin-bottom: 0.4rem;
+ color: var(--ifm-color-info);
+}
+
+.admonition_warning {
+ border-left-color: var(--ifm-color-warning);
+ background: var(--ifm-color-warning-contrast-background);
+}
+
+.admonition_warning .admonitionTitle {
+ color: var(--ifm-color-warning-dark);
+}
+
+.admonition_danger {
+ border-left-color: var(--ifm-color-danger);
+ background: var(--ifm-color-danger-contrast-background);
+}
+
+.admonition_danger .admonitionTitle {
+ color: var(--ifm-color-danger-dark);
+}
+
+.admonition_tip {
+ border-left-color: var(--ifm-color-success);
+ background: var(--ifm-color-success-contrast-background);
+}
+
+.admonition_tip .admonitionTitle {
+ color: var(--ifm-color-success-dark);
+}
diff --git a/docs/static/frigate-api.yaml b/docs/static/frigate-api.yaml
index 60621ff4e9..af3a019b4c 100644
--- a/docs/static/frigate-api.yaml
+++ b/docs/static/frigate-api.yaml
@@ -1,14 +1,14 @@
+# Generated by generate_api_auth_spec.py — do not edit by hand.
+# Regenerate with: python3 generate_api_auth_spec.py
+# The empty info.title is intentional: a docusaurus-openapi-docs convention
+# that suppresses the generated API introduction page.
openapi: 3.1.0
info:
- # To avoid the introduction page we set the title to empty string
- # https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/blob/4e771d309f6defe395449b26cc3c65814d72cbcc/packages/docusaurus-plugin-openapi-docs/src/openapi/openapi.ts#L92-L129
- title: ""
+ title: ''
version: 0.1.0
-
servers:
- url: https://demo.frigate.video/api
- url: http://localhost:5001/api
-
paths:
/auth/first_time_login:
get:
@@ -16,30 +16,33 @@ paths:
- Auth
summary: First Time Login
description: |-
+ **Access:** Public — no authentication required.
+
Return whether the admin first-time login help flag is set in config.
This endpoint is intentionally unauthenticated so the login page can
query it before a user is authenticated.
operationId: first_time_login_auth_first_time_login_get
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
+ security: []
+ x-required-role: public
/auth:
get:
tags:
- Auth
summary: Authenticate request
- description: >-
- Authenticates the current request based on proxy headers or JWT token.
- This endpoint verifies authentication credentials and manages JWT token
- refresh. On success, no JSON body is returned; authentication state is
- communicated via response headers and cookies.
+ description: |-
+ **Access:** Public — no authentication required.
+
+ Authenticates the current request based on proxy headers or JWT token. This endpoint verifies authentication credentials and manages JWT token refresh. On success, no JSON body is returned; authentication state is communicated via response headers and cookies.
operationId: auth_auth_get
responses:
- "202":
+ '202':
description: Authentication Accepted (no response body)
content:
application/json:
@@ -50,65 +53,67 @@ paths:
schema:
type: string
remote-role:
- description: "Resolved role (e.g., admin, viewer, or custom)"
+ description: Resolved role (e.g., admin, viewer, or custom)
schema:
type: string
Set-Cookie:
description: May include refreshed JWT cookie when applicable
schema:
type: string
- "401":
+ '401':
description: Authentication Failed
+ security: []
+ x-required-role: public
/profile:
get:
tags:
- Auth
summary: Get user profile
- description: >-
- Returns the current authenticated user's profile including username,
- role, and allowed cameras. This endpoint requires authentication and
- returns information about the user's permissions.
+ description: |-
+ **Access:** Any authenticated user.
+
+ Returns the current authenticated user's profile including username, role, and allowed cameras. This endpoint requires authentication and returns information about the user's permissions.
operationId: profile_profile_get
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
/logout:
get:
tags:
- Auth
summary: Logout user
- description: >-
- Logs out the current user by clearing the session cookie. After logout,
- subsequent requests will require re-authentication.
+ description: |-
+ **Access:** Public — no authentication required.
+
+ Logs out the current user by clearing the session cookie. After logout, subsequent requests will require re-authentication.
operationId: logout_logout_get
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
+ security: []
+ x-required-role: public
/login:
post:
tags:
- Auth
summary: Login with credentials
- description: >-
- Authenticates a user with username and password. Returns a JWT token as
- a secure HTTP-only cookie that can be used for subsequent API requests.
- The JWT token can also be retrieved from the response and used as a
- Bearer token in the Authorization header.
+ description: |-
+ **Access:** Public — no authentication required.
+ Authenticates a user with username and password. Returns a JWT token as a secure HTTP-only cookie that can be used for subsequent API requests. The JWT token can also be retrieved from the response and used as a Bearer token in the Authorization header.
Example using Bearer token:
-
```
-
- curl -H "Authorization: Bearer "
- https://frigate_ip:8971/api/profile
-
+ curl -H "Authorization: Bearer " https://frigate_ip:8971/api/profile
```
operationId: login_login_post
requestBody:
@@ -116,68 +121,79 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/AppPostLoginBody"
+ $ref: '#/components/schemas/AppPostLoginBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security: []
+ x-required-role: public
/users:
get:
tags:
- Auth
summary: Get all users
- description: >-
- Returns a list of all users with their usernames and roles. Requires
- admin role. Each user object contains the username and assigned role.
+ description: |-
+ **Access:** Admin role required.
+
+ Returns a list of all users with their usernames and roles. Requires admin role. Each user object contains the username and assigned role.
operationId: get_users_users_get
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
post:
tags:
- Auth
summary: Create new user
- description: >-
- Creates a new user with the specified username, password, and role.
- Requires admin role. Password must be at least 12 characters long.
+ description: |-
+ **Access:** Admin role required.
+
+ Creates a new user with the specified username, password, and role. Requires admin role. Password must be at least 12 characters long.
operationId: create_user_users_post
requestBody:
required: true
content:
application/json:
schema:
- $ref: "#/components/schemas/AppPostUsersBody"
+ $ref: '#/components/schemas/AppPostUsersBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/users/{username}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /users/{username}:
delete:
tags:
- Auth
summary: Delete user
- description: >-
- Deletes a user by username. The built-in admin user cannot be deleted.
- Requires admin role. Returns success message or error if user not found.
+ description: |-
+ **Access:** Admin role required.
+
+ Deletes a user by username. The built-in admin user cannot be deleted. Requires admin role. Returns success message or error if user not found.
operationId: delete_user_users__username__delete
parameters:
- name: username
@@ -187,28 +203,29 @@ paths:
type: string
title: Username
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/users/{username}/password":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /users/{username}/password:
put:
tags:
- Auth
summary: Update user password
- description: >-
- Updates a user's password. Users can only change their own password
- unless they have admin role. Requires the current password to verify
- identity for non-admin users. Password must be at least 12 characters
- long. If user changes their own password, a new JWT cookie is
- automatically issued.
+ description: |-
+ **Access:** Any authenticated user.
+
+ Updates a user's password. Users can only change their own password unless they have admin role. Requires the current password to verify identity for non-admin users. Password must be at least 12 characters long. If user changes their own password, a new JWT cookie is automatically issued.
operationId: update_password_users__username__password_put
parameters:
- name: username
@@ -222,28 +239,31 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/AppPutPasswordBody"
+ $ref: '#/components/schemas/AppPutPasswordBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/users/{username}/role":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
+ /users/{username}/role:
put:
tags:
- Auth
summary: Update user role
- description: >-
- Updates a user's role. The built-in admin user's role cannot be
- modified. Requires admin role. Valid roles are defined in the
- configuration.
+ description: |-
+ **Access:** Admin role required.
+
+ Updates a user's role. The built-in admin user's role cannot be modified. Requires admin role. Valid roles are defined in the configuration.
operationId: update_role_users__username__role_put
parameters:
- name: username
@@ -257,19 +277,22 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/AppPutRoleBody"
+ $ref: '#/components/schemas/AppPutRoleBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/go2rtc/streams:
get:
tags:
@@ -277,44 +300,54 @@ paths:
summary: Go2Rtc Streams
operationId: go2rtc_streams_go2rtc_streams_get
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "/go2rtc/streams/{camera_name}":
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
+ description: '**Access:** Any authenticated user.'
+ /go2rtc/streams/{stream_name}:
get:
tags:
- Camera
summary: Go2Rtc Camera Stream
- operationId: go2rtc_camera_stream_go2rtc_streams__camera_name__get
+ operationId: go2rtc_camera_stream_go2rtc_streams__stream_name__get
parameters:
- - name: camera_name
+ - name: stream_name
in: path
required: true
schema:
anyOf:
- type: string
- - type: "null"
- title: Camera Name
+ - type: 'null'
+ title: Stream Name
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/go2rtc/streams/{stream_name}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ description: '**Access:** Authenticated user with access to the referenced camera.'
put:
tags:
- Camera
summary: Go2Rtc Add Stream
- description: Add or update a go2rtc stream configuration.
+ description: |-
+ **Access:** Admin role required.
+
+ Add or update a go2rtc stream configuration.
operationId: go2rtc_add_stream_go2rtc_streams__stream_name__put
parameters:
- name: stream_name
@@ -328,25 +361,31 @@ paths:
required: false
schema:
type: string
- default: ""
+ default: ''
title: Src
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
delete:
tags:
- Camera
summary: Go2Rtc Delete Stream
- description: Delete a go2rtc stream.
+ description: |-
+ **Access:** Admin role required.
+
+ Delete a go2rtc stream.
operationId: go2rtc_delete_stream_go2rtc_streams__stream_name__delete
parameters:
- name: stream_name
@@ -356,17 +395,20 @@ paths:
type: string
title: Stream Name
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/ffprobe:
get:
tags:
@@ -379,7 +421,7 @@ paths:
required: false
schema:
type: string
- default: ""
+ default: ''
title: Paths
- name: detailed
in: query
@@ -389,23 +431,65 @@ paths:
default: false
title: Detailed
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ description: '**Access:** Admin role required.'
+ /keyframe_analysis:
+ get:
+ tags:
+ - Camera
+ summary: Keyframe Analysis
+ description: |-
+ **Access:** Admin role required.
+
+ Probe a camera's record stream and classify its keyframe spacing.
+
+ Detects smart/+ codecs and long/variable GOPs that degrade recording.
+ operationId: keyframe_analysis_keyframe_analysis_get
+ parameters:
+ - name: camera
+ in: query
+ required: false
+ schema:
+ type: string
+ default: ''
+ title: Camera
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema: {}
+ '422':
+ description: Validation Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/ffprobe/snapshot:
get:
tags:
- Camera
summary: Ffprobe Snapshot
- description: Get a snapshot from a stream URL using ffmpeg.
+ description: |-
+ **Access:** Admin role required.
+
+ Get a snapshot from a stream URL using ffmpeg.
operationId: ffprobe_snapshot_ffprobe_snapshot_get
parameters:
- name: url
@@ -413,7 +497,7 @@ paths:
required: false
schema:
type: string
- default: ""
+ default: ''
title: Url
- name: timeout
in: query
@@ -423,30 +507,32 @@ paths:
default: 10
title: Timeout
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/reolink/detect:
get:
tags:
- Camera
summary: Reolink Detect
- description: >-
+ description: |-
+ **Access:** Admin role required.
+
Detect Reolink camera capabilities and recommend optimal protocol.
-
Queries the Reolink camera API to determine the camera's resolution
-
- and recommends either http-flv (for 5MP and below) or rtsp (for higher
- resolutions).
+ and recommends either http-flv (for 5MP and below) or rtsp (for higher resolutions).
operationId: reolink_detect_reolink_detect_get
parameters:
- name: host
@@ -454,44 +540,46 @@ paths:
required: false
schema:
type: string
- default: ""
+ default: ''
title: Host
- name: username
in: query
required: false
schema:
type: string
- default: ""
+ default: ''
title: Username
- name: password
in: query
required: false
schema:
type: string
- default: ""
+ default: ''
title: Password
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/onvif/probe:
get:
tags:
- Camera
summary: Probe ONVIF device
- description: >-
- Probe an ONVIF device to determine capabilities and optionally test
- available stream URIs. Query params: host (required), port (default 80),
- username, password, test (boolean), auth_type (basic or digest, default
- basic).
+ description: |-
+ **Access:** Admin role required.
+
+ Probe an ONVIF device to determine capabilities and optionally test available stream URIs. Query params: host (required), port (default 80), username, password, test (boolean), auth_type (basic or digest, default basic).
operationId: onvif_probe_onvif_probe_get
parameters:
- name: host
@@ -512,14 +600,14 @@ paths:
required: false
schema:
type: string
- default: ""
+ default: ''
title: Username
- name: password
in: query
required: false
schema:
type: string
- default: ""
+ default: ''
title: Password
- name: test
in: query
@@ -536,23 +624,28 @@ paths:
default: basic
title: Auth Type
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/cameras/{camera_name}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /cameras/{camera_name}:
delete:
tags:
- Camera
summary: Delete Camera
description: |-
+ **Access:** Admin role required.
+
Delete a camera and all its associated data.
Removes the camera from config, stops processes, and cleans up
@@ -577,24 +670,31 @@ paths:
default: false
title: Delete Exports
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/camera/{camera_name}/set/{feature}/{sub_command}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /camera/{camera_name}/set/{feature}/{sub_command}:
put:
tags:
- Camera
summary: Camera Set
- description: Set a camera feature state. Use camera_name='*' to target all cameras.
- operationId: camera_set_camera__camera_name__set__feature___sub_command__put
+ description: |-
+ **Access:** Admin role required.
+
+ Set a camera feature state. Use camera_name='*' to target all cameras.
+ operationId:
+ camera_set_camera__camera_name__set__feature___sub_command__put
parameters:
- name: camera_name
in: path
@@ -614,32 +714,38 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Sub Command
requestBody:
required: true
content:
application/json:
schema:
- $ref: "#/components/schemas/CameraSetBody"
+ $ref: '#/components/schemas/CameraSetBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/camera/{camera_name}/set/{feature}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /camera/{camera_name}/set/{feature}:
put:
tags:
- Camera
summary: Camera Set
- description: Set a camera feature state. Use camera_name='*' to target all cameras.
+ description: |-
+ **Access:** Admin role required.
+
+ Set a camera feature state. Use camera_name='*' to target all cameras.
operationId: camera_set_camera__camera_name__set__feature__put
parameters:
- name: camera_name
@@ -660,116 +766,208 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Sub Command
requestBody:
required: true
content:
application/json:
schema:
- $ref: "#/components/schemas/CameraSetBody"
+ $ref: '#/components/schemas/CameraSetBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/chat/tools:
get:
tags:
- Chat
summary: Get available tools
- description: Returns OpenAI-compatible tool definitions for function calling.
+ description: |-
+ **Access:** Admin role required.
+
+ Returns OpenAI-compatible tool definitions for function calling.
operationId: get_tools_chat_tools_get
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/chat/execute:
post:
tags:
- Chat
summary: Execute a tool
- description: Execute a tool function call from an LLM.
+ description: |-
+ **Access:** Admin role required.
+
+ Execute a tool function call from an LLM.
operationId: execute_tool_chat_execute_post
requestBody:
required: true
content:
application/json:
schema:
- $ref: "#/components/schemas/ToolExecuteRequest"
+ $ref: '#/components/schemas/ToolExecuteRequest'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/chat/completion:
post:
tags:
- Chat
summary: Chat completion with tool calling
- description: >-
- Send a chat message to the configured GenAI provider with tool calling
- support. The LLM can call Frigate tools to answer questions about your
- cameras and events.
+ description: |-
+ **Access:** Admin role required.
+
+ Send a chat message to the configured GenAI provider with tool calling support. The LLM can call Frigate tools to answer questions about your cameras and events.
operationId: chat_completion_chat_completion_post
requestBody:
required: true
content:
application/json:
schema:
- $ref: "#/components/schemas/ChatCompletionRequest"
+ $ref: '#/components/schemas/ChatCompletionRequest'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /vlm/monitor:
+ post:
+ tags:
+ - Chat
+ summary: Start a VLM watch job
+ description: |-
+ **Access:** Admin role required.
+
+ Start monitoring a camera with the vision provider. The VLM analyzes live frames until the specified condition is met, then sends a notification. Only one watch job can run at a time.
+ operationId: start_vlm_monitor_vlm_monitor_post
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/VLMMonitorRequest'
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema: {}
+ '422':
+ description: Validation Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ get:
+ tags:
+ - Chat
+ summary: Get current VLM watch job
+ description: |-
+ **Access:** Admin role required.
+
+ Returns the current (or most recently completed) VLM watch job.
+ operationId: get_vlm_monitor_vlm_monitor_get
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema: {}
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ delete:
+ tags:
+ - Chat
+ summary: Cancel the current VLM watch job
+ description: |-
+ **Access:** Admin role required.
+
+ Cancels the running watch job if one exists.
+ operationId: cancel_vlm_monitor_vlm_monitor_delete
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema: {}
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/faces:
get:
tags:
- Classification
summary: Get all registered faces
description: |-
+ **Access:** Admin role required.
+
Returns a dictionary mapping face names to lists of image filenames.
Each key represents a registered face name, and the value is a list of image
files associated with that face. Supported image formats include .webp, .png,
.jpg, and .jpeg.
operationId: get_faces_faces_get
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/FacesResponse"
+ $ref: '#/components/schemas/FacesResponse'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/faces/reprocess:
post:
tags:
- Classification
summary: Reprocess a face training image
description: |-
+ **Access:** Admin role required.
+
Reprocesses a face training image to update the prediction.
Requires face recognition to be enabled in the configuration. The training file
must exist in the faces/train directory. Returns a success response or an error
@@ -782,23 +980,28 @@ paths:
type: object
title: Body
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/faces/train/{name}/classify":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /faces/train/{name}/classify:
post:
tags:
- Classification
summary: Classify and save a face training image
description: |-
+ **Access:** Admin role required.
+
Adds a training image to a specific face name for face recognition.
Accepts either a training file from the train directory or an event_id to extract
the face from. The image is saved to the face's directory and the face classifier
@@ -820,24 +1023,29 @@ paths:
type: object
title: Body
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/faces/{name}/create":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /faces/{name}/create:
post:
tags:
- Classification
summary: Create a new face name
description: |-
+ **Access:** Admin role required.
+
Creates a new folder for a face name in the faces directory.
This is used to organize face training images. The face name is sanitized and
spaces are replaced with underscores. Returns a success message or an error if
@@ -851,26 +1059,30 @@ paths:
type: string
title: Name
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/faces/{name}/register":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /faces/{name}/register:
post:
tags:
- Classification
summary: Register a face image
- description: >-
- Registers a face image for a specific face name by uploading an image
- file.
+ description: |-
+ **Access:** Admin role required.
+
+ Registers a face image for a specific face name by uploading an image file.
The uploaded image is processed and added to the face recognition system. Returns a
success response with details about the registration, or an error if face recognition
is not enabled or the image cannot be processed.
@@ -887,27 +1099,31 @@ paths:
content:
multipart/form-data:
schema:
- $ref: >-
- #/components/schemas/Body_register_face_faces__name__register_post
+ $ref: '#/components/schemas/Body_register_face_faces__name__register_post'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/faces/recognize:
post:
tags:
- Classification
summary: Recognize a face from an uploaded image
description: |-
+ **Access:** Admin role required.
+
Recognizes a face from an uploaded image file by comparing it against
registered faces in the system. Returns the recognized face name and confidence score,
or an error if face recognition is not enabled or the image cannot be processed.
@@ -917,28 +1133,74 @@ paths:
content:
multipart/form-data:
schema:
- $ref: "#/components/schemas/Body_recognize_face_faces_recognize_post"
+ $ref: '#/components/schemas/Body_recognize_face_faces_recognize_post'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/FaceRecognitionResponse"
- "422":
+ $ref: '#/components/schemas/FaceRecognitionResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/faces/{name}/delete":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /faces/{name}/reclassify:
+ post:
+ tags:
+ - Classification
+ summary: Reclassify a face image to a different name
+ description: |-
+ **Access:** Admin role required.
+
+ Moves a single face image from one person's folder to another.
+ The image is moved and renamed, and the face classifier is cleared to
+ incorporate the change. Returns a success message or an error if the
+ image or target name is invalid.
+ operationId: reclassify_face_image_faces__name__reclassify_post
+ parameters:
+ - name: name
+ in: path
+ required: true
+ schema:
+ type: string
+ title: Name
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ title: Body
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
+ description: Validation Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /faces/{name}/delete:
post:
tags:
- Classification
summary: Delete face images
- description: >-
- Deletes specific face images for a given face name. The image IDs must
- belong
+ description: |-
+ **Access:** Admin role required.
+
+ Deletes specific face images for a given face name. The image IDs must belong
to the specified face folder. To delete an entire face folder, all image IDs in that
folder must be sent. Returns a success message or an error if face recognition is not enabled.
operationId: deregister_faces_faces__name__delete_post
@@ -954,26 +1216,31 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/DeleteFaceImagesBody"
+ $ref: '#/components/schemas/DeleteFaceImagesBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/faces/{old_name}/rename":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /faces/{old_name}/rename:
put:
tags:
- Classification
summary: Rename a face name
description: |-
+ **Access:** Admin role required.
+
Renames a face name in the system. The old name must exist and the new
name must be valid. Returns a success message or an error if face recognition is not enabled.
operationId: rename_face_faces__old_name__rename_put
@@ -989,26 +1256,31 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/RenameFaceBody"
+ $ref: '#/components/schemas/RenameFaceBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/lpr/reprocess:
put:
tags:
- Classification
summary: Reprocess a license plate
description: |-
+ **Access:** Admin role required.
+
Reprocesses a license plate image to update the plate.
Requires license plate recognition to be enabled in the configuration. The event_id
must exist in the database. Returns a success message or an error if license plate
@@ -1022,39 +1294,49 @@ paths:
type: string
title: Event Id
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/reindex:
put:
tags:
- Classification
summary: Reindex embeddings
description: |-
+ **Access:** Admin role required.
+
Reindexes the embeddings for all tracked objects.
Requires semantic search to be enabled in the configuration. Returns a success message or an error if semantic search is not enabled.
operationId: reindex_embeddings_reindex_put
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
+ $ref: '#/components/schemas/GenericResponse'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/audio/transcribe:
put:
tags:
- Classification
summary: Transcribe audio
description: |-
+ **Access:** Admin role required.
+
Transcribes audio from a specific event.
Requires audio transcription to be enabled in the configuration. The event_id
must exist in the database. Returns a success message or an error if audio transcription is not enabled or the event_id is invalid.
@@ -1064,26 +1346,31 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/AudioTranscriptionBody"
+ $ref: '#/components/schemas/AudioTranscriptionBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/classification/{name}/dataset":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /classification/{name}/dataset:
get:
tags:
- Classification
summary: Get classification dataset
description: |-
+ **Access:** Admin role required.
+
Gets the dataset for a specific classification model.
The name must exist in the classification models. Returns a success message or an error if the name is invalid.
operationId: get_classification_dataset_classification__name__dataset_get
@@ -1095,23 +1382,28 @@ paths:
type: string
title: Name
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/classification/attributes:
get:
tags:
- Classification
summary: Get custom classification attributes
description: |-
+ **Access:** Admin role required.
+
Returns custom classification attributes for a given object type.
Only includes models with classification_type set to 'attribute'.
By default returns a flat sorted list of all attribute labels.
@@ -1132,23 +1424,28 @@ paths:
default: false
title: Group By Model
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/classification/{name}/train":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /classification/{name}/train:
get:
tags:
- Classification
summary: Get classification train images
description: |-
+ **Access:** Admin role required.
+
Gets the train images for a specific classification model.
The name must exist in the classification models. Returns a success message or an error if the name is invalid.
operationId: get_classification_images_classification__name__train_get
@@ -1160,22 +1457,27 @@ paths:
type: string
title: Name
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
post:
tags:
- Classification
summary: Train a classification model
description: |-
+ **Access:** Admin role required.
+
Trains a specific classification model.
The name must exist in the classification models. Returns a success message or an error if the name is invalid.
operationId: train_configured_model_classification__name__train_post
@@ -1187,28 +1489,32 @@ paths:
type: string
title: Name
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/classification/{name}/dataset/{category}/delete":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /classification/{name}/dataset/{category}/delete:
post:
tags:
- Classification
summary: Delete classification dataset images
- description: >-
- Deletes specific dataset images for a given classification model and
- category.
+ description: |-
+ **Access:** Admin role required.
+
+ Deletes specific dataset images for a given classification model and category.
The image IDs must belong to the specified category. Returns a success message or an error if the name or category is invalid.
- operationId: >-
+ operationId:
delete_classification_dataset_images_classification__name__dataset__category__delete_post
parameters:
- name: name
@@ -1230,27 +1536,79 @@ paths:
type: object
title: Body
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/classification/{name}/dataset/{old_category}/rename":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /classification/{name}/dataset/{category}/reclassify:
+ post:
+ tags:
+ - Classification
+ summary: Reclassify a dataset image to a different category
+ description: |-
+ **Access:** Admin role required.
+
+ Moves a single dataset image from one category to another.
+ The image is re-saved as PNG in the target category and removed from the source.
+ operationId:
+ reclassify_classification_image_classification__name__dataset__category__reclassify_post
+ parameters:
+ - name: name
+ in: path
+ required: true
+ schema:
+ type: string
+ title: Name
+ - name: category
+ in: path
+ required: true
+ schema:
+ type: string
+ title: Category
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ title: Body
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
+ description: Validation Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /classification/{name}/dataset/{old_category}/rename:
put:
tags:
- Classification
summary: Rename a classification category
description: |-
+ **Access:** Admin role required.
+
Renames a classification category for a given classification model.
The old category must exist and the new name must be valid. Returns a success message or an error if the name is invalid.
- operationId: >-
+ operationId:
rename_classification_category_classification__name__dataset__old_category__rename_put
parameters:
- name: name
@@ -1272,28 +1630,32 @@ paths:
type: object
title: Body
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/classification/{name}/dataset/categorize":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /classification/{name}/dataset/categorize:
post:
tags:
- Classification
summary: Categorize a classification image
- description: >-
- Categorizes a specific classification image for a given classification
- model and category.
+ description: |-
+ **Access:** Admin role required.
+
+ Categorizes a specific classification image for a given classification model and category.
The image must exist in the specified category. Returns a success message or an error if the name or category is invalid.
- operationId: >-
+ operationId:
categorize_classification_image_classification__name__dataset_categorize_post
parameters:
- name: name
@@ -1309,28 +1671,33 @@ paths:
type: object
title: Body
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/classification/{name}/dataset/{category}/create":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /classification/{name}/dataset/{category}/create:
post:
tags:
- Classification
summary: Create an empty classification category folder
description: |-
+ **Access:** Admin role required.
+
Creates an empty folder for a classification category.
This is used to create folders for categories that don't have images yet.
Returns a success message or an error if the name is invalid.
- operationId: >-
+ operationId:
create_classification_category_classification__name__dataset__category__create_post
parameters:
- name: name
@@ -1346,27 +1713,32 @@ paths:
type: string
title: Category
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/classification/{name}/train/delete":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /classification/{name}/train/delete:
post:
tags:
- Classification
summary: Delete classification train images
description: |-
+ **Access:** Admin role required.
+
Deletes specific train images for a given classification model.
The image IDs must belong to the specified train folder. Returns a success message or an error if the name is invalid.
- operationId: >-
+ operationId:
delete_classification_train_images_classification__name__train_delete_post
parameters:
- name: name
@@ -1382,76 +1754,95 @@ paths:
type: object
title: Body
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/classification/generate_examples/state:
post:
tags:
- Classification
summary: Generate state classification examples
- description: Generate examples for state classification.
- operationId: generate_state_examples_classification_generate_examples_state_post
+ description: |-
+ **Access:** Admin role required.
+
+ Generate examples for state classification.
+ operationId:
+ generate_state_examples_classification_generate_examples_state_post
requestBody:
required: true
content:
application/json:
schema:
- $ref: "#/components/schemas/GenerateStateExamplesBody"
+ $ref: '#/components/schemas/GenerateStateExamplesBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/classification/generate_examples/object:
post:
tags:
- Classification
summary: Generate object classification examples
- description: Generate examples for object classification.
- operationId: generate_object_examples_classification_generate_examples_object_post
+ description: |-
+ **Access:** Admin role required.
+
+ Generate examples for object classification.
+ operationId:
+ generate_object_examples_classification_generate_examples_object_post
requestBody:
required: true
content:
application/json:
schema:
- $ref: "#/components/schemas/GenerateObjectExamplesBody"
+ $ref: '#/components/schemas/GenerateObjectExamplesBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/classification/{name}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /classification/{name}:
delete:
tags:
- Classification
summary: Delete a classification model
description: |-
+ **Access:** Admin role required.
+
Deletes a specific classification model and all its associated data.
Works even if the model is not in the config (e.g., partially created during wizard).
Returns a success message.
@@ -1464,18 +1855,21 @@ paths:
type: string
title: Name
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/review:
get:
tags:
@@ -1520,7 +1914,7 @@ paths:
in: query
required: false
schema:
- $ref: "#/components/schemas/SeverityEnum"
+ $ref: '#/components/schemas/SeverityEnum'
- name: before
in: query
required: false
@@ -1534,21 +1928,25 @@ paths:
type: number
title: After
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
type: array
items:
- $ref: "#/components/schemas/ReviewSegmentResponse"
+ $ref: '#/components/schemas/ReviewSegmentResponse'
title: Response Review Review Get
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
+ description: '**Access:** Any authenticated user.'
/review_ids:
get:
tags:
@@ -1563,21 +1961,25 @@ paths:
type: string
title: Ids
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
type: array
items:
- $ref: "#/components/schemas/ReviewSegmentResponse"
+ $ref: '#/components/schemas/ReviewSegmentResponse'
title: Response Review Ids Review Ids Get
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ description: '**Access:** Authenticated user with access to the referenced camera.'
/review/summary:
get:
tags:
@@ -1614,18 +2016,22 @@ paths:
default: utc
title: Timezone
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/ReviewSummaryResponse"
- "422":
+ $ref: '#/components/schemas/ReviewSummaryResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
+ description: '**Access:** Any authenticated user.'
/reviews/viewed:
post:
tags:
@@ -1637,20 +2043,24 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/ReviewModifyMultipleBody"
+ $ref: '#/components/schemas/ReviewModifyMultipleBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ description: '**Access:** Authenticated user with access to the referenced camera.'
/reviews/delete:
post:
tags:
@@ -1662,26 +2072,33 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/ReviewModifyMultipleBody"
+ $ref: '#/components/schemas/ReviewModifyMultipleBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ description: '**Access:** Admin role required.'
/review/activity/motion:
get:
tags:
- Review
summary: Motion Activity
- description: Get motion and audio activity.
+ description: |-
+ **Access:** Any authenticated user.
+
+ Get motion and audio activity.
operationId: motion_activity_review_activity_motion_get
parameters:
- name: cameras
@@ -1711,22 +2128,25 @@ paths:
default: 30
title: Scale
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
type: array
items:
- $ref: "#/components/schemas/ReviewActivityMotionResponse"
+ $ref: '#/components/schemas/ReviewActivityMotionResponse'
title: Response Motion Activity Review Activity Motion Get
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/review/event/{event_id}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
+ /review/event/{event_id}:
get:
tags:
- Review
@@ -1740,19 +2160,23 @@ paths:
type: string
title: Event Id
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/ReviewSegmentResponse"
- "422":
+ $ref: '#/components/schemas/ReviewSegmentResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/review/{review_id}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ description: '**Access:** Authenticated user with access to the referenced camera.'
+ /review/{review_id}:
get:
tags:
- Review
@@ -1766,19 +2190,23 @@ paths:
type: string
title: Review Id
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/ReviewSegmentResponse"
- "422":
+ $ref: '#/components/schemas/ReviewSegmentResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/review/{review_id}/viewed":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ description: '**Access:** Authenticated user with access to the referenced camera.'
+ /review/{review_id}/viewed:
delete:
tags:
- Review
@@ -1792,25 +2220,32 @@ paths:
type: string
title: Review Id
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/review/summarize/start/{start_ts}/end/{end_ts}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
+ description: '**Access:** Any authenticated user.'
+ /review/summarize/start/{start_ts}/end/{end_ts}:
post:
tags:
- Review
summary: Generate Review Summary
- description: Use GenAI to summarize review items over a period of time.
- operationId: >-
+ description: |-
+ **Access:** Admin role required.
+
+ Use GenAI to summarize review items over a period of time.
+ operationId:
generate_review_summary_review_summarize_start__start_ts__end__end_ts__post
parameters:
- name: start_ts
@@ -1826,17 +2261,20 @@ paths:
type: number
title: End Ts
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/:
get:
tags:
@@ -1844,12 +2282,15 @@ paths:
summary: Is Healthy
operationId: is_healthy__get
responses:
- "200":
+ '200':
description: Successful Response
content:
text/plain:
schema:
type: string
+ security: []
+ x-required-role: public
+ description: '**Access:** Public — no authentication required.'
/config/schema.json:
get:
tags:
@@ -1857,11 +2298,14 @@ paths:
summary: Config Schema
operationId: config_schema_config_schema_json_get
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
+ security: []
+ x-required-role: public
+ description: '**Access:** Public — no authentication required.'
/version:
get:
tags:
@@ -1869,12 +2313,15 @@ paths:
summary: Version
operationId: version_version_get
responses:
- "200":
+ '200':
description: Successful Response
content:
text/plain:
schema:
type: string
+ security: []
+ x-required-role: public
+ description: '**Access:** Public — no authentication required.'
/stats:
get:
tags:
@@ -1882,11 +2329,15 @@ paths:
summary: Stats
operationId: stats_stats_get
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
+ description: '**Access:** Any authenticated user.'
/stats/history:
get:
tags:
@@ -1901,30 +2352,90 @@ paths:
type: string
title: Keys
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ description: '**Access:** Admin role required.'
/metrics:
get:
tags:
- App
summary: Metrics
- description: Expose Prometheus metrics endpoint and update metrics with latest stats
+ description: |-
+ **Access:** Any authenticated user.
+
+ Expose Prometheus metrics endpoint and update metrics with latest stats
operationId: metrics_metrics_get
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
+ /genai/models:
+ get:
+ tags:
+ - App
+ summary: List available GenAI models
+ description: |-
+ **Access:** Admin role required.
+
+ Returns available models for each configured GenAI provider.
+ operationId: genai_models_genai_models_get
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema: {}
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /genai/probe:
+ post:
+ tags:
+ - App
+ summary: Probe a GenAI provider without saving config
+ description: |-
+ **Access:** Admin role required.
+
+ Builds a transient client from the request body and returns its available models. Used to validate provider credentials in the UI before saving the configuration.
+ operationId: genai_probe_genai_probe_post
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GenAIProbeBody'
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema: {}
+ '422':
+ description: Validation Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/config:
get:
tags:
@@ -1932,65 +2443,91 @@ paths:
summary: Config
operationId: config_config_get
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
+ description: '**Access:** Any authenticated user.'
/profiles:
get:
tags:
- App
summary: Get Profiles
- description: List all available profiles and the currently active profile.
+ description: |-
+ **Access:** Any authenticated user.
+
+ List all available profiles and the currently active profile.
operationId: get_profiles_profiles_get
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
/profile/active:
get:
tags:
- App
summary: Get Active Profile
- description: Get the currently active profile.
+ description: |-
+ **Access:** Admin role required.
+
+ Get the currently active profile.
operationId: get_active_profile_profile_active_get
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/ffmpeg/presets:
get:
tags:
- App
summary: Ffmpeg Presets
- description: Return available ffmpeg preset keys for config UI usage.
+ description: |-
+ **Access:** Admin role required.
+
+ Return available ffmpeg preset keys for config UI usage.
operationId: ffmpeg_presets_ffmpeg_presets_get
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/config/raw_paths:
get:
tags:
- App
summary: Config Raw Paths
- description: >-
- Admin-only endpoint that returns camera paths and go2rtc streams without
- credential masking.
+ description: |-
+ **Access:** Admin role required.
+
+ Admin-only endpoint that returns camera paths and go2rtc streams without credential masking.
operationId: config_raw_paths_config_raw_paths_get
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/config/raw:
get:
tags:
@@ -1998,11 +2535,15 @@ paths:
summary: Config Raw
operationId: config_raw_config_raw_get
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ description: '**Access:** Admin role required.'
/config/save:
post:
tags:
@@ -2023,17 +2564,21 @@ paths:
schema:
title: Body
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ description: '**Access:** Admin role required.'
/config/set:
put:
tags:
@@ -2045,19 +2590,23 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/AppConfigSetBody"
+ $ref: '#/components/schemas/AppConfigSetBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ description: '**Access:** Admin role required.'
/vainfo:
get:
tags:
@@ -2065,11 +2614,15 @@ paths:
summary: Vainfo
operationId: vainfo_vainfo_get
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
+ description: '**Access:** Any authenticated user.'
/nvinfo:
get:
tags:
@@ -2077,18 +2630,25 @@ paths:
summary: Nvinfo
operationId: nvinfo_nvinfo_get
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "/logs/{service}":
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
+ description: '**Access:** Any authenticated user.'
+ /logs/{service}:
get:
tags:
- App
- Logs
summary: Logs
- description: Get logs for the requested service (frigate/nginx/go2rtc)
+ description: |-
+ **Access:** Admin role required.
+
+ Get logs for the requested service (frigate/nginx/go2rtc)
operationId: logs_logs__service__get
parameters:
- name: service
@@ -2107,7 +2667,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Download
- name: stream
in: query
@@ -2115,7 +2675,7 @@ paths:
schema:
anyOf:
- type: boolean
- - type: "null"
+ - type: 'null'
default: false
title: Stream
- name: start
@@ -2124,7 +2684,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
default: 0
title: Start
- name: end
@@ -2133,20 +2693,23 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: End
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/restart:
post:
tags:
@@ -2154,19 +2717,24 @@ paths:
summary: Restart
operationId: restart_restart_post
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ description: '**Access:** Admin role required.'
/media/sync:
post:
tags:
- App
summary: Start media sync job
- description: >-
- Start an asynchronous media sync job to find and (optionally) remove
- orphaned media files.
+ description: |-
+ **Access:** Admin role required.
+
+ Start an asynchronous media sync job to find and (optionally) remove orphaned media files.
Returns 202 with job details when queued, or 409 if a job is already running.
operationId: sync_media_media_sync_post
requestBody:
@@ -2174,43 +2742,51 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/MediaSyncBody"
+ $ref: '#/components/schemas/MediaSyncBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/media/sync/current:
get:
tags:
- App
summary: Get current media sync job
- description: >-
- Retrieve the current running media sync job, if any. Returns the job
- details
+ description: |-
+ **Access:** Admin role required.
+
+ Retrieve the current running media sync job, if any. Returns the job details
or null when no job is active.
operationId: get_media_sync_current_media_sync_current_get
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "/media/sync/status/{job_id}":
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /media/sync/status/{job_id}:
get:
tags:
- App
summary: Get media sync job status
- description: >-
- Get status and results for the specified media sync job id. Returns 200
- with
+ description: |-
+ **Access:** Admin role required.
+
+ Get status and results for the specified media sync job id. Returns 200 with
job details including results, or 404 if the job is not found.
operationId: get_media_sync_status_media_sync_status__job_id__get
parameters:
@@ -2221,17 +2797,20 @@ paths:
type: string
title: Job Id
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/labels:
get:
tags:
@@ -2244,20 +2823,24 @@ paths:
required: false
schema:
type: string
- default: ""
+ default: ''
title: Camera
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
+ description: '**Access:** Any authenticated user.'
/sub_labels:
get:
tags:
@@ -2271,20 +2854,24 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Split Joined
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
+ description: '**Access:** Any authenticated user.'
/audio_labels:
get:
tags:
@@ -2292,11 +2879,15 @@ paths:
summary: Get Audio Labels
operationId: get_audio_labels_audio_labels_get
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ description: '**Access:** Admin role required.'
/plus/models:
get:
tags:
@@ -2312,17 +2903,21 @@ paths:
default: false
title: Filterbycurrentmodeldetector
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
+ description: '**Access:** Any authenticated user.'
/recognized_license_plates:
get:
tags:
@@ -2336,20 +2931,24 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Split Joined
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
+ description: '**Access:** Any authenticated user.'
/timeline:
get:
tags:
@@ -2377,26 +2976,33 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Source Id
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
+ description: '**Access:** Any authenticated user.'
/timeline/hourly:
get:
tags:
- App
summary: Hourly Timeline
- description: Get hourly summary for timeline.
+ description: |-
+ **Access:** Any authenticated user.
+
+ Get hourly summary for timeline.
operationId: hourly_timeline_timeline_hourly_get
parameters:
- name: cameras
@@ -2405,7 +3011,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: all
title: Cameras
- name: labels
@@ -2414,7 +3020,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: all
title: Labels
- name: after
@@ -2423,7 +3029,7 @@ paths:
schema:
anyOf:
- type: number
- - type: "null"
+ - type: 'null'
title: After
- name: before
in: query
@@ -2431,7 +3037,7 @@ paths:
schema:
anyOf:
- type: number
- - type: "null"
+ - type: 'null'
title: Before
- name: limit
in: query
@@ -2439,7 +3045,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
default: 200
title: Limit
- name: timezone
@@ -2448,32 +3054,38 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: utc
title: Timezone
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/preview/{camera_name}/start/{start_ts}/end/{end_ts}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
+ /preview/{camera_name}/start/{start_ts}/end/{end_ts}:
get:
tags:
- Preview
summary: Get preview clips for time range
description: |-
+ **Access:** Any authenticated user.
+
Gets all preview clips for a specified camera and time range.
Returns a list of preview video clips that overlap with the requested time period,
ordered by start time. Use camera_name='all' to get previews from all cameras.
Returns an error if no previews are found.
- operationId: preview_ts_preview__camera_name__start__start_ts__end__end_ts__get
+ operationId:
+ preview_ts_preview__camera_name__start__start_ts__end__end_ts__get
parameters:
- name: camera_name
in: path
@@ -2494,34 +3106,38 @@ paths:
type: number
title: End Ts
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
type: array
items:
- $ref: "#/components/schemas/PreviewModel"
- title: >-
- Response Preview Ts Preview Camera Name Start Start Ts
+ $ref: '#/components/schemas/PreviewModel'
+ title: Response Preview Ts Preview Camera Name Start Start Ts
End End Ts Get
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/preview/{year_month}/{day}/{hour}/{camera_name}/{tz_name}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
+ /preview/{year_month}/{day}/{hour}/{camera_name}/{tz_name}:
get:
tags:
- Preview
summary: Get preview clips for specific hour
description: |-
+ **Access:** Any authenticated user.
+
Gets all preview clips for a specific hour in a given timezone.
Converts the provided date/time from the specified timezone to UTC and retrieves
all preview clips for that hour. Use camera_name='all' to get previews from all cameras.
The tz_name should be a timezone like 'America/New_York' (use commas instead of slashes).
- operationId: >-
+ operationId:
preview_hour_preview__year_month___day___hour___camera_name___tz_name__get
parameters:
- name: year_month
@@ -2555,34 +3171,37 @@ paths:
type: string
title: Tz Name
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
type: array
items:
- $ref: "#/components/schemas/PreviewModel"
- title: >-
- Response Preview Hour Preview Year Month Day Hour
+ $ref: '#/components/schemas/PreviewModel'
+ title: Response Preview Hour Preview Year Month Day Hour
Camera Name Tz Name Get
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/preview/{camera_name}/start/{start_ts}/end/{end_ts}/frames":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
+ /preview/{camera_name}/start/{start_ts}/end/{end_ts}/frames:
get:
tags:
- Preview
summary: Get cached preview frame filenames
- description: >-
- Gets a list of cached preview frame filenames for a specific camera and
- time range.
+ description: |-
+ **Access:** Authenticated user with access to the referenced camera.
+
+ Gets a list of cached preview frame filenames for a specific camera and time range.
Returns an array of filenames for preview frames that fall within the specified time period,
sorted in chronological order. These are individual frame images cached for quick preview display.
- operationId: >-
+ operationId:
get_preview_frames_from_cache_preview__camera_name__start__start_ts__end__end_ts__frames_get
parameters:
- name: camera_name
@@ -2591,7 +3210,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera Name
- name: start_ts
in: path
@@ -2606,7 +3225,7 @@ paths:
type: number
title: End Ts
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
@@ -2614,36 +3233,45 @@ paths:
type: array
items:
type: string
- title: >-
- Response Get Preview Frames From Cache Preview Camera Name
- Start Start Ts End End Ts Frames Get
- "422":
+ title: Response Get Preview Frames From Cache Preview Camera
+ Name Start Start Ts End End Ts Frames Get
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
/notifications/pubkey:
get:
tags:
- Notifications
summary: Get VAPID public key
description: |-
+ **Access:** Any authenticated user.
+
Gets the VAPID public key for the notifications.
Returns the public key or an error if notifications are not enabled.
operationId: get_vapid_pub_key_notifications_pubkey_get
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
/notifications/register:
post:
tags:
- Notifications
summary: Register notifications
description: |-
+ **Access:** Any authenticated user.
+
Registers a notifications subscription.
Returns a success message or an error if the subscription is not provided.
operationId: register_notifications_notifications_register_post
@@ -2654,23 +3282,28 @@ paths:
type: object
title: Body
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
/exports:
get:
tags:
- Export
summary: Get exports
description: |-
+ **Access:** Any authenticated user.
+
Gets all exports from the database for cameras the user has access to.
Returns a list of exports ordered by date (most recent first).
operationId: get_exports_exports_get
@@ -2681,7 +3314,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Export Case Id
- name: cameras
in: query
@@ -2689,7 +3322,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: all
title: Cameras
- name: start_date
@@ -2698,7 +3331,7 @@ paths:
schema:
anyOf:
- type: number
- - type: "null"
+ - type: 'null'
title: Start Date
- name: end_date
in: query
@@ -2706,201 +3339,90 @@ paths:
schema:
anyOf:
- type: number
- - type: "null"
+ - type: 'null'
title: End Date
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
type: array
items:
- $ref: "#/components/schemas/ExportModel"
+ $ref: '#/components/schemas/ExportModel'
title: Response Get Exports Exports Get
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- /exports/batch:
- post:
- tags:
- - Export
- summary: Start recording export batch
- description: >-
- Starts recording exports for a batch of items, each with its own camera
- and time range. Optionally assigns them to a new or existing export case.
- When neither export_case_id nor new_case_name is provided, exports are
- added as uncategorized. Attaching to an existing case is admin-only.
- operationId: export_recordings_batch_exports_batch_post
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/BatchExportBody"
- responses:
- "202":
- description: Successful Response
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/BatchExportResponse"
- "400":
- description: Bad Request
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/GenericResponse"
- "403":
- description: Forbidden
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/GenericResponse"
- "404":
- description: Not Found
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/GenericResponse"
- "503":
- description: Service Unavailable
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
- description: Validation Error
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/HTTPValidationError"
- /exports/delete:
- post:
- tags:
- - Export
- summary: Bulk delete exports
- description: >-
- Deletes one or more exports by ID. All IDs must exist and none can be
- in-progress. Admin-only.
- operationId: bulk_delete_exports_exports_delete_post
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/ExportBulkDeleteBody"
- responses:
- "200":
- description: Successful Response
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/GenericResponse"
- "400":
- description: Bad Request - one or more exports are in-progress
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/GenericResponse"
- "404":
- description: Not Found - one or more export IDs do not exist
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
- description: Validation Error
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/HTTPValidationError"
- /exports/reassign:
- post:
- tags:
- - Export
- summary: Bulk reassign exports to a case
- description: >-
- Assigns or unassigns one or more exports to/from a case. All IDs must
- exist. Pass export_case_id as null to unassign (move to uncategorized).
- Admin-only.
- operationId: bulk_reassign_exports_exports_reassign_post
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/ExportBulkReassignBody"
- responses:
- "200":
- description: Successful Response
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/GenericResponse"
- "404":
- description: Not Found - one or more export IDs or the target case do not exist
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
- description: Validation Error
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
/cases:
get:
tags:
- Export
summary: Get export cases
- description: Gets all export cases from the database.
+ description: |-
+ **Access:** Any authenticated user.
+
+ Gets all export cases from the database.
operationId: get_export_cases_cases_get
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
type: array
items:
- $ref: "#/components/schemas/ExportCaseModel"
+ $ref: '#/components/schemas/ExportCaseModel'
title: Response Get Export Cases Cases Get
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
post:
tags:
- Export
summary: Create export case
- description: Creates a new export case.
+ description: |-
+ **Access:** Admin role required.
+
+ Creates a new export case.
operationId: create_export_case_cases_post
requestBody:
required: true
content:
application/json:
schema:
- $ref: "#/components/schemas/ExportCaseCreateBody"
+ $ref: '#/components/schemas/ExportCaseCreateBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/ExportCaseModel"
- "422":
+ $ref: '#/components/schemas/ExportCaseModel'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/cases/{case_id}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /cases/{case_id}:
get:
tags:
- Export
summary: Get a single export case
- description: Gets a specific export case by ID.
+ description: |-
+ **Access:** Any authenticated user.
+
+ Gets a specific export case by ID.
operationId: get_export_case_cases__case_id__get
parameters:
- name: case_id
@@ -2910,23 +3432,29 @@ paths:
type: string
title: Case Id
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/ExportCaseModel"
- "422":
+ $ref: '#/components/schemas/ExportCaseModel'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
patch:
tags:
- Export
summary: Update export case
- description: Updates an existing export case.
+ description: |-
+ **Access:** Admin role required.
+
+ Updates an existing export case.
operationId: update_export_case_cases__case_id__patch
parameters:
- name: case_id
@@ -2940,25 +3468,30 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/ExportCaseUpdateBody"
+ $ref: '#/components/schemas/ExportCaseUpdateBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
delete:
tags:
- Export
summary: Delete export case
description: |-
+ **Access:** Admin role required.
+
Deletes an export case.
Exports that reference this case will have their export_case set to null.
operationId: delete_export_case_cases__case_id__delete
@@ -2969,30 +3502,162 @@ paths:
schema:
type: string
title: Case Id
+ - name: delete_exports
+ in: query
+ required: false
+ schema:
+ type: boolean
+ default: false
+ title: Delete Exports
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/export/{camera_name}/start/{start_time}/end/{end_time}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /cases/{case_id}/download:
+ get:
+ tags:
+ - Export
+ summary: Download export case as zip
+ description: |-
+ **Access:** Any authenticated user.
+
+ Streams a zip archive containing every completed export's mp4 for the given case.
+ operationId: download_export_case_cases__case_id__download_get
+ parameters:
+ - name: case_id
+ in: path
+ required: true
+ schema:
+ type: string
+ title: Case Id
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema: {}
+ '422':
+ description: Validation Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
+ /jobs/export:
+ get:
+ tags:
+ - Export
+ summary: Get active export jobs
+ description: |-
+ **Access:** Any authenticated user.
+
+ Gets queued and running export jobs.
+ operationId: get_active_export_jobs_jobs_export_get
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/ExportJobModel'
+ title: Response Get Active Export Jobs Jobs Export Get
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
+ /jobs/export/{export_id}:
+ get:
+ tags:
+ - Export
+ summary: Get export job status
+ description: |-
+ **Access:** Authenticated user with access to the referenced camera.
+
+ Gets queued, running, or completed status for a specific export job.
+ operationId: get_export_job_status_jobs_export__export_id__get
+ parameters:
+ - name: export_id
+ in: path
+ required: true
+ schema:
+ type: string
+ title: Export Id
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ExportJobModel'
+ '422':
+ description: Validation Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ /exports/batch:
+ post:
+ tags:
+ - Export
+ summary: Start recording export batch
+ description: |-
+ **Access:** Any authenticated user.
+
+ Starts recording exports for a batch of items, each with its own camera and time range, and assigns them to a single export case. Attaching to an existing case is temporarily admin-only until case-level ACLs exist.
+ operationId: export_recordings_batch_exports_batch_post
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/BatchExportBody'
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/BatchExportResponse'
+ '422':
+ description: Validation Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
+ /export/{camera_name}/start/{start_time}/end/{end_time}:
post:
tags:
- Export
summary: Start recording export
description: |-
+ **Access:** Authenticated user with access to the referenced camera.
+
Starts an export of a recording for the specified time range.
The export can be from recordings or preview footage. Returns the export ID if
successful, or an error message if the camera is invalid or no recordings/previews
are found for the time range.
- operationId: >-
+ operationId:
export_recording_export__camera_name__start__start_time__end__end_time__post
parameters:
- name: camera_name
@@ -3001,7 +3666,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera Name
- name: start_time
in: path
@@ -3020,26 +3685,31 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/ExportRecordingsBody"
+ $ref: '#/components/schemas/ExportRecordingsBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/StartExportResponse"
- "422":
+ $ref: '#/components/schemas/StartExportResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/export/{event_id}/rename":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ /export/{event_id}/rename:
patch:
tags:
- Export
summary: Rename export
description: |-
+ **Access:** Admin role required.
+
Renames an export.
NOTE: This changes the friendly name of the export, not the filename.
operationId: export_rename_export__event_id__rename_patch
@@ -3055,33 +3725,37 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/ExportRenameBody"
+ $ref: '#/components/schemas/ExportRenameBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/export/custom/{camera_name}/start/{start_time}/end/{end_time}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /export/custom/{camera_name}/start/{start_time}/end/{end_time}:
post:
tags:
- Export
summary: Start custom recording export
- description: >-
- Starts an export of a recording for the specified time range using
- custom FFmpeg arguments.
+ description: |-
+ **Access:** Authenticated user with access to the referenced camera.
+
+ Starts an export of a recording for the specified time range using custom FFmpeg arguments.
The export can be from recordings or preview footage. Returns the export ID if
successful, or an error message if the camera is invalid or no recordings/previews
are found for the time range. If ffmpeg_input_args and ffmpeg_output_args are not provided,
defaults to timelapse export settings.
- operationId: >-
+ operationId:
export_recording_custom_export_custom__camera_name__start__start_time__end__end_time__post
parameters:
- name: camera_name
@@ -3090,7 +3764,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera Name
- name: start_time
in: path
@@ -3109,26 +3783,31 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/ExportRecordingsCustomBody"
+ $ref: '#/components/schemas/ExportRecordingsCustomBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/StartExportResponse"
- "422":
+ $ref: '#/components/schemas/StartExportResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/exports/{export_id}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ /exports/{export_id}:
get:
tags:
- Export
summary: Get a single export
description: |-
+ **Access:** Authenticated user with access to the referenced camera.
+
Gets a specific export by ID. The user must have access to the camera
associated with the export.
operationId: get_export_exports__export_id__get
@@ -3140,24 +3819,94 @@ paths:
type: string
title: Export Id
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/ExportModel"
- "422":
+ $ref: '#/components/schemas/ExportModel'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ /exports/delete:
+ post:
+ tags:
+ - Export
+ summary: Bulk delete exports
+ description: |-
+ **Access:** Admin role required.
+
+ Deletes one or more exports by ID. All IDs must exist and none can be in-progress.
+ operationId: bulk_delete_exports_exports_delete_post
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ExportBulkDeleteBody'
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
+ description: Validation Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /exports/reassign:
+ post:
+ tags:
+ - Export
+ summary: Bulk reassign exports to a case
+ description: |-
+ **Access:** Admin role required.
+
+ Assigns or unassigns one or more exports to/from a case. All IDs must exist.
+ operationId: bulk_reassign_exports_exports_reassign_post
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ExportBulkReassignBody'
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
+ description: Validation Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/events:
get:
tags:
- Events
summary: Get events
- description: Returns a list of events.
+ description: |-
+ **Access:** Any authenticated user.
+
+ Returns a list of events.
operationId: events_events_get
parameters:
- name: camera
@@ -3166,7 +3915,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: all
title: Camera
- name: cameras
@@ -3175,7 +3924,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: all
title: Cameras
- name: label
@@ -3184,7 +3933,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: all
title: Label
- name: labels
@@ -3193,7 +3942,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: all
title: Labels
- name: sub_label
@@ -3202,7 +3951,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: all
title: Sub Label
- name: sub_labels
@@ -3211,7 +3960,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: all
title: Sub Labels
- name: attributes
@@ -3220,7 +3969,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: all
title: Attributes
- name: zone
@@ -3229,7 +3978,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: all
title: Zone
- name: zones
@@ -3238,7 +3987,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: all
title: Zones
- name: limit
@@ -3247,7 +3996,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
default: 100
title: Limit
- name: after
@@ -3256,7 +4005,7 @@ paths:
schema:
anyOf:
- type: number
- - type: "null"
+ - type: 'null'
title: After
- name: before
in: query
@@ -3264,7 +4013,7 @@ paths:
schema:
anyOf:
- type: number
- - type: "null"
+ - type: 'null'
title: Before
- name: time_range
in: query
@@ -3272,8 +4021,8 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
- default: "00:00,24:00"
+ - type: 'null'
+ default: 00:00,24:00
title: Time Range
- name: has_clip
in: query
@@ -3281,7 +4030,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Has Clip
- name: has_snapshot
in: query
@@ -3289,7 +4038,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Has Snapshot
- name: in_progress
in: query
@@ -3297,7 +4046,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: In Progress
- name: include_thumbnails
in: query
@@ -3305,7 +4054,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
default: 1
title: Include Thumbnails
- name: favorites
@@ -3314,7 +4063,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Favorites
- name: min_score
in: query
@@ -3322,7 +4071,7 @@ paths:
schema:
anyOf:
- type: number
- - type: "null"
+ - type: 'null'
title: Min Score
- name: max_score
in: query
@@ -3330,7 +4079,7 @@ paths:
schema:
anyOf:
- type: number
- - type: "null"
+ - type: 'null'
title: Max Score
- name: min_speed
in: query
@@ -3338,7 +4087,7 @@ paths:
schema:
anyOf:
- type: number
- - type: "null"
+ - type: 'null'
title: Min Speed
- name: max_speed
in: query
@@ -3346,7 +4095,7 @@ paths:
schema:
anyOf:
- type: number
- - type: "null"
+ - type: 'null'
title: Max Speed
- name: recognized_license_plate
in: query
@@ -3354,7 +4103,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: all
title: Recognized License Plate
- name: is_submitted
@@ -3363,7 +4112,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Is Submitted
- name: min_length
in: query
@@ -3371,7 +4120,7 @@ paths:
schema:
anyOf:
- type: number
- - type: "null"
+ - type: 'null'
title: Min Length
- name: max_length
in: query
@@ -3379,7 +4128,7 @@ paths:
schema:
anyOf:
- type: number
- - type: "null"
+ - type: 'null'
title: Max Length
- name: event_id
in: query
@@ -3387,7 +4136,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Event Id
- name: sort
in: query
@@ -3395,7 +4144,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Sort
- name: timezone
in: query
@@ -3403,31 +4152,36 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: utc
title: Timezone
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
type: array
items:
- $ref: "#/components/schemas/EventResponse"
+ $ref: '#/components/schemas/EventResponse'
title: Response Events Events Get
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
/events/explore:
get:
tags:
- Events
summary: Get summary of objects
description: |-
+ **Access:** Any authenticated user.
+
Gets a summary of objects from the database.
Returns a list of objects with a max of `limit` objects for each label.
operationId: events_explore_events_explore_get
@@ -3440,27 +4194,32 @@ paths:
default: 10
title: Limit
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
type: array
items:
- $ref: "#/components/schemas/EventResponse"
+ $ref: '#/components/schemas/EventResponse'
title: Response Events Explore Events Explore Get
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
/event_ids:
get:
tags:
- Events
summary: Get events by ids
description: |-
+ **Access:** Authenticated user with access to the referenced camera.
+
Gets events by a list of ids.
Returns a list of events.
operationId: event_ids_event_ids_get
@@ -3472,27 +4231,32 @@ paths:
type: string
title: Ids
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
type: array
items:
- $ref: "#/components/schemas/EventResponse"
+ $ref: '#/components/schemas/EventResponse'
title: Response Event Ids Event Ids Get
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
/events/search:
get:
tags:
- Events
summary: Search events
description: |-
+ **Access:** Any authenticated user.
+
Searches for events in the database.
Returns a list of events.
operationId: events_search_events_search_get
@@ -3503,7 +4267,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Query
- name: event_id
in: query
@@ -3511,7 +4275,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Event Id
- name: search_type
in: query
@@ -3519,7 +4283,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: thumbnail
title: Search Type
- name: include_thumbnails
@@ -3528,7 +4292,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
default: 1
title: Include Thumbnails
- name: limit
@@ -3537,7 +4301,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
default: 50
title: Limit
- name: cameras
@@ -3546,7 +4310,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: all
title: Cameras
- name: labels
@@ -3555,7 +4319,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: all
title: Labels
- name: sub_labels
@@ -3564,7 +4328,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: all
title: Sub Labels
- name: attributes
@@ -3573,7 +4337,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: all
title: Attributes
- name: zones
@@ -3582,7 +4346,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: all
title: Zones
- name: after
@@ -3591,7 +4355,7 @@ paths:
schema:
anyOf:
- type: number
- - type: "null"
+ - type: 'null'
title: After
- name: before
in: query
@@ -3599,7 +4363,7 @@ paths:
schema:
anyOf:
- type: number
- - type: "null"
+ - type: 'null'
title: Before
- name: time_range
in: query
@@ -3607,8 +4371,8 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
- default: "00:00,24:00"
+ - type: 'null'
+ default: 00:00,24:00
title: Time Range
- name: has_clip
in: query
@@ -3616,7 +4380,7 @@ paths:
schema:
anyOf:
- type: boolean
- - type: "null"
+ - type: 'null'
title: Has Clip
- name: has_snapshot
in: query
@@ -3624,7 +4388,7 @@ paths:
schema:
anyOf:
- type: boolean
- - type: "null"
+ - type: 'null'
title: Has Snapshot
- name: is_submitted
in: query
@@ -3632,7 +4396,7 @@ paths:
schema:
anyOf:
- type: boolean
- - type: "null"
+ - type: 'null'
title: Is Submitted
- name: timezone
in: query
@@ -3640,7 +4404,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: utc
title: Timezone
- name: min_score
@@ -3649,7 +4413,7 @@ paths:
schema:
anyOf:
- type: number
- - type: "null"
+ - type: 'null'
title: Min Score
- name: max_score
in: query
@@ -3657,7 +4421,7 @@ paths:
schema:
anyOf:
- type: number
- - type: "null"
+ - type: 'null'
title: Max Score
- name: min_speed
in: query
@@ -3665,7 +4429,7 @@ paths:
schema:
anyOf:
- type: number
- - type: "null"
+ - type: 'null'
title: Min Speed
- name: max_speed
in: query
@@ -3673,7 +4437,7 @@ paths:
schema:
anyOf:
- type: number
- - type: "null"
+ - type: 'null'
title: Max Speed
- name: recognized_license_plate
in: query
@@ -3681,7 +4445,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: all
title: Recognized License Plate
- name: sort
@@ -3690,20 +4454,23 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Sort
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
/events/summary:
get:
tags:
@@ -3717,7 +4484,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: utc
title: Timezone
- name: has_clip
@@ -3726,7 +4493,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Has Clip
- name: has_snapshot
in: query
@@ -3734,26 +4501,33 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Has Snapshot
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/events/{event_id}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
+ description: '**Access:** Any authenticated user.'
+ /events/{event_id}:
get:
tags:
- Events
summary: Get event by id
- description: Gets an event by its id.
+ description: |-
+ **Access:** Authenticated user with access to the referenced camera.
+
+ Gets an event by its id.
operationId: event_events__event_id__get
parameters:
- name: event_id
@@ -3763,23 +4537,28 @@ paths:
type: string
title: Event Id
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/EventResponse"
- "422":
+ $ref: '#/components/schemas/EventResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
delete:
tags:
- Events
summary: Delete event
description: |-
+ **Access:** Admin role required.
+
Deletes an event from the database.
Returns a success message or an error if the event is not found.
operationId: delete_event_events__event_id__delete
@@ -3791,24 +4570,29 @@ paths:
type: string
title: Event Id
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/events/{event_id}/retain":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /events/{event_id}/retain:
post:
tags:
- Events
summary: Set event retain indefinitely.
description: |-
+ **Access:** Admin role required.
+
Sets an event to retain indefinitely.
Returns a success message or an error if the event is not found.
NOTE: This is a legacy endpoint and is not supported in the frontend.
@@ -3821,23 +4605,28 @@ paths:
type: string
title: Event Id
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
delete:
tags:
- Events
summary: Stop event from being retained indefinitely
description: |-
+ **Access:** Admin role required.
+
Stops an event from being retained indefinitely.
Returns a success message or an error if the event is not found.
NOTE: This is a legacy endpoint and is not supported in the frontend.
@@ -3850,24 +4639,29 @@ paths:
type: string
title: Event Id
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/events/{event_id}/plus":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /events/{event_id}/plus:
post:
tags:
- Events
summary: Send event to Frigate+
description: |-
+ **Access:** Admin role required.
+
Sends an event to Frigate+.
Returns a success message or an error if the event is not found.
operationId: send_to_plus_events__event_id__plus_post
@@ -3882,26 +4676,31 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/SubmitPlusBody"
+ $ref: '#/components/schemas/SubmitPlusBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/EventUploadPlusResponse"
- "422":
+ $ref: '#/components/schemas/EventUploadPlusResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/events/{event_id}/false_positive":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /events/{event_id}/false_positive:
put:
tags:
- Events
summary: Submit false positive to Frigate+
description: |-
+ **Access:** Admin role required.
+
Submit an event as a false positive to Frigate+.
This endpoint is the same as the standard Frigate+ submission endpoint,
but is specifically for marking an event as a false positive.
@@ -3914,24 +4713,29 @@ paths:
type: string
title: Event Id
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/EventUploadPlusResponse"
- "422":
+ $ref: '#/components/schemas/EventUploadPlusResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/events/{event_id}/sub_label":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /events/{event_id}/sub_label:
post:
tags:
- Events
summary: Set event sub label
description: |-
+ **Access:** Admin role required.
+
Sets an event's sub label.
Returns a success message or an error if the event is not found.
operationId: set_sub_label_events__event_id__sub_label_post
@@ -3947,26 +4751,31 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/EventsSubLabelBody"
+ $ref: '#/components/schemas/EventsSubLabelBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/events/{event_id}/recognized_license_plate":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /events/{event_id}/recognized_license_plate:
post:
tags:
- Events
summary: Set event license plate
description: |-
+ **Access:** Admin role required.
+
Sets an event's license plate.
Returns a success message or an error if the event is not found.
operationId: set_plate_events__event_id__recognized_license_plate_post
@@ -3982,28 +4791,32 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/EventsLPRBody"
+ $ref: '#/components/schemas/EventsLPRBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/events/{event_id}/attributes":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /events/{event_id}/attributes:
post:
tags:
- Events
summary: Set custom classification attributes
- description: >-
- Sets an event's custom classification attributes for all attribute-type
- models that apply to the event's object type.
+ description: |-
+ **Access:** Admin role required.
+
+ Sets an event's custom classification attributes for all attribute-type models that apply to the event's object type.
operationId: set_attributes_events__event_id__attributes_post
parameters:
- name: event_id
@@ -4017,26 +4830,31 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/EventsAttributesBody"
+ $ref: '#/components/schemas/EventsAttributesBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/events/{event_id}/description":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /events/{event_id}/description:
post:
tags:
- Events
summary: Set event description
description: |-
+ **Access:** Admin role required.
+
Sets an event's description.
Returns a success message or an error if the event is not found.
operationId: set_description_events__event_id__description_post
@@ -4052,29 +4870,35 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/EventsDescriptionBody"
+ $ref: '#/components/schemas/EventsDescriptionBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/events/{event_id}/description/regenerate":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /events/{event_id}/description/regenerate:
put:
tags:
- Events
summary: Regenerate event description
description: |-
+ **Access:** Admin role required.
+
Regenerates an event's description.
Returns a success message or an error if the event is not found.
- operationId: regenerate_description_events__event_id__description_regenerate_put
+ operationId:
+ regenerate_description_events__event_id__description_regenerate_put
parameters:
- name: event_id
in: path
@@ -4087,8 +4911,8 @@ paths:
required: false
schema:
anyOf:
- - $ref: "#/components/schemas/RegenerateDescriptionEnum"
- - type: "null"
+ - $ref: '#/components/schemas/RegenerateDescriptionEnum'
+ - type: 'null'
default: thumbnails
title: Source
- name: force
@@ -4097,28 +4921,33 @@ paths:
schema:
anyOf:
- type: boolean
- - type: "null"
+ - type: 'null'
default: false
title: Force
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/description/generate:
post:
tags:
- Events
summary: Generate description embedding
description: |-
+ **Access:** Admin role required.
+
Generates an embedding for an event's description.
Returns a success message or an error if the event is not found.
operationId: generate_description_embedding_description_generate_post
@@ -4127,26 +4956,31 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/EventsDescriptionBody"
+ $ref: '#/components/schemas/EventsDescriptionBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/events/:
delete:
tags:
- Events
summary: Delete events
description: |-
+ **Access:** Admin role required.
+
Deletes a list of events from the database.
Returns a success message or an error if the events are not found.
operationId: delete_events_events__delete
@@ -4155,26 +4989,31 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/EventsDeleteBody"
+ $ref: '#/components/schemas/EventsDeleteBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/EventMultiDeleteResponse"
- "422":
+ $ref: '#/components/schemas/EventMultiDeleteResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/events/{camera_name}/{label}/create":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /events/{camera_name}/{label}/create:
post:
tags:
- Events
summary: Create manual event
description: |-
+ **Access:** Admin role required.
+
Creates a manual event in the database.
Returns a success message or an error if the event is not found.
NOTES:
@@ -4198,31 +5037,36 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/EventsCreateBody"
+ $ref: '#/components/schemas/EventsCreateBody'
default:
- score: 0
+ score: 0.0
duration: 30
include_recording: true
draw: {}
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/EventCreateResponse"
- "422":
+ $ref: '#/components/schemas/EventCreateResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/events/{event_id}/end":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /events/{event_id}/end:
put:
tags:
- Events
summary: End manual event
description: |-
+ **Access:** Admin role required.
+
Ends a manual event.
Returns a success message or an error if the event is not found.
NOTE: This should only be used for manual events.
@@ -4239,26 +5083,31 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/EventsEndBody"
+ $ref: '#/components/schemas/EventsEndBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/trigger/embedding:
post:
tags:
- Events
summary: Create trigger embedding
description: |-
+ **Access:** Admin role required.
+
Creates a trigger embedding for a specific trigger.
Returns a success message or an error if the trigger is not found.
operationId: create_trigger_embedding_trigger_embedding_post
@@ -4280,30 +5129,36 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/TriggerEmbeddingBody"
+ $ref: '#/components/schemas/TriggerEmbeddingBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
type: object
title: Response Create Trigger Embedding Trigger Embedding Post
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/trigger/embedding/{camera_name}/{name}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /trigger/embedding/{camera_name}/{name}:
put:
tags:
- Events
summary: Update trigger embedding
description: |-
+ **Access:** Admin role required.
+
Updates a trigger embedding for a specific trigger.
Returns a success message or an error if the trigger is not found.
- operationId: update_trigger_embedding_trigger_embedding__camera_name___name__put
+ operationId:
+ update_trigger_embedding_trigger_embedding__camera_name___name__put
parameters:
- name: camera_name
in: path
@@ -4322,31 +5177,36 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/TriggerEmbeddingBody"
+ $ref: '#/components/schemas/TriggerEmbeddingBody'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
type: object
- title: >-
- Response Update Trigger Embedding Trigger Embedding Camera
- Name Name Put
- "422":
+ title: Response Update Trigger Embedding Trigger Embedding
+ Camera Name Name Put
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
delete:
tags:
- Events
summary: Delete trigger embedding
description: |-
+ **Access:** Admin role required.
+
Deletes a trigger embedding for a specific trigger.
Returns a success message or an error if the trigger is not found.
- operationId: delete_trigger_embedding_trigger_embedding__camera_name___name__delete
+ operationId:
+ delete_trigger_embedding_trigger_embedding__camera_name___name__delete
parameters:
- name: camera_name
in: path
@@ -4361,27 +5221,31 @@ paths:
type: string
title: Name
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
type: object
- title: >-
- Response Delete Trigger Embedding Trigger Embedding Camera
- Name Name Delete
- "422":
+ title: Response Delete Trigger Embedding Trigger Embedding
+ Camera Name Name Delete
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/triggers/status/{camera_name}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /triggers/status/{camera_name}:
get:
tags:
- Events
summary: Get triggers status
description: |-
+ **Access:** Admin role required.
+
Gets the status of all triggers for a specific camera.
Returns a success message or an error if the camera is not found.
operationId: get_triggers_status_triggers_status__camera_name__get
@@ -4393,20 +5257,24 @@ paths:
type: string
title: Camera Name
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
type: object
- title: Response Get Triggers Status Triggers Status Camera Name Get
- "422":
+ title: Response Get Triggers Status Triggers Status Camera Name
+ Get
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/{camera_name}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /{camera_name}:
get:
tags:
- Media
@@ -4419,7 +5287,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera Name
- name: fps
in: query
@@ -4441,7 +5309,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Bbox
- name: timestamp
in: query
@@ -4449,7 +5317,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Timestamp
- name: zones
in: query
@@ -4457,7 +5325,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Zones
- name: mask
in: query
@@ -4465,7 +5333,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Mask
- name: motion
in: query
@@ -4473,7 +5341,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Motion
- name: regions
in: query
@@ -4481,21 +5349,25 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Regions
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/{camera_name}/ptz/info":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ description: '**Access:** Authenticated user with access to the referenced camera.'
+ /{camera_name}/ptz/info:
get:
tags:
- Media
@@ -4508,29 +5380,33 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera Name
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/{camera_name}/latest.{extension}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ description: '**Access:** Authenticated user with access to the referenced camera.'
+ /{camera_name}/latest.{extension}:
get:
tags:
- Media
summary: Latest Frame
- description: >-
- Returns the latest frame from the specified camera in the requested
- format (jpg, png, webp). Falls back to preview frames if the camera is
- offline.
+ description: |-
+ **Access:** Authenticated user with access to the referenced camera.
+
+ Returns the latest frame from the specified camera in the requested format (jpg, png, webp). Falls back to preview frames if the camera is offline.
operationId: latest_frame__camera_name__latest__extension__get
parameters:
- name: camera_name
@@ -4539,20 +5415,20 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera Name
- name: extension
in: path
required: true
schema:
- $ref: "#/components/schemas/Extension"
+ $ref: '#/components/schemas/Extension'
- name: bbox
in: query
required: false
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Bbox
- name: timestamp
in: query
@@ -4560,7 +5436,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Timestamp
- name: zones
in: query
@@ -4568,7 +5444,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Zones
- name: mask
in: query
@@ -4576,7 +5452,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Mask
- name: motion
in: query
@@ -4584,7 +5460,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Motion
- name: paths
in: query
@@ -4592,7 +5468,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Paths
- name: regions
in: query
@@ -4600,7 +5476,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Regions
- name: quality
in: query
@@ -4608,7 +5484,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
default: 70
title: Quality
- name: height
@@ -4617,7 +5493,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Height
- name: store
in: query
@@ -4625,26 +5501,29 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Store
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/{camera_name}/recordings/{frame_time}/snapshot.{format}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ /{camera_name}/recordings/{frame_time}/snapshot.{format}:
get:
tags:
- Media
summary: Get Snapshot From Recording
- operationId: >-
+ operationId:
get_snapshot_from_recording__camera_name__recordings__frame_time__snapshot__format__get
parameters:
- name: camera_name
@@ -4653,7 +5532,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera Name
- name: frame_time
in: path
@@ -4677,23 +5556,28 @@ paths:
type: integer
title: Height
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/{camera_name}/plus/{frame_time}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ description: '**Access:** Authenticated user with access to the referenced camera.'
+ /{camera_name}/plus/{frame_time}:
post:
tags:
- Media
summary: Submit Recording Snapshot To Plus
- operationId: submit_recording_snapshot_to_plus__camera_name__plus__frame_time__post
+ operationId:
+ submit_recording_snapshot_to_plus__camera_name__plus__frame_time__post
parameters:
- name: camera_name
in: path
@@ -4701,7 +5585,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera Name
- name: frame_time
in: path
@@ -4710,26 +5594,32 @@ paths:
type: string
title: Frame Time
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/{camera_name}/start/{start_ts}/end/{end_ts}/clip.mp4":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ description: '**Access:** Authenticated user with access to the referenced camera.'
+ /{camera_name}/start/{start_ts}/end/{end_ts}/clip.mp4:
get:
tags:
- Media
summary: Recording Clip
- description: >-
- For iOS devices, use the master.m3u8 HLS link instead of clip.mp4.
- Safari does not reliably process progressive mp4 files.
- operationId: recording_clip__camera_name__start__start_ts__end__end_ts__clip_mp4_get
+ description: |-
+ **Access:** Authenticated user with access to the referenced camera.
+
+ For iOS devices, use the master.m3u8 HLS link instead of clip.mp4. Safari does not reliably process progressive mp4 files.
+ operationId:
+ recording_clip__camera_name__start__start_ts__end__end_ts__clip_mp4_get
parameters:
- name: camera_name
in: path
@@ -4737,7 +5627,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera Name
- name: start_ts
in: path
@@ -4752,25 +5642,29 @@ paths:
type: number
title: End Ts
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/vod/{camera_name}/start/{start_ts}/end/{end_ts}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ /vod/{camera_name}/start/{start_ts}/end/{end_ts}:
get:
tags:
- Media
summary: Vod Ts
- description: >-
- Returns an HLS playlist for the specified timestamp-range on the
- specified camera. Append /master.m3u8 or /index.m3u8 for HLS playback.
+ description: |-
+ **Access:** Authenticated user with access to the referenced camera.
+
+ Returns an HLS playlist for the specified timestamp-range on the specified camera. Append /master.m3u8 or /index.m3u8 for HLS playback.
operationId: vod_ts_vod__camera_name__start__start_ts__end__end_ts__get
parameters:
- name: camera_name
@@ -4779,7 +5673,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera Name
- name: start_ts
in: path
@@ -4801,26 +5695,31 @@ paths:
default: false
title: Force Discontinuity
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/vod/{year_month}/{day}/{hour}/{camera_name}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ /vod/{year_month}/{day}/{hour}/{camera_name}:
get:
tags:
- Media
summary: Vod Hour No Timezone
- description: >-
- Returns an HLS playlist for the specified date-time on the specified
- camera. Append /master.m3u8 or /index.m3u8 for HLS playback.
- operationId: vod_hour_no_timezone_vod__year_month___day___hour___camera_name__get
+ description: |-
+ **Access:** Authenticated user with access to the referenced camera.
+
+ Returns an HLS playlist for the specified date-time on the specified camera. Append /master.m3u8 or /index.m3u8 for HLS playback.
+ operationId:
+ vod_hour_no_timezone_vod__year_month___day___hour___camera_name__get
parameters:
- name: year_month
in: path
@@ -4846,30 +5745,34 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera Name
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/vod/{year_month}/{day}/{hour}/{camera_name}/{tz_name}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ /vod/{year_month}/{day}/{hour}/{camera_name}/{tz_name}:
get:
tags:
- Media
summary: Vod Hour
- description: >-
- Returns an HLS playlist for the specified date-time (with timezone) on
- the specified camera. Append /master.m3u8 or /index.m3u8 for HLS
- playback.
- operationId: vod_hour_vod__year_month___day___hour___camera_name___tz_name__get
+ description: |-
+ **Access:** Authenticated user with access to the referenced camera.
+
+ Returns an HLS playlist for the specified date-time (with timezone) on the specified camera. Append /master.m3u8 or /index.m3u8 for HLS playback.
+ operationId:
+ vod_hour_vod__year_month___day___hour___camera_name___tz_name__get
parameters:
- name: year_month
in: path
@@ -4895,7 +5798,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera Name
- name: tz_name
in: path
@@ -4904,25 +5807,29 @@ paths:
type: string
title: Tz Name
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/vod/event/{event_id}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ /vod/event/{event_id}:
get:
tags:
- Media
summary: Vod Event
- description: >-
- Returns an HLS playlist for the specified object. Append /master.m3u8 or
- /index.m3u8 for HLS playback.
+ description: |-
+ **Access:** Authenticated user with access to the referenced camera.
+
+ Returns an HLS playlist for the specified object. Append /master.m3u8 or /index.m3u8 for HLS playback.
operationId: vod_event_vod_event__event_id__get
parameters:
- name: event_id
@@ -4941,26 +5848,31 @@ paths:
title: Padding
description: Padding to apply to the vod.
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/vod/clip/{camera_name}/start/{start_ts}/end/{end_ts}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ /vod/clip/{camera_name}/start/{start_ts}/end/{end_ts}:
get:
tags:
- Media
summary: Vod Clip
- description: >-
- Returns an HLS playlist for a timestamp range with HLS discontinuity
- enabled. Append /master.m3u8 or /index.m3u8 for HLS playback.
- operationId: vod_clip_vod_clip__camera_name__start__start_ts__end__end_ts__get
+ description: |-
+ **Access:** Authenticated user with access to the referenced camera.
+
+ Returns an HLS playlist for a timestamp range with HLS discontinuity enabled. Append /master.m3u8 or /index.m3u8 for HLS playback.
+ operationId:
+ vod_clip_vod_clip__camera_name__start__start_ts__end__end_ts__get
parameters:
- name: camera_name
in: path
@@ -4968,7 +5880,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera Name
- name: start_ts
in: path
@@ -4983,23 +5895,29 @@ paths:
type: number
title: End Ts
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/events/{event_id}/snapshot.jpg":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ /events/{event_id}/snapshot.jpg:
get:
tags:
- Media
summary: Event Snapshot
- description: Returns a snapshot image for the specified object id.
+ description: |-
+ **Access:** Authenticated user with access to the referenced camera.
+
+ Returns a snapshot image for the specified object id.
operationId: event_snapshot_events__event_id__snapshot_jpg_get
parameters:
- name: event_id
@@ -5014,7 +5932,7 @@ paths:
schema:
anyOf:
- type: boolean
- - type: "null"
+ - type: 'null'
default: false
title: Download
- name: timestamp
@@ -5023,7 +5941,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Timestamp
- name: bbox
in: query
@@ -5031,7 +5949,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Bbox
- name: crop
in: query
@@ -5039,7 +5957,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Crop
- name: height
in: query
@@ -5047,7 +5965,7 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Height
- name: quality
in: query
@@ -5055,21 +5973,24 @@ paths:
schema:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Quality
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/events/{event_id}/thumbnail.{extension}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ /events/{event_id}/thumbnail.{extension}:
get:
tags:
- Media
@@ -5086,7 +6007,7 @@ paths:
in: path
required: true
schema:
- $ref: "#/components/schemas/Extension"
+ $ref: '#/components/schemas/Extension'
- name: max_cache_age
in: query
required: false
@@ -5106,27 +6027,23 @@ paths:
- android
default: ios
title: Format
- - name: camera_name
- in: query
- required: false
- schema:
- anyOf:
- - type: string
- - type: "null"
- title: Camera Name
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/{camera_name}/grid.jpg":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ description: '**Access:** Authenticated user with access to the referenced camera.'
+ /{camera_name}/grid.jpg:
get:
tags:
- Media
@@ -5139,7 +6056,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera Name
- name: color
in: query
@@ -5156,23 +6073,30 @@ paths:
default: 0.5
title: Font Scale
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/{camera_name}/region_grid":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ description: '**Access:** Authenticated user with access to the referenced camera.'
+ /{camera_name}/region_grid:
delete:
tags:
- Media
summary: Clear Region Grid
- description: Clear the region grid for a camera.
+ description: |-
+ **Access:** Admin role required.
+
+ Clear the region grid for a camera.
operationId: clear_region_grid__camera_name__region_grid_delete
parameters:
- name: camera_name
@@ -5182,23 +6106,27 @@ paths:
type: string
title: Camera Name
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/events/{event_id}/snapshot-clean.webp":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /events/{event_id}/snapshot-clean.webp:
get:
tags:
- Media
summary: Event Snapshot Clean
- operationId: event_snapshot_clean_events__event_id__snapshot_clean_webp_get
+ operationId:
+ event_snapshot_clean_events__event_id__snapshot_clean_webp_get
parameters:
- name: event_id
in: path
@@ -5213,27 +6141,23 @@ paths:
type: boolean
default: false
title: Download
- - name: camera_name
- in: query
- required: false
- schema:
- anyOf:
- - type: string
- - type: "null"
- title: Camera Name
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/events/{event_id}/clip.mp4":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ description: '**Access:** Authenticated user with access to the referenced camera.'
+ /events/{event_id}/clip.mp4:
get:
tags:
- Media
@@ -5255,27 +6179,61 @@ paths:
default: 0
title: Padding
description: Padding to apply to clip.
- - name: camera_name
- in: query
- required: false
- schema:
- anyOf:
- - type: string
- - type: "null"
- title: Camera Name
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/events/{event_id}/preview.gif":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ description: '**Access:** Authenticated user with access to the referenced camera.'
+ /review/{review_id}/clip.mp4:
+ get:
+ tags:
+ - Media
+ summary: Review Clip
+ operationId: review_clip_review__review_id__clip_mp4_get
+ parameters:
+ - name: review_id
+ in: path
+ required: true
+ schema:
+ type: string
+ title: Review Id
+ - name: padding
+ in: query
+ required: false
+ schema:
+ type: integer
+ description: Padding to apply to clip.
+ default: 0
+ title: Padding
+ description: Padding to apply to clip.
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema: {}
+ '422':
+ description: Validation Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ description: '**Access:** Authenticated user with access to the referenced camera.'
+ /events/{event_id}/preview.gif:
get:
tags:
- Media
@@ -5288,32 +6246,29 @@ paths:
schema:
type: string
title: Event Id
- - name: camera_name
- in: query
- required: false
- schema:
- anyOf:
- - type: string
- - type: "null"
- title: Camera Name
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/{camera_name}/start/{start_ts}/end/{end_ts}/preview.gif":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ description: '**Access:** Authenticated user with access to the referenced camera.'
+ /{camera_name}/start/{start_ts}/end/{end_ts}/preview.gif:
get:
tags:
- Media
summary: Preview Gif
- operationId: preview_gif__camera_name__start__start_ts__end__end_ts__preview_gif_get
+ operationId:
+ preview_gif__camera_name__start__start_ts__end__end_ts__preview_gif_get
parameters:
- name: camera_name
in: path
@@ -5321,7 +6276,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera Name
- name: start_ts
in: path
@@ -5345,23 +6300,28 @@ paths:
title: Max Cache Age
description: Max cache age in seconds. Default 30 days in seconds.
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/{camera_name}/start/{start_ts}/end/{end_ts}/preview.mp4":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ description: '**Access:** Authenticated user with access to the referenced camera.'
+ /{camera_name}/start/{start_ts}/end/{end_ts}/preview.mp4:
get:
tags:
- Media
summary: Preview Mp4
- operationId: preview_mp4__camera_name__start__start_ts__end__end_ts__preview_mp4_get
+ operationId:
+ preview_mp4__camera_name__start__start_ts__end__end_ts__preview_mp4_get
parameters:
- name: camera_name
in: path
@@ -5369,7 +6329,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera Name
- name: start_ts
in: path
@@ -5393,18 +6353,22 @@ paths:
title: Max Cache Age
description: Max cache age in seconds. Default 7 days in seconds.
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/review/{event_id}/preview":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ description: '**Access:** Authenticated user with access to the referenced camera.'
+ /review/{event_id}/preview:
get:
tags:
- Media
@@ -5427,32 +6391,31 @@ paths:
- mp4
default: gif
title: Format
- - name: camera_name
- in: query
- required: false
- schema:
- anyOf:
- - type: string
- - type: "null"
- title: Camera Name
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/preview/{file_name}/thumbnail.webp":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ description: '**Access:** Authenticated user with access to the referenced camera.'
+ /preview/{file_name}/thumbnail.webp:
get:
tags:
- Media
summary: Preview Thumbnail
- description: Get a thumbnail from the cached preview frames.
+ description: |-
+ **Access:** Authenticated user with access to the referenced camera.
+
+ Get a thumbnail from the cached preview frames.
operationId: preview_thumbnail_preview__file_name__thumbnail_webp_get
parameters:
- name: file_name
@@ -5461,32 +6424,30 @@ paths:
schema:
type: string
title: File Name
- - name: camera_name
- in: query
- required: false
- schema:
- anyOf:
- - type: string
- - type: "null"
- title: Camera Name
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/preview/{file_name}/thumbnail.jpg":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ /preview/{file_name}/thumbnail.jpg:
get:
tags:
- Media
summary: Preview Thumbnail
- description: Get a thumbnail from the cached preview frames.
+ description: |-
+ **Access:** Authenticated user with access to the referenced camera.
+
+ Get a thumbnail from the cached preview frames.
operationId: preview_thumbnail_preview__file_name__thumbnail_jpg_get
parameters:
- name: file_name
@@ -5495,27 +6456,22 @@ paths:
schema:
type: string
title: File Name
- - name: camera_name
- in: query
- required: false
- schema:
- anyOf:
- - type: string
- - type: "null"
- title: Camera Name
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/{camera_name}/{label}/thumbnail.jpg":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ /{camera_name}/{label}/thumbnail.jpg:
get:
tags:
- Media
@@ -5528,7 +6484,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera Name
- name: label
in: path
@@ -5537,18 +6493,22 @@ paths:
type: string
title: Label
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/{camera_name}/{label}/best.jpg":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ description: '**Access:** Authenticated user with access to the referenced camera.'
+ /{camera_name}/{label}/best.jpg:
get:
tags:
- Media
@@ -5561,7 +6521,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera Name
- name: label
in: path
@@ -5570,18 +6530,22 @@ paths:
type: string
title: Label
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/{camera_name}/{label}/clip.mp4":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ description: '**Access:** Authenticated user with access to the referenced camera.'
+ /{camera_name}/{label}/clip.mp4:
get:
tags:
- Media
@@ -5594,7 +6558,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera Name
- name: label
in: path
@@ -5603,25 +6567,30 @@ paths:
type: string
title: Label
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/{camera_name}/{label}/snapshot.jpg":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ description: '**Access:** Authenticated user with access to the referenced camera.'
+ /{camera_name}/{label}/snapshot.jpg:
get:
tags:
- Media
summary: Label Snapshot
- description: >-
- Returns the snapshot image from the latest event for the given camera
- and label combo
+ description: |-
+ **Access:** Authenticated user with access to the referenced camera.
+
+ Returns the snapshot image from the latest event for the given camera and label combo
operationId: label_snapshot__camera_name___label__snapshot_jpg_get
parameters:
- name: camera_name
@@ -5630,7 +6599,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera Name
- name: label
in: path
@@ -5639,23 +6608,28 @@ paths:
type: string
title: Label
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/{camera_name}/search/motion":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ /{camera_name}/search/motion:
post:
tags:
- Motion Search
summary: Start motion search job
description: |-
+ **Access:** Authenticated user with access to the referenced camera.
+
Starts an asynchronous search for significant motion changes within
a user-defined Region of Interest (ROI) over a specified time range. Returns a job_id
that can be used to poll for results.
@@ -5667,34 +6641,40 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera Name
requestBody:
required: true
content:
application/json:
schema:
- $ref: "#/components/schemas/MotionSearchRequest"
+ $ref: '#/components/schemas/MotionSearchRequest'
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/MotionSearchStartResponse"
- "422":
+ $ref: '#/components/schemas/MotionSearchStartResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/{camera_name}/search/motion/{job_id}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ /{camera_name}/search/motion/{job_id}:
get:
tags:
- Motion Search
summary: Get motion search job status
- description: Returns the status and results (if complete) of a motion search job.
- operationId: >-
+ description: |-
+ **Access:** Authenticated user with access to the referenced camera.
+
+ Returns the status and results (if complete) of a motion search job.
+ operationId:
get_motion_search_status_endpoint__camera_name__search_motion__job_id__get
parameters:
- name: camera_name
@@ -5703,7 +6683,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera Name
- name: job_id
in: path
@@ -5712,25 +6692,31 @@ paths:
type: string
title: Job Id
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/MotionSearchStatusResponse"
- "422":
+ $ref: '#/components/schemas/MotionSearchStatusResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/{camera_name}/search/motion/{job_id}/cancel":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ /{camera_name}/search/motion/{job_id}/cancel:
post:
tags:
- Motion Search
summary: Cancel motion search job
- description: Cancels an active motion search job if it is still processing.
- operationId: >-
+ description: |-
+ **Access:** Authenticated user with access to the referenced camera.
+
+ Cancels an active motion search job if it is still processing.
+ operationId:
cancel_motion_search_endpoint__camera_name__search_motion__job_id__cancel_post
parameters:
- name: camera_name
@@ -5739,7 +6725,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera Name
- name: job_id
in: path
@@ -5748,17 +6734,20 @@ paths:
type: string
title: Job Id
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
/recordings/storage:
get:
tags:
@@ -5766,17 +6755,24 @@ paths:
summary: Get Recordings Storage Usage
operationId: get_recordings_storage_usage_recordings_storage_get
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ description: '**Access:** Admin role required.'
/recordings/summary:
get:
tags:
- Recordings
summary: All Recordings Summary
- description: Returns true/false by day indicating if recordings exist
+ description: |-
+ **Access:** Any authenticated user.
+
+ Returns true/false by day indicating if recordings exist
operationId: all_recordings_summary_recordings_summary_get
parameters:
- name: timezone
@@ -5792,27 +6788,33 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: all
title: Cameras
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/{camera_name}/recordings/summary":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
+ /{camera_name}/recordings/summary:
get:
tags:
- Recordings
summary: Recordings Summary
- description: Returns hourly summary for recordings of given camera
+ description: |-
+ **Access:** Authenticated user with access to the referenced camera.
+
+ Returns hourly summary for recordings of given camera
operationId: recordings_summary__camera_name__recordings_summary_get
parameters:
- name: camera_name
@@ -5821,7 +6823,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera Name
- name: timezone
in: query
@@ -5831,25 +6833,29 @@ paths:
default: utc
title: Timezone
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/{camera_name}/recordings":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
+ /{camera_name}/recordings:
get:
tags:
- Recordings
summary: Recordings
- description: >-
- Return specific camera recordings between the given 'after'/'end' times.
- If not provided the last hour will be used
+ description: |-
+ **Access:** Authenticated user with access to the referenced camera.
+
+ Return specific camera recordings between the given 'after'/'end' times. If not provided the last hour will be used
operationId: recordings__camera_name__recordings_get
parameters:
- name: camera_name
@@ -5858,40 +6864,44 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera Name
- name: after
in: query
required: false
schema:
type: number
- default: 1774023877.74743
title: After
- name: before
in: query
required: false
schema:
type: number
- default: 1774027477.74744
title: Before
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema: {}
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: camera
/recordings/unavailable:
get:
tags:
- Recordings
summary: No Recordings
- description: Get time ranges with no recordings.
+ description: |-
+ **Access:** Any authenticated user.
+
+ Get time ranges with no recordings.
operationId: no_recordings_recordings_unavailable_get
parameters:
- name: cameras
@@ -5921,7 +6931,7 @@ paths:
default: 30
title: Scale
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
@@ -5930,18 +6940,23 @@ paths:
items:
type: object
title: Response No Recordings Recordings Unavailable Get
- "422":
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
- "/recordings/start/{start}/end/{end}":
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateUserAuth: []
+ x-required-role: any
+ /recordings/start/{start}/end/{end}:
delete:
tags:
- Recordings
summary: Delete recordings
description: |-
+ **Access:** Admin role required.
+
Deletes recordings within the specified time range.
Recordings can be filtered by cameras and kept based on motion, objects, or audio attributes.
operationId: delete_recordings_recordings_start__start__end__end__delete
@@ -5968,7 +6983,7 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Keep
- name: cameras
in: query
@@ -5976,76 +6991,140 @@ paths:
schema:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
default: all
title: Cameras
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/GenericResponse"
- "422":
+ $ref: '#/components/schemas/GenericResponse'
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/debug_replay/start:
post:
tags:
- App
summary: Start debug replay
- description: Start a debug replay session from camera recordings.
+ description: |-
+ **Access:** Admin role required.
+
+ Start a debug replay session from camera recordings. Returns immediately while clip generation runs as a background job; subscribe to the 'debug_replay' job_state WS topic to track progress.
operationId: start_debug_replay_debug_replay_start_post
requestBody:
required: true
content:
application/json:
schema:
- $ref: "#/components/schemas/DebugReplayStartBody"
+ $ref: '#/components/schemas/DebugReplayStartBody'
responses:
- "200":
+ '202':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/DebugReplayStartResponse"
- "422":
+ $ref: '#/components/schemas/DebugReplayStartResponse'
+ '400':
+ description: Invalid camera, time range, or no recordings
+ '409':
+ description: A replay session is already active
+ '422':
description: Validation Error
content:
application/json:
schema:
- $ref: "#/components/schemas/HTTPValidationError"
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
+ /debug_replay/start_from_export:
+ post:
+ tags:
+ - App
+ summary: Start debug replay from an export
+ description: |-
+ **Access:** Admin role required.
+
+ Start a debug replay session covering an existing export's time range. The end time is derived from the export's video duration.
+ operationId:
+ start_debug_replay_from_export_debug_replay_start_from_export_post
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/DebugReplayStartFromExportBody'
+ responses:
+ '202':
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/DebugReplayStartResponse'
+ '400':
+ description: Invalid export, time range, or no recordings
+ '404':
+ description: Export not found
+ '409':
+ description: A replay session is already active
+ '422':
+ description: Validation Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPValidationError'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/debug_replay/status:
get:
tags:
- App
summary: Get debug replay status
- description: Get the status of the current debug replay session.
+ description: |-
+ **Access:** Admin role required.
+
+ Get the status of the current debug replay session.
operationId: get_debug_replay_status_debug_replay_status_get
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/DebugReplayStatusResponse"
+ $ref: '#/components/schemas/DebugReplayStatusResponse'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
/debug_replay/stop:
post:
tags:
- App
summary: Stop debug replay
- description: Stop the active debug replay session and clean up all artifacts.
+ description: |-
+ **Access:** Admin role required.
+
+ Stop the active debug replay session and clean up all artifacts.
operationId: stop_debug_replay_debug_replay_stop_post
responses:
- "200":
+ '200':
description: Successful Response
content:
application/json:
schema:
- $ref: "#/components/schemas/DebugReplayStopResponse"
+ $ref: '#/components/schemas/DebugReplayStopResponse'
+ security:
+ - frigateAdminAuth: []
+ x-required-role: admin
components:
schemas:
AppConfigSetBody:
@@ -6057,12 +7136,12 @@ components:
update_topic:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Update Topic
config_data:
anyOf:
- type: object
- - type: "null"
+ - type: 'null'
title: Config Data
skip_save:
type: boolean
@@ -6094,7 +7173,7 @@ components:
role:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Role
default: viewer
type: object
@@ -6110,7 +7189,7 @@ components:
old_password:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Old Password
type: object
required:
@@ -6135,6 +7214,155 @@ components:
required:
- event_id
title: AudioTranscriptionBody
+ BatchExportBody:
+ properties:
+ items:
+ items:
+ $ref: '#/components/schemas/BatchExportItem'
+ type: array
+ maxItems: 50
+ minItems: 1
+ title: Items
+ description: List of export items. Each item has its own camera and
+ time range.
+ export_case_id:
+ anyOf:
+ - type: string
+ maxLength: 30
+ - type: 'null'
+ title: Export case ID
+ description: Existing export case ID to assign all exports to.
+ Attaching to an existing case is temporarily admin-only until
+ case-level ACLs exist.
+ new_case_name:
+ anyOf:
+ - type: string
+ maxLength: 100
+ - type: 'null'
+ title: New case name
+ description: Name of a new export case to create when export_case_id
+ is omitted
+ new_case_description:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: New case description
+ description: Optional description for a newly created export case
+ type: object
+ required:
+ - items
+ title: BatchExportBody
+ BatchExportItem:
+ properties:
+ camera:
+ type: string
+ title: Camera name
+ start_time:
+ type: number
+ title: Start time
+ end_time:
+ type: number
+ title: End time
+ image_path:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Existing thumbnail path
+ description: Optional existing image to use as the export thumbnail
+ friendly_name:
+ anyOf:
+ - type: string
+ maxLength: 256
+ - type: 'null'
+ title: Friendly name
+ description: Optional friendly name for this specific export item
+ client_item_id:
+ anyOf:
+ - type: string
+ maxLength: 128
+ - type: 'null'
+ title: Client item ID
+ description: Optional opaque client identifier echoed back in results
+ type: object
+ required:
+ - camera
+ - start_time
+ - end_time
+ title: BatchExportItem
+ BatchExportResponse:
+ properties:
+ export_case_id:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Export Case Id
+ description: Export case ID associated with the batch
+ export_ids:
+ items:
+ type: string
+ type: array
+ title: Export Ids
+ description: Export IDs successfully queued
+ results:
+ items:
+ $ref: '#/components/schemas/BatchExportResultModel'
+ type: array
+ title: Results
+ description: Per-item batch export results
+ type: object
+ required:
+ - export_ids
+ - results
+ title: BatchExportResponse
+ description: Response model for starting an export batch.
+ BatchExportResultModel:
+ properties:
+ camera:
+ type: string
+ title: Camera
+ description: Camera name for this export attempt
+ export_id:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Export Id
+ description: The export ID when the export was successfully queued
+ success:
+ type: boolean
+ title: Success
+ description: Whether the export was successfully queued
+ status:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Status
+ description: Queue status for this camera export
+ error:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Error
+ description: Validation or queueing error for this item, if any
+ item_index:
+ anyOf:
+ - type: integer
+ - type: 'null'
+ title: Item Index
+ description: Zero-based index of this result within the request items
+ list
+ client_item_id:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Client Item Id
+ description: Opaque client-supplied item identifier echoed from the
+ request
+ type: object
+ required:
+ - camera
+ - success
+ title: BatchExportResultModel
+ description: Per-item result for a batch export request.
Body_recognize_face_faces_recognize_post:
properties:
file:
@@ -6165,28 +7393,42 @@ components:
required:
- value
title: CameraSetBody
+ ChaptersEnum:
+ type: string
+ enum:
+ - none
+ - recording_segments
+ - review_items
+ title: ChaptersEnum
ChatCompletionRequest:
properties:
messages:
items:
- $ref: "#/components/schemas/ChatMessage"
+ $ref: '#/components/schemas/ChatMessage'
type: array
title: Messages
description: List of messages in the conversation
max_tool_iterations:
type: integer
- maximum: 10
- minimum: 1
+ maximum: 10.0
+ minimum: 1.0
title: Max Tool Iterations
- description: "Maximum number of tool call iterations (default: 5)"
+ description: 'Maximum number of tool call iterations (default: 5)'
default: 5
stream:
type: boolean
title: Stream
- description: >-
- If true, stream the final assistant response in the body as
- newline-delimited JSON.
+ description: If true, stream the final assistant response in the body
+ as newline-delimited JSON.
default: false
+ enable_thinking:
+ anyOf:
+ - type: boolean
+ - type: 'null'
+ title: Enable Thinking
+ description: Per-request thinking toggle. None means use the provider
+ default. Ignored by providers that do not expose a per-request
+ thinking switch.
type: object
required:
- messages
@@ -6199,25 +7441,39 @@ components:
title: Role
description: "Message role: 'user', 'assistant', 'system', or 'tool'"
content:
- type: string
+ anyOf:
+ - {}
+ - type: 'null'
title: Content
- description: Message content
+ description: Message content. Usually a string, but may be a
+ multimodal content list (e.g. text + image_url) or null for
+ assistant turns that only request tool calls.
tool_call_id:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Tool Call Id
- description: "For tool messages, the ID of the tool call"
+ description: For tool messages, the ID of the tool call
name:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Name
- description: "For tool messages, the tool name"
+ description: For tool messages, the tool name
+ tool_calls:
+ anyOf:
+ - items:
+ type: object
+ type: array
+ - type: 'null'
+ title: Tool Calls
+ description: For assistant messages replayed from prior turns, the
+ OpenAI-format tool calls the model previously requested. Replaying
+ these verbatim keeps the conversation prefix byte-for-byte identical
+ so the model server's prompt cache hits on follow-up turns.
type: object
required:
- role
- - content
title: ChatMessage
description: A single message in a chat conversation.
DayReview:
@@ -6264,6 +7520,17 @@ components:
- end_time
title: DebugReplayStartBody
description: Request body for starting a debug replay session.
+ DebugReplayStartFromExportBody:
+ properties:
+ export_id:
+ type: string
+ title: Export id
+ type: object
+ required:
+ - export_id
+ title: DebugReplayStartFromExportBody
+ description: Request body for starting a debug replay session from an
+ export.
DebugReplayStartResponse:
properties:
success:
@@ -6272,10 +7539,14 @@ components:
replay_camera:
type: string
title: Replay Camera
+ job_id:
+ type: string
+ title: Job Id
type: object
required:
- success
- replay_camera
+ - job_id
title: DebugReplayStartResponse
description: Response for starting a debug replay session.
DebugReplayStatusResponse:
@@ -6286,22 +7557,22 @@ components:
replay_camera:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Replay Camera
source_camera:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Source Camera
start_time:
anyOf:
- type: number
- - type: "null"
+ - type: 'null'
title: Start Time
end_time:
anyOf:
- type: number
- - type: "null"
+ - type: 'null'
title: End Time
live_ready:
type: boolean
@@ -6311,7 +7582,13 @@ components:
required:
- active
title: DebugReplayStatusResponse
- description: Response for debug replay status.
+ description: |-
+ Response for debug replay status.
+
+ Returns only session-presence fields. Startup progress and error
+ details flow through the job_state WebSocket topic via the
+ debug_replay job (see frigate.jobs.debug_replay); the
+ Replay page subscribes there with useJobStatus("debug_replay").
DebugReplayStopResponse:
properties:
success:
@@ -6383,7 +7660,7 @@ components:
sub_label:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Sub Label
camera:
type: string
@@ -6394,12 +7671,12 @@ components:
end_time:
anyOf:
- type: number
- - type: "null"
+ - type: 'null'
title: End Time
false_positive:
anyOf:
- type: boolean
- - type: "null"
+ - type: 'null'
title: False Positive
zones:
items:
@@ -6409,7 +7686,7 @@ components:
thumbnail:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Thumbnail
has_clip:
type: boolean
@@ -6423,22 +7700,22 @@ components:
plus_id:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Plus Id
model_hash:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Model Hash
detector_type:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Detector Type
model_type:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Model Type
data:
type: object
@@ -6490,36 +7767,36 @@ components:
sub_label:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Sub Label
score:
anyOf:
- type: number
- - type: "null"
+ - type: 'null'
title: Score
default: 0
duration:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Duration
default: 30
include_recording:
anyOf:
- type: boolean
- - type: "null"
+ - type: 'null'
title: Include Recording
default: true
draw:
anyOf:
- type: object
- - type: "null"
+ - type: 'null'
title: Draw
default: {}
pre_capture:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Pre Capture
type: object
title: EventsCreateBody
@@ -6539,7 +7816,7 @@ components:
description:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: The description of the event
type: object
required:
@@ -6550,7 +7827,7 @@ components:
end_time:
anyOf:
- type: number
- - type: "null"
+ - type: 'null'
title: End Time
type: object
title: EventsEndBody
@@ -6563,157 +7840,14 @@ components:
recognizedLicensePlateScore:
anyOf:
- type: number
- maximum: 1
- exclusiveMinimum: 0
- - type: "null"
+ maximum: 1.0
+ exclusiveMinimum: 0.0
+ - type: 'null'
title: Score for recognized license plate
type: object
required:
- recognizedLicensePlate
title: EventsLPRBody
- BatchExportBody:
- properties:
- items:
- items:
- $ref: "#/components/schemas/BatchExportItem"
- type: array
- minItems: 1
- maxItems: 50
- title: Items
- description: List of export items. Each item has its own camera and time range.
- export_case_id:
- anyOf:
- - type: string
- maxLength: 30
- - type: "null"
- title: Export case ID
- description: Existing export case ID to assign all exports to. Attaching to an existing case is temporarily admin-only until case-level ACLs exist.
- new_case_name:
- anyOf:
- - type: string
- maxLength: 100
- - type: "null"
- title: New case name
- description: Name of a new export case to create when export_case_id is omitted
- new_case_description:
- anyOf:
- - type: string
- - type: "null"
- title: New case description
- description: Optional description for a newly created export case
- type: object
- required:
- - items
- title: BatchExportBody
- BatchExportItem:
- properties:
- camera:
- type: string
- title: Camera name
- start_time:
- type: number
- title: Start time
- end_time:
- type: number
- title: End time
- image_path:
- anyOf:
- - type: string
- - type: "null"
- title: Existing thumbnail path
- description: Optional existing image to use as the export thumbnail
- friendly_name:
- anyOf:
- - type: string
- maxLength: 256
- - type: "null"
- title: Friendly name
- description: Optional friendly name for this specific export item
- client_item_id:
- anyOf:
- - type: string
- maxLength: 128
- - type: "null"
- title: Client item ID
- description: Optional opaque client identifier echoed back in results
- type: object
- required:
- - camera
- - start_time
- - end_time
- title: BatchExportItem
- BatchExportResponse:
- properties:
- export_case_id:
- anyOf:
- - type: string
- - type: "null"
- title: Export Case Id
- description: Export case ID associated with the batch
- export_ids:
- items:
- type: string
- type: array
- title: Export Ids
- description: Export IDs successfully queued
- results:
- items:
- $ref: "#/components/schemas/BatchExportResultModel"
- type: array
- title: Results
- description: Per-item batch export results
- type: object
- required:
- - export_ids
- - results
- title: BatchExportResponse
- description: Response model for starting an export batch.
- BatchExportResultModel:
- properties:
- camera:
- type: string
- title: Camera
- description: Camera name for this export attempt
- export_id:
- anyOf:
- - type: string
- - type: "null"
- title: Export Id
- description: The export ID when the export was successfully queued
- success:
- type: boolean
- title: Success
- description: Whether the export was successfully queued
- status:
- anyOf:
- - type: string
- - type: "null"
- title: Status
- description: Queue status for this camera export
- error:
- anyOf:
- - type: string
- - type: "null"
- title: Error
- description: Validation or queueing error for this item, if any
- item_index:
- anyOf:
- - type: integer
- - type: "null"
- title: Item Index
- description: Zero-based index of this result within the request items list
- client_item_id:
- anyOf:
- - type: string
- - type: "null"
- title: Client Item Id
- description: Opaque client-supplied item identifier echoed from the request
- type: object
- required:
- - camera
- - success
- title: BatchExportResultModel
- description: Per-item result for a batch export request.
EventsSubLabelBody:
properties:
subLabel:
@@ -6723,14 +7857,14 @@ components:
subLabelScore:
anyOf:
- type: number
- maximum: 1
- exclusiveMinimum: 0
- - type: "null"
+ maximum: 1.0
+ exclusiveMinimum: 0.0
+ - type: 'null'
title: Score for sub label
camera:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Camera this object is detected on.
type: object
required:
@@ -6763,9 +7897,10 @@ components:
anyOf:
- type: string
maxLength: 30
- - type: "null"
+ - type: 'null'
title: Export Case Id
- description: "Case ID to assign to, or null to unassign from current case"
+ description: Case ID to assign to, or null to unassign from current
+ case
type: object
required:
- ids
@@ -6781,7 +7916,7 @@ components:
description:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Description
description: Optional description of the export case
type: object
@@ -6802,7 +7937,7 @@ components:
description:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Description
description: Optional description of the export case
created_at:
@@ -6827,18 +7962,101 @@ components:
anyOf:
- type: string
maxLength: 100
- - type: "null"
+ - type: 'null'
title: Name
description: Updated friendly name of the export case
description:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Description
description: Updated description of the export case
type: object
title: ExportCaseUpdateBody
description: Request body for updating an existing export case.
+ ExportJobModel:
+ properties:
+ id:
+ type: string
+ title: Id
+ description: Unique identifier for the export job
+ job_type:
+ type: string
+ title: Job Type
+ description: Job type
+ status:
+ type: string
+ title: Status
+ description: Current job status
+ camera:
+ type: string
+ title: Camera
+ description: Camera associated with this export job
+ name:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Name
+ description: Friendly name for the export
+ export_case_id:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Export Case Id
+ description: ID of the export case this export belongs to
+ request_start_time:
+ type: number
+ title: Request Start Time
+ description: Requested export start time
+ request_end_time:
+ type: number
+ title: Request End Time
+ description: Requested export end time
+ start_time:
+ anyOf:
+ - type: number
+ - type: 'null'
+ title: Start Time
+ description: Unix timestamp when execution started
+ end_time:
+ anyOf:
+ - type: number
+ - type: 'null'
+ title: End Time
+ description: Unix timestamp when execution completed
+ error_message:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Error Message
+ description: Error message for failed jobs
+ results:
+ anyOf:
+ - type: object
+ - type: 'null'
+ title: Results
+ description: Result metadata for completed jobs
+ current_step:
+ type: string
+ title: Current Step
+ description: Current execution step (queued, preparing, encoding,
+ encoding_retry, finalizing)
+ default: queued
+ progress_percent:
+ type: number
+ title: Progress Percent
+ description: Progress percentage of the current step (0.0 - 100.0)
+ default: 0.0
+ type: object
+ required:
+ - id
+ - job_type
+ - status
+ - camera
+ - request_start_time
+ - request_end_time
+ title: ExportJobModel
+ description: Model representing a queued or running export job.
ExportModel:
properties:
id:
@@ -6872,7 +8090,7 @@ components:
export_case_id:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Export Case Id
description: ID of the export case this export belongs to
type: object
@@ -6889,14 +8107,14 @@ components:
ExportRecordingsBody:
properties:
source:
- $ref: "#/components/schemas/PlaybackSourceEnum"
+ $ref: '#/components/schemas/PlaybackSourceEnum'
title: Playback source
default: recordings
name:
anyOf:
- type: string
maxLength: 256
- - type: "null"
+ - type: 'null'
title: Friendly name
image_path:
type: string
@@ -6905,15 +8123,22 @@ components:
anyOf:
- type: string
maxLength: 30
- - type: "null"
+ - type: 'null'
title: Export case ID
description: ID of the export case to assign this export to
+ chapters:
+ anyOf:
+ - $ref: '#/components/schemas/ChaptersEnum'
+ - type: 'null'
+ title: Chapter mode
+ description: Optional chapter metadata to embed in the export. When
+ omitted, the camera's configured export chapter mode is used.
type: object
title: ExportRecordingsBody
ExportRecordingsCustomBody:
properties:
source:
- $ref: "#/components/schemas/PlaybackSourceEnum"
+ $ref: '#/components/schemas/PlaybackSourceEnum'
title: Playback source
default: recordings
name:
@@ -6927,31 +8152,28 @@ components:
anyOf:
- type: string
maxLength: 30
- - type: "null"
+ - type: 'null'
title: Export case ID
description: ID of the export case to assign this export to
ffmpeg_input_args:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: FFmpeg input arguments
- description: >-
- Custom FFmpeg input arguments. If not provided, defaults to
- timelapse input args.
+ description: Custom FFmpeg input arguments. If not provided, defaults
+ to timelapse input args.
ffmpeg_output_args:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: FFmpeg output arguments
- description: >-
- Custom FFmpeg output arguments. If not provided, defaults to
- timelapse output args.
+ description: Custom FFmpeg output arguments. If not provided, defaults
+ to timelapse output args.
cpu_fallback:
type: boolean
title: CPU Fallback
- description: >-
- If true, retry export without hardware acceleration if the initial
- export fails.
+ description: If true, retry export without hardware acceleration if
+ the initial export fails.
default: false
type: object
title: ExportRecordingsCustomBody
@@ -6982,25 +8204,23 @@ components:
score:
anyOf:
- type: number
- - type: "null"
+ - type: 'null'
title: Score
description: Confidence score of the recognition (0-1)
face_name:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Face Name
description: The recognized face name if successful
type: object
required:
- success
title: FaceRecognitionResponse
- description: >-
+ description: |-
Response model for face recognition endpoint.
-
- Returns the result of attempting to recognize a face from an uploaded
- image.
+ Returns the result of attempting to recognize a face from an uploaded image.
FacesResponse:
additionalProperties:
items:
@@ -7020,6 +8240,36 @@ components:
"john_doe": ["face1.webp", "face2.jpg"],
"jane_smith": ["face3.png"]
}
+ GenAIProbeBody:
+ properties:
+ provider:
+ $ref: '#/components/schemas/GenAIProviderEnum'
+ api_key:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Api Key
+ base_url:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Base Url
+ provider_options:
+ type: object
+ title: Provider Options
+ type: object
+ required:
+ - provider
+ title: GenAIProbeBody
+ GenAIProviderEnum:
+ type: string
+ enum:
+ - openai
+ - azure_openai
+ - gemini
+ - ollama
+ - llamacpp
+ title: GenAIProviderEnum
GenerateObjectExamplesBody:
properties:
model_name:
@@ -7029,7 +8279,8 @@ components:
label:
type: string
title: Label
- description: "Object label to collect examples for (e.g., 'person', 'car')"
+ description: Object label to collect examples for (e.g., 'person',
+ 'car')
type: object
required:
- model_name
@@ -7053,9 +8304,8 @@ components:
minItems: 4
type: object
title: Cameras
- description: >-
- Dictionary mapping camera names to normalized crop coordinates in
- [x1, y1, x2, y2] format (values 0-1)
+ description: Dictionary mapping camera names to normalized crop
+ coordinates in [x1, y1, x2, y2] format (values 0-1)
type: object
required:
- model_name
@@ -7078,7 +8328,7 @@ components:
properties:
detail:
items:
- $ref: "#/components/schemas/ValidationError"
+ $ref: '#/components/schemas/ValidationError'
type: array
title: Detail
type: object
@@ -7109,28 +8359,26 @@ components:
dry_run:
type: boolean
title: Dry Run
- description: "If True, only report orphans without deleting them"
+ description: If True, only report orphans without deleting them
default: true
media_types:
items:
type: string
type: array
title: Media Types
- description: >-
- Types of media to sync: 'all', 'event_snapshots',
- 'event_thumbnails', 'review_thumbnails', 'previews', 'exports',
- 'recordings'
+ description: "Types of media to sync: 'all', 'event_snapshots', 'event_thumbnails',
+ 'review_thumbnails', 'previews', 'exports', 'recordings'"
default:
- all
force:
type: boolean
title: Force
- description: "If True, bypass safety threshold checks"
+ description: If True, bypass safety threshold checks
default: false
verbose:
type: boolean
title: Verbose
- description: "If True, write full orphan file list to /config/media_sync/.txt"
+ description: If True, write full orphan file list to disk
default: false
type: object
title: MediaSyncBody
@@ -7163,7 +8411,7 @@ components:
wall_time_seconds:
type: number
title: Wall Time Seconds
- default: 0
+ default: 0.0
segments_with_errors:
type: integer
title: Segments With Errors
@@ -7188,28 +8436,22 @@ components:
type: array
type: array
title: Polygon Points
- description: "List of [x, y] normalized coordinates (0-1) defining the ROI polygon"
+ description: List of [x, y] normalized coordinates (0-1) defining the
+ ROI polygon
threshold:
type: integer
- maximum: 255
- minimum: 1
+ maximum: 255.0
+ minimum: 1.0
title: Threshold
description: Pixel difference threshold (1-255)
default: 30
min_area:
type: number
- maximum: 100
+ maximum: 100.0
minimum: 0.1
title: Min Area
description: Minimum change area as a percentage of the ROI
- default: 5
- frame_skip:
- type: integer
- maximum: 30
- minimum: 1
- title: Frame Skip
- description: "Process every Nth frame (1=all frames, 5=every 5th frame)"
- default: 5
+ default: 5.0
parallel:
type: boolean
title: Parallel
@@ -7217,8 +8459,8 @@ components:
default: false
max_results:
type: integer
- maximum: 200
- minimum: 1
+ maximum: 200.0
+ minimum: 1.0
title: Max Results
description: Maximum number of search results to return
default: 25
@@ -7277,24 +8519,34 @@ components:
results:
anyOf:
- items:
- $ref: "#/components/schemas/MotionSearchResult"
+ $ref: '#/components/schemas/MotionSearchResult'
type: array
- - type: "null"
+ - type: 'null'
title: Results
total_frames_processed:
anyOf:
- type: integer
- - type: "null"
+ - type: 'null'
title: Total Frames Processed
error_message:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Error Message
metrics:
anyOf:
- - $ref: "#/components/schemas/MotionSearchMetricsResponse"
- - type: "null"
+ - $ref: '#/components/schemas/MotionSearchMetricsResponse'
+ - type: 'null'
+ scanning_timestamp:
+ anyOf:
+ - type: number
+ - type: 'null'
+ title: Scanning Timestamp
+ progress:
+ anyOf:
+ - type: number
+ - type: 'null'
+ title: Progress
type: object
required:
- success
@@ -7409,7 +8661,7 @@ components:
type: boolean
title: Has Been Reviewed
severity:
- $ref: "#/components/schemas/SeverityEnum"
+ $ref: '#/components/schemas/SeverityEnum'
thumb_path:
type: string
title: Thumb Path
@@ -7429,10 +8681,10 @@ components:
ReviewSummaryResponse:
properties:
last24Hours:
- $ref: "#/components/schemas/Last24HoursReview"
+ $ref: '#/components/schemas/Last24HoursReview'
root:
additionalProperties:
- $ref: "#/components/schemas/DayReview"
+ $ref: '#/components/schemas/DayReview'
type: object
title: Root
type: object
@@ -7459,9 +8711,15 @@ components:
export_id:
anyOf:
- type: string
- - type: "null"
+ - type: 'null'
title: Export Id
description: The export ID if successfully started
+ status:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Status
+ description: Queue status for the export job
type: object
required:
- success
@@ -7493,14 +8751,14 @@ components:
TriggerEmbeddingBody:
properties:
type:
- $ref: "#/components/schemas/TriggerType"
+ $ref: '#/components/schemas/TriggerType'
data:
type: string
title: Data
threshold:
type: number
- maximum: 1
- minimum: 0
+ maximum: 1.0
+ minimum: 0.0
title: Threshold
default: 0.5
type: object
@@ -7514,6 +8772,36 @@ components:
- thumbnail
- description
title: TriggerType
+ VLMMonitorRequest:
+ properties:
+ camera:
+ type: string
+ title: Camera
+ condition:
+ type: string
+ title: Condition
+ max_duration_minutes:
+ type: integer
+ title: Max Duration Minutes
+ default: 60
+ labels:
+ items:
+ type: string
+ type: array
+ title: Labels
+ default: []
+ zones:
+ items:
+ type: string
+ type: array
+ title: Zones
+ default: []
+ type: object
+ required:
+ - camera
+ - condition
+ title: VLMMonitorRequest
+ description: Request model for starting a VLM watch job.
ValidationError:
properties:
loc:
@@ -7535,3 +8823,19 @@ components:
- msg
- type
title: ValidationError
+ securitySchemes:
+ frigateAdminAuth:
+ type: apiKey
+ in: cookie
+ name: frigate_token
+ description: Authenticated session whose resolved role is 'admin'. The
+ session is established via the JWT cookie issued by POST /login, or via
+ proxy auth headers (remote-user / remote-role) when Frigate runs behind
+ an authenticating reverse proxy.
+ frigateUserAuth:
+ type: apiKey
+ in: cookie
+ name: frigate_token
+ description: Any authenticated session (role 'viewer' or higher),
+ established via the JWT cookie issued by POST /login, or via proxy auth
+ headers when Frigate runs behind an authenticating reverse proxy.
diff --git a/frigate/__main__.py b/frigate/__main__.py
index f3181e4946..df27b42a27 100644
--- a/frigate/__main__.py
+++ b/frigate/__main__.py
@@ -4,7 +4,6 @@ import multiprocessing as mp
import signal
import sys
import threading
-from typing import Union
import ruamel.yaml
from pydantic import ValidationError
@@ -54,7 +53,7 @@ def main() -> None:
print("*************************************************************\n")
# Attempt to get the original config file for line number tracking
config_path = find_config_file()
- with open(config_path, "r") as f:
+ with open(config_path) as f:
yaml_config = ruamel.yaml.YAML()
yaml_config.preserve_quotes = True
full_config = yaml_config.load(f)
@@ -68,7 +67,7 @@ def main() -> None:
try:
for i, part in enumerate(error_path):
- key: Union[int, str] = (
+ key: int | str = (
int(part) if isinstance(part, str) and part.isdigit() else part
)
diff --git a/frigate/api/app.py b/frigate/api/app.py
index 57d1f0a799..ac839158e2 100644
--- a/frigate/api/app.py
+++ b/frigate/api/app.py
@@ -12,7 +12,7 @@ from datetime import datetime, timedelta
from functools import reduce
from io import StringIO
from pathlib import Path as FilePath
-from typing import Any, Dict, List, Optional
+from typing import Any
import aiofiles
import ruamel.yaml
@@ -34,15 +34,18 @@ from frigate.api.auth import (
from frigate.api.defs.query.app_query_parameters import AppTimelineHourlyQueryParameters
from frigate.api.defs.request.app_body import (
AppConfigSetBody,
+ GenAIProbeBody,
MediaSyncBody,
)
from frigate.api.defs.tags import Tags
-from frigate.config import FrigateConfig
+from frigate.config import FrigateConfig, GenAIConfig, GenAIProviderEnum
from frigate.config.camera.updater import (
CameraConfigUpdateEnum,
CameraConfigUpdateTopic,
)
+from frigate.const import REDACTED_CREDENTIAL_SENTINEL
from frigate.ffmpeg_presets import FFMPEG_HWACCEL_VAAPI, _gpu_selector
+from frigate.genai import PROVIDERS, load_providers
from frigate.jobs.media_sync import (
get_current_media_sync_job,
get_media_sync_job_by_id,
@@ -59,7 +62,11 @@ from frigate.util.builtin import (
process_config_query_string,
update_yaml_file_bulk,
)
-from frigate.util.config import apply_section_update, find_config_file
+from frigate.util.config import (
+ apply_section_update,
+ find_config_file,
+ redact_credential,
+)
from frigate.util.schema import get_config_schema
from frigate.util.services import (
get_nvidia_driver_info,
@@ -75,6 +82,14 @@ logger = logging.getLogger(__name__)
router = APIRouter(tags=[Tags.app])
+# Short timeout for the /genai/probe path. The probe is interactive — fail
+# fast on hung providers rather than holding an API worker thread.
+_PROBE_TIMEOUT_SECONDS = 10
+# Outer cap that returns control to the caller even if the underlying sync
+# HTTP call ignores its timeout. The sync work continues in the background
+# thread; only the response is bounded.
+_PROBE_OUTER_TIMEOUT_SECONDS = 15
+
@router.get(
"/", response_class=PlainTextResponse, dependencies=[Depends(allow_public())]
@@ -96,11 +111,46 @@ def version():
@router.get("/stats", dependencies=[Depends(allow_any_authenticated())])
-def stats(request: Request):
- return JSONResponse(content=request.app.stats_emitter.get_latest_stats())
+def stats(
+ request: Request,
+ allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
+):
+ stats_data = request.app.stats_emitter.get_latest_stats()
+
+ # Admins see the full snapshot
+ if request.headers.get("remote-role") == "admin":
+ return JSONResponse(content=stats_data)
+
+ allowed_set = set(allowed_cameras)
+
+ # Shallow-copy so we don't mutate the cached stats history entry.
+ filtered = {**stats_data}
+
+ cameras = stats_data.get("cameras")
+ if cameras is not None:
+ filtered["cameras"] = {
+ name: data for name, data in cameras.items() if name in allowed_set
+ }
+
+ bandwidth = stats_data.get("bandwidth_usages")
+ if bandwidth is not None:
+ filtered["bandwidth_usages"] = {
+ name: data for name, data in bandwidth.items() if name in allowed_set
+ }
+
+ # cmdline can leak camera URLs/paths; strip but keep cpu/mem so
+ # client-side problem heuristics still work.
+ cpu_usages = stats_data.get("cpu_usages")
+ if cpu_usages is not None:
+ filtered["cpu_usages"] = {
+ pid: {k: v for k, v in usage.items() if k != "cmdline"}
+ for pid, usage in cpu_usages.items()
+ }
+
+ return JSONResponse(content=filtered)
-@router.get("/stats/history", dependencies=[Depends(allow_any_authenticated())])
+@router.get("/stats/history", dependencies=[Depends(require_role(["admin"]))])
def stats_history(request: Request, keys: str = None):
if keys:
keys = keys.split(",")
@@ -114,7 +164,7 @@ def metrics(request: Request):
# Retrieve the latest statistics and update the Prometheus metrics
stats = request.app.stats_emitter.get_latest_stats()
# query DB for count of events by camera, label
- event_counts: List[Dict[str, Any]] = (
+ event_counts: list[dict[str, Any]] = (
Event.select(Event.camera, Event.label, fn.Count())
.group_by(Event.camera, Event.label)
.dicts()
@@ -135,6 +185,95 @@ def genai_models(request: Request):
return JSONResponse(content=request.app.genai_manager.list_models())
+@router.post(
+ "/genai/probe",
+ dependencies=[Depends(require_role(["admin"]))],
+ summary="Probe a GenAI provider without saving config",
+ description=(
+ "Builds a transient client from the request body and returns its "
+ "available models. Used to validate provider credentials in the UI "
+ "before saving the configuration."
+ ),
+)
+async def genai_probe(body: GenAIProbeBody):
+ load_providers()
+
+ provider_cls = PROVIDERS.get(body.provider)
+ if not provider_cls:
+ return JSONResponse(
+ status_code=400,
+ content={"success": False, "message": "Unknown provider"},
+ )
+
+ # The OpenAI-compatible SDKs accept "timeout" as a constructor kwarg via
+ # provider_options; other plugins use GenAIClient.timeout passed below.
+ # Don't inject timeout for Gemini — its HttpOptions interprets the value
+ # in milliseconds and would clash with the plugin's own default.
+ probe_provider_options: dict[str, Any] = dict(body.provider_options or {})
+ if body.provider in (GenAIProviderEnum.openai, GenAIProviderEnum.azure_openai):
+ probe_provider_options.setdefault("timeout", _PROBE_TIMEOUT_SECONDS)
+
+ try:
+ transient_cfg = GenAIConfig(
+ provider=body.provider,
+ api_key=body.api_key,
+ base_url=body.base_url,
+ provider_options=probe_provider_options,
+ # model is required by the schema but irrelevant for listing.
+ model="probe",
+ roles=[],
+ )
+ except ValidationError:
+ logger.exception("GenAI probe: invalid configuration")
+ return JSONResponse(
+ status_code=400,
+ content={"success": False, "message": "Invalid provider configuration"},
+ )
+
+ try:
+ client = provider_cls(
+ transient_cfg,
+ timeout=_PROBE_TIMEOUT_SECONDS,
+ validate_model=False,
+ )
+ except Exception:
+ logger.exception("GenAI probe: failed to construct client")
+ return JSONResponse(
+ content={
+ "success": False,
+ "message": "Failed to connect to provider",
+ },
+ )
+
+ try:
+ models = await asyncio.wait_for(
+ asyncio.to_thread(client.list_models),
+ timeout=_PROBE_OUTER_TIMEOUT_SECONDS,
+ )
+ except TimeoutError:
+ return JSONResponse(
+ content={"success": False, "message": "Probe timed out"},
+ )
+ except Exception:
+ logger.exception("GenAI probe: list_models failed")
+ return JSONResponse(
+ content={"success": False, "message": "Provider returned no models"},
+ )
+
+ if not models:
+ return JSONResponse(
+ content={
+ "success": False,
+ "message": (
+ "No models returned. Check the API key, base URL, and "
+ "that the provider is reachable."
+ ),
+ },
+ )
+
+ return JSONResponse(content={"success": True, "models": models})
+
+
@router.get("/config", dependencies=[Depends(allow_any_authenticated())])
def config(request: Request):
config_obj: FrigateConfig = request.app.frigate_config
@@ -146,25 +285,28 @@ def config(request: Request):
for name, detector in config_obj.detectors.items()
}
- # remove the mqtt password
- config["mqtt"].pop("password", None)
+ # remove environment_vars for non-admin users
+ if request.headers.get("remote-role") != "admin":
+ config.pop("environment_vars", None)
- # remove the proxy secret
- config["proxy"].pop("auth_secret", None)
+ # redact mqtt credentials
+ redact_credential(config["mqtt"], "password")
- # remove genai api keys
- for genai_name, genai_cfg in config.get("genai", {}).items():
+ # redact proxy secret
+ redact_credential(config["proxy"], "auth_secret")
+
+ # redact genai api keys
+ for _genai_name, genai_cfg in config.get("genai", {}).items():
if isinstance(genai_cfg, dict):
- genai_cfg.pop("api_key", None)
+ redact_credential(genai_cfg, "api_key")
for camera_name, camera in request.app.frigate_config.cameras.items():
camera_dict = config["cameras"][camera_name]
- # remove onvif credentials
+ # redact onvif credentials
onvif_dict = camera_dict.get("onvif", {})
if onvif_dict:
- onvif_dict.pop("user", None)
- onvif_dict.pop("password", None)
+ redact_credential(onvif_dict, "password")
# clean paths
for input in camera_dict.get("ffmpeg", {}).get("inputs", []):
@@ -232,7 +374,7 @@ def config(request: Request):
if model_path:
model_json_path = FilePath(model_path).with_suffix(".json")
try:
- with open(model_json_path, "r") as f:
+ with open(model_json_path) as f:
model_plus_data = json.load(f)
config["model"]["plus"] = model_plus_data
except FileNotFoundError:
@@ -360,7 +502,7 @@ def config_raw():
status_code=404,
)
- with open(config_file, "r") as f:
+ with open(config_file) as f:
raw_config = f.read()
f.close()
@@ -494,6 +636,40 @@ def config_save(save_option: str, body: Any = Body(media_type="text/plain")):
)
+def _restore_masked_camera_paths(config_data: dict, config: FrigateConfig) -> None:
+ """Substitute incoming `*:*` masked credentials with the in-memory ones.
+
+ The /config response masks ffmpeg input credentials, so the settings UI
+ sends the masked path back when sibling fields (e.g. hwaccel_args) are
+ edited. Without this we'd write `rtsp://*:*@host` into YAML and lose
+ the real credentials. Mutates `config_data` in place.
+ """
+ cameras = config_data.get("cameras")
+ if not isinstance(cameras, dict):
+ return
+
+ for camera_name, camera_data in cameras.items():
+ if not isinstance(camera_data, dict):
+ continue
+ inputs = camera_data.get("ffmpeg", {}).get("inputs")
+ if not isinstance(inputs, list):
+ continue
+ existing = config.cameras.get(camera_name)
+ if existing is None:
+ continue
+ existing_paths = [inp.path for inp in existing.ffmpeg.inputs]
+ for index, input_obj in enumerate(inputs):
+ if not isinstance(input_obj, dict):
+ continue
+ path = input_obj.get("path")
+ if not isinstance(path, str):
+ continue
+ if ("://*:*@" in path or "user=*&password=*" in path) and index < len(
+ existing_paths
+ ):
+ input_obj["path"] = existing_paths[index]
+
+
def _config_set_in_memory(request: Request, body: AppConfigSetBody) -> JSONResponse:
"""Apply config changes in-memory only, without writing to YAML.
@@ -504,8 +680,13 @@ def _config_set_in_memory(request: Request, body: AppConfigSetBody) -> JSONRespo
try:
updates = {}
if body.config_data:
+ _restore_masked_camera_paths(body.config_data, request.app.frigate_config)
updates = flatten_config_data(body.config_data)
updates = {k: ("" if v is None else v) for k, v in updates.items()}
+ # Drop any field whose value is still the redaction sentinel
+ updates = {
+ k: v for k, v in updates.items() if v != REDACTED_CREDENTIAL_SENTINEL
+ }
if not updates:
return JSONResponse(
@@ -569,6 +750,40 @@ def _config_set_in_memory(request: Request, body: AppConfigSetBody) -> JSONRespo
settings,
)
+ # detect resize also republishes motion + objects so other
+ # processes pick up the rebuilt masks, and fires refresh so
+ # the camera maintainer recycles the camera process to pick
+ # up the new ffmpeg cmd / SHM sizing
+ if field == "detect":
+ cam_cfg = config.cameras.get(camera)
+ if cam_cfg is not None:
+ if cam_cfg.motion is not None:
+ request.app.config_publisher.publish_update(
+ CameraConfigUpdateTopic(
+ CameraConfigUpdateEnum.motion, camera
+ ),
+ cam_cfg.motion,
+ )
+ request.app.config_publisher.publish_update(
+ CameraConfigUpdateTopic(
+ CameraConfigUpdateEnum.objects, camera
+ ),
+ cam_cfg.objects,
+ )
+ if cam_cfg.zones:
+ request.app.config_publisher.publish_update(
+ CameraConfigUpdateTopic(
+ CameraConfigUpdateEnum.zones, camera
+ ),
+ cam_cfg.zones,
+ )
+ request.app.config_publisher.publish_update(
+ CameraConfigUpdateTopic(
+ CameraConfigUpdateEnum.refresh, camera
+ ),
+ cam_cfg,
+ )
+
return JSONResponse(
content={"success": True, "message": "Config applied in-memory"},
status_code=200,
@@ -592,7 +807,7 @@ def config_set(request: Request, body: AppConfigSetBody):
try:
with lock:
- with open(config_file, "r") as f:
+ with open(config_file) as f:
old_raw_config = f.read()
try:
@@ -610,9 +825,19 @@ def config_set(request: Request, body: AppConfigSetBody):
if query_string:
updates = process_config_query_string(query_string)
elif body.config_data:
+ _restore_masked_camera_paths(
+ body.config_data, request.app.frigate_config
+ )
updates = flatten_config_data(body.config_data)
# Convert None values to empty strings for deletion (e.g., when deleting masks)
updates = {k: ("" if v is None else v) for k, v in updates.items()}
+ # Drop sentinel-valued fields so untouched credential
+ # placeholders don't clobber the saved YAML value.
+ updates = {
+ k: v
+ for k, v in updates.items()
+ if v != REDACTED_CREDENTIAL_SENTINEL
+ }
if not updates:
return JSONResponse(
@@ -629,7 +854,7 @@ def config_set(request: Request, body: AppConfigSetBody):
update_yaml_file_bulk(config_file, updates)
# validate the updated config
- with open(config_file, "r") as f:
+ with open(config_file) as f:
new_raw_config = f.read()
try:
@@ -683,6 +908,11 @@ def config_set(request: Request, body: AppConfigSetBody):
status_code=500,
)
+ # drop runtime overrides for any fields the user just rewrote in
+ # yaml so a stale override doesn't silently win after restart
+ if request.app.dispatcher is not None:
+ request.app.dispatcher.clear_runtime_state_for_yaml_keys(updates.keys())
+
if body.requires_restart == 0 or body.update_topic:
old_config: FrigateConfig = request.app.frigate_config
request.app.frigate_config = config
@@ -696,6 +926,8 @@ def config_set(request: Request, body: AppConfigSetBody):
if request.app.dispatcher is not None:
request.app.dispatcher.config = config
+ for comm in request.app.dispatcher.comms:
+ comm.config = config
if body.update_topic:
if body.update_topic.startswith("config/cameras/"):
@@ -792,20 +1024,20 @@ def nvinfo():
@router.get(
"/logs/{service}",
tags=[Tags.logs],
- dependencies=[Depends(allow_any_authenticated())],
+ dependencies=[Depends(require_role(["admin"]))],
)
async def logs(
service: str = Path(enum=["frigate", "nginx", "go2rtc"]),
- download: Optional[str] = None,
- stream: Optional[bool] = False,
- start: Optional[int] = 0,
- end: Optional[int] = None,
+ download: str | None = None,
+ stream: bool | None = False,
+ start: int | None = 0,
+ end: int | None = None,
):
"""Get logs for the requested service (frigate/nginx/go2rtc)"""
def download_logs(service_location: str):
try:
- file = open(service_location, "r")
+ file = open(service_location)
contents = file.read()
file.close()
return JSONResponse(jsonable_encoder(contents))
@@ -820,7 +1052,7 @@ async def logs(
"""Asynchronously stream log lines."""
buffer = ""
try:
- async with aiofiles.open(file_path, "r") as file:
+ async with aiofiles.open(file_path) as file:
await file.seek(0, 2)
while True:
line = await file.readline()
@@ -858,7 +1090,7 @@ async def logs(
# For full logs initially
try:
- async with aiofiles.open(service_location, "r") as file:
+ async with aiofiles.open(service_location) as file:
contents = await file.read()
total_lines, log_lines = process_logs(contents, service, start, end)
@@ -997,12 +1229,27 @@ def get_media_sync_status(job_id: str):
@router.get("/labels", dependencies=[Depends(allow_any_authenticated())])
-def get_labels(camera: str = ""):
+def get_labels(
+ camera: str = "",
+ allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
+):
try:
if camera:
+ if camera not in allowed_cameras:
+ return JSONResponse(
+ content={
+ "success": False,
+ "message": f"Access denied to camera '{camera}'",
+ },
+ status_code=403,
+ )
events = Event.select(Event.label).where(Event.camera == camera).distinct()
else:
- events = Event.select(Event.label).distinct()
+ events = (
+ Event.select(Event.label)
+ .where(Event.camera << allowed_cameras)
+ .distinct()
+ )
except Exception as e:
logger.error(e)
return JSONResponse(
@@ -1015,9 +1262,16 @@ def get_labels(camera: str = ""):
@router.get("/sub_labels", dependencies=[Depends(allow_any_authenticated())])
-def get_sub_labels(split_joined: Optional[int] = None):
+def get_sub_labels(
+ split_joined: int | None = None,
+ allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
+):
try:
- events = Event.select(Event.sub_label).distinct()
+ events = (
+ Event.select(Event.sub_label)
+ .where(Event.camera << allowed_cameras)
+ .distinct()
+ )
except Exception:
return JSONResponse(
content=({"success": False, "message": "Failed to get sub_labels"}),
@@ -1097,8 +1351,8 @@ def plusModels(request: Request, filterByCurrentModelDetector: bool = False):
"/recognized_license_plates", dependencies=[Depends(allow_any_authenticated())]
)
def get_recognized_license_plates(
- split_joined: Optional[int] = None,
- allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
+ split_joined: int | None = None,
+ allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
):
try:
query = (
@@ -1139,8 +1393,8 @@ def get_recognized_license_plates(
def timeline(
camera: str = "all",
limit: int = 100,
- source_id: Optional[str] = None,
- allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
+ source_id: str | None = None,
+ allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
):
clauses = []
@@ -1154,20 +1408,20 @@ def timeline(
]
if camera != "all":
- clauses.append((Timeline.camera == camera))
+ clauses.append(Timeline.camera == camera)
if source_id:
source_ids = [sid.strip() for sid in source_id.split(",")]
if len(source_ids) == 1:
- clauses.append((Timeline.source_id == source_ids[0]))
+ clauses.append(Timeline.source_id == source_ids[0])
else:
- clauses.append((Timeline.source_id.in_(source_ids)))
+ clauses.append(Timeline.source_id.in_(source_ids))
# Enforce per-camera access control
- clauses.append((Timeline.camera << allowed_cameras))
+ clauses.append(Timeline.camera << allowed_cameras)
if len(clauses) == 0:
- clauses.append((True))
+ clauses.append(True)
timeline = (
Timeline.select(*selected_columns)
@@ -1183,7 +1437,7 @@ def timeline(
@router.get("/timeline/hourly", dependencies=[Depends(allow_any_authenticated())])
def hourly_timeline(
params: AppTimelineHourlyQueryParameters = Depends(),
- allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
+ allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
):
"""Get hourly summary for timeline."""
cameras = params.cameras
@@ -1200,23 +1454,23 @@ def hourly_timeline(
if cameras != "all":
camera_list = cameras.split(",")
- clauses.append((Timeline.camera << camera_list))
+ clauses.append(Timeline.camera << camera_list)
# Enforce per-camera access control
- clauses.append((Timeline.camera << allowed_cameras))
+ clauses.append(Timeline.camera << allowed_cameras)
if labels != "all":
label_list = labels.split(",")
- clauses.append((Timeline.data["label"] << label_list))
+ clauses.append(Timeline.data["label"] << label_list)
if before:
- clauses.append((Timeline.timestamp < before))
+ clauses.append(Timeline.timestamp < before)
if after:
- clauses.append((Timeline.timestamp > after))
+ clauses.append(Timeline.timestamp > after)
if len(clauses) == 0:
- clauses.append((True))
+ clauses.append(True)
timeline = (
Timeline.select(
diff --git a/frigate/api/auth.py b/frigate/api/auth.py
index d1c9688186..204702e3ac 100644
--- a/frigate/api/auth.py
+++ b/frigate/api/auth.py
@@ -11,7 +11,7 @@ import secrets
import time
from datetime import datetime
from pathlib import Path
-from typing import List, Optional
+from urllib.parse import parse_qs, urlparse
from fastapi import APIRouter, Depends, HTTPException, Request, Response
from fastapi.responses import JSONResponse, RedirectResponse
@@ -26,6 +26,11 @@ from frigate.api.defs.request.app_body import (
AppPutRoleBody,
)
from frigate.api.defs.tags import Tags
+from frigate.api.media_auth import (
+ check_camera_access,
+ deny_response_for_media_uri,
+ is_role_restricted,
+)
from frigate.config import AuthConfig, NetworkingConfig, ProxyConfig
from frigate.const import CONFIG_DIR, JWT_SECRET_ENV_VAR, PASSWORD_HASH_ALGORITHM
from frigate.models import User
@@ -248,7 +253,14 @@ rateLimiter = RateLimiter()
def get_remote_addr(request: Request):
- route = list(reversed(request.headers.get("x-forwarded-for").split(",")))
+ # fall back to the direct TCP peer when no proxy chain is present
+ direct_addr = request.client.host if request.client else None
+
+ forwarded_for = request.headers.get("x-forwarded-for")
+ if not forwarded_for:
+ return direct_addr or "127.0.0.1"
+
+ route = list(reversed(forwarded_for.split(",")))
logger.debug(f"IP Route: {[r for r in route]}")
trusted_proxies = []
for proxy in request.app.frigate_config.auth.trusted_proxies:
@@ -285,13 +297,8 @@ def get_remote_addr(request: Request):
logger.debug(f"First untrusted IP: {str(ip)}")
return str(ip)
- # if there wasn't anything in the route, just return the default
- remote_addr = None
-
- if hasattr(request, "remote_addr"):
- remote_addr = request.remote_addr
-
- return remote_addr or "127.0.0.1"
+ # every hop in the route was trusted, so fall back to the direct peer
+ return direct_addr or "127.0.0.1"
def _cleanup_first_load_seen() -> None:
@@ -382,7 +389,7 @@ def verify_password(password, password_hash):
return secrets.compare_digest(password_hash, compare_hash)
-def validate_password_strength(password: str) -> tuple[bool, Optional[str]]:
+def validate_password_strength(password: str) -> tuple[bool, str | None]:
"""
Validate password strength.
@@ -408,13 +415,19 @@ def create_encoded_jwt(user, role, expiration, secret):
)
-def set_jwt_cookie(response: Response, cookie_name, encoded_jwt, expiration, secure):
+def set_jwt_cookie(response: Response, cookie_name, encoded_jwt, max_age, secure):
# TODO: ideally this would set secure as well, but that requires TLS
+ # SameSite is intentionally left unset (browsers default to Lax). Setting
+ # SameSite=Lax/Strict would stop the cookie from being sent in cross-origin
+ # iframes, breaking embedded views such as the Home Assistant Frigate card.
+ # CSRF is instead mitigated by requiring a custom X-CSRF-TOKEN header, which
+ # cross-origin pages cannot set without a CORS preflight that Frigate never
+ # grants (see check_csrf in api/fastapi_app.py).
response.set_cookie(
key=cookie_name,
value=encoded_jwt,
httponly=True,
- expires=expiration,
+ max_age=max_age,
secure=secure,
)
@@ -431,7 +444,7 @@ async def get_current_user(request: Request):
return {"username": username, "role": role}
-def require_role(required_roles: List[str]):
+def require_role(required_roles: list[str]):
async def role_checker(request: Request):
proxy_config: ProxyConfig = request.app.frigate_config.proxy
config_roles = list(request.app.frigate_config.auth.roles.keys())
@@ -633,6 +646,9 @@ def auth(request: Request):
logger.debug("X-Proxy-Secret header does not match configured secret value")
return fail_response
+ original_url = request.headers.get("x-original-url")
+ frigate_config = request.app.frigate_config
+
# if auth is disabled, just apply the proxy header map and return success
if not auth_config.enabled:
# pass the user header value from the upstream proxy if a mapping is specified
@@ -649,6 +665,15 @@ def auth(request: Request):
role = resolve_role(request.headers, proxy_config, config_roles_set)
success_response.headers["remote-role"] = role
+
+ deny_status = deny_response_for_media_uri(original_url, role, frigate_config)
+ if deny_status is not None:
+ return Response("", status_code=deny_status)
+
+ deny_status = deny_response_for_go2rtc_stream(original_url, role, request)
+ if deny_status is not None:
+ return Response("", status_code=deny_status)
+
return success_response
# now apply authentication
@@ -737,12 +762,21 @@ def auth(request: Request):
success_response,
JWT_COOKIE_NAME,
new_encoded_jwt,
- new_expiration,
+ JWT_SESSION_LENGTH,
JWT_COOKIE_SECURE,
)
success_response.headers["remote-user"] = user
success_response.headers["remote-role"] = role
+
+ deny_status = deny_response_for_media_uri(original_url, role, frigate_config)
+ if deny_status is not None:
+ return Response("", status_code=deny_status)
+
+ deny_status = deny_response_for_go2rtc_stream(original_url, role, request)
+ if deny_status is not None:
+ return Response("", status_code=deny_status)
+
return success_response
except Exception as e:
logger.error(f"Error parsing jwt: {e}")
@@ -812,6 +846,11 @@ limiter = Limiter(key_func=get_remote_addr)
)
@limiter.limit(limit_value=rateLimiter.get_limit)
def login(request: Request, body: AppPostLoginBody):
+ if not request.app.frigate_config.auth.enabled:
+ return JSONResponse(
+ content={"message": "Authentication is disabled"}, status_code=404
+ )
+
JWT_COOKIE_NAME = request.app.frigate_config.auth.cookie_name
JWT_COOKIE_SECURE = request.app.frigate_config.auth.cookie_secure
JWT_SESSION_LENGTH = request.app.frigate_config.auth.session_length
@@ -836,7 +875,11 @@ def login(request: Request, body: AppPostLoginBody):
encoded_jwt = create_encoded_jwt(user, role, expiration, request.app.jwt_token)
response = Response("", 200)
set_jwt_cookie(
- response, JWT_COOKIE_NAME, encoded_jwt, expiration, JWT_COOKIE_SECURE
+ response,
+ JWT_COOKIE_NAME,
+ encoded_jwt,
+ JWT_SESSION_LENGTH,
+ JWT_COOKIE_SECURE,
)
# Clear admin_first_time_login flag after successful admin login so the
# UI stops showing the first-time login documentation link.
@@ -998,7 +1041,11 @@ async def update_password(
)
# Set new JWT cookie on response
set_jwt_cookie(
- response, JWT_COOKIE_NAME, encoded_jwt, expiration, JWT_COOKIE_SECURE
+ response,
+ JWT_COOKIE_NAME,
+ encoded_jwt,
+ JWT_SESSION_LENGTH,
+ JWT_COOKIE_SECURE,
)
return response
@@ -1043,7 +1090,7 @@ async def update_role(
async def require_camera_access(
- camera_name: Optional[str] = None,
+ camera_name: str | None = None,
request: Request = None,
):
"""Dependency to enforce camera access based on user role."""
@@ -1064,19 +1111,19 @@ async def require_camera_access(
raise HTTPException(status_code=current_user.status_code, detail=detail)
role = current_user["role"]
- all_camera_names = set(request.app.frigate_config.cameras.keys())
- roles_dict = request.app.frigate_config.auth.roles
- allowed_cameras = User.get_allowed_cameras(role, roles_dict, all_camera_names)
+ frigate_config = request.app.frigate_config
- # Admin or full access bypasses
- if role == "admin" or not roles_dict.get(role):
+ if check_camera_access(role, camera_name, frigate_config):
return
- if camera_name not in allowed_cameras:
- raise HTTPException(
- status_code=403,
- detail=f"Access denied to camera '{camera_name}'. Allowed: {allowed_cameras}",
- )
+ all_camera_names = set(frigate_config.cameras.keys())
+ allowed_cameras = User.get_allowed_cameras(
+ role, frigate_config.auth.roles, all_camera_names
+ )
+ raise HTTPException(
+ status_code=403,
+ detail=f"Access denied to camera '{camera_name}'. Allowed: {allowed_cameras}",
+ )
def _get_stream_owner_cameras(request: Request, stream_name: str) -> set[str]:
@@ -1093,8 +1140,68 @@ def _get_stream_owner_cameras(request: Request, stream_name: str) -> set[str]:
return owner_cameras
+# nginx proxies these paths straight to go2rtc with authentication-only checks
+# (see auth_request.conf). Each names the desired stream via the `src` query
+# param, so the camera-level check must happen here in the `/auth` subrequest —
+# `require_go2rtc_stream_access` only guards the REST `/go2rtc/streams/{name}`
+# endpoint, not these proxied live-stream paths.
+GO2RTC_STREAM_PROXY_PATHS = frozenset(
+ {
+ "/live/mse/api/ws",
+ "/live/webrtc/api/ws",
+ "/api/go2rtc/webrtc",
+ }
+)
+
+
+def deny_response_for_go2rtc_stream(
+ original_url: str | None, role: str | None, request: Request
+) -> int | None:
+ """Block role-restricted users from go2rtc live streams they cannot access.
+
+ Returns 403 when any `src` stream named in `original_url` resolves to a
+ camera outside the role's allow-list (or when no `src` is provided on a
+ stream-proxy path), otherwise None. Mirrors the resolution logic in
+ `require_go2rtc_stream_access` so substream names map to their owning
+ camera correctly.
+ """
+ if not original_url:
+ return None
+
+ parsed = urlparse(original_url)
+ if parsed.path not in GO2RTC_STREAM_PROXY_PATHS:
+ return None
+
+ frigate_config = request.app.frigate_config
+
+ # admin and full-access roles (no allow-list) bypass the camera check
+ if not role or not is_role_restricted(role, frigate_config):
+ return None
+
+ sources = parse_qs(parsed.query).get("src", [])
+ if not sources:
+ # a stream-proxy request naming no stream has nothing legitimate to
+ # show a restricted user
+ return 403
+
+ allowed_cameras = set(
+ User.get_allowed_cameras(
+ role,
+ frigate_config.auth.roles,
+ set(frigate_config.cameras.keys()),
+ )
+ )
+
+ # deny if any requested source resolves outside the allow-list
+ for src in sources:
+ if not (_get_stream_owner_cameras(request, src) & allowed_cameras):
+ return 403
+
+ return None
+
+
async def require_go2rtc_stream_access(
- stream_name: Optional[str] = None,
+ stream_name: str | None = None,
request: Request = None,
):
"""Dependency to enforce go2rtc stream access based on owning camera access."""
diff --git a/frigate/api/camera.py b/frigate/api/camera.py
index 7a3b19439e..a86c35883f 100644
--- a/frigate/api/camera.py
+++ b/frigate/api/camera.py
@@ -19,7 +19,9 @@ from zeep.exceptions import Fault, TransportError
from zeep.transports import AsyncTransport
from frigate.api.auth import (
+ _get_stream_owner_cameras,
allow_any_authenticated,
+ get_current_user,
require_go2rtc_stream_access,
require_role,
)
@@ -31,11 +33,16 @@ from frigate.config.camera.updater import (
CameraConfigUpdateTopic,
)
from frigate.config.env import substitute_frigate_vars
-from frigate.util.builtin import clean_camera_user_pass
+from frigate.models import User
+from frigate.util.builtin import clean_camera_user_pass, get_record_segment_time
from frigate.util.camera_cleanup import cleanup_camera_db, cleanup_camera_files
from frigate.util.config import find_config_file
from frigate.util.image import run_ffmpeg_snapshot
-from frigate.util.services import ffprobe_stream
+from frigate.util.services import (
+ analyze_record_keyframes,
+ ffprobe_stream,
+ is_restricted_go2rtc_source,
+)
logger = logging.getLogger(__name__)
@@ -66,8 +73,8 @@ def _is_valid_host(host: str) -> bool:
@router.get("/go2rtc/streams", dependencies=[Depends(allow_any_authenticated())])
-def go2rtc_streams():
- r = requests.get("http://127.0.0.1:1984/api/streams")
+async def go2rtc_streams(request: Request):
+ r = await asyncio.to_thread(requests.get, "http://127.0.0.1:1984/api/streams")
if not r.ok:
logger.error("Failed to fetch streams from go2rtc")
return JSONResponse(
@@ -75,6 +82,24 @@ def go2rtc_streams():
status_code=500,
)
stream_data = r.json()
+
+ # Roles with an explicit camera list see only streams owned by an allowed
+ # camera. Admin and full-access roles (no list / empty list) see all streams.
+ current_user = await get_current_user(request)
+ if not isinstance(current_user, JSONResponse):
+ role = current_user["role"]
+ roles_dict = request.app.frigate_config.auth.roles
+ if role != "admin" and roles_dict.get(role):
+ all_camera_names = set(request.app.frigate_config.cameras.keys())
+ allowed_cameras = set(
+ User.get_allowed_cameras(role, roles_dict, all_camera_names)
+ )
+ stream_data = {
+ name: data
+ for name, data in stream_data.items()
+ if _get_stream_owner_cameras(request, name) & allowed_cameras
+ }
+
for data in stream_data.values():
for producer in data.get("producers") or []:
producer["url"] = clean_camera_user_pass(producer.get("url", ""))
@@ -122,13 +147,41 @@ def go2rtc_camera_stream(request: Request, stream_name: str):
)
def go2rtc_add_stream(request: Request, stream_name: str, src: str = ""):
"""Add or update a go2rtc stream configuration."""
+ if src and is_restricted_go2rtc_source(src):
+ logger.warning(
+ "Rejected go2rtc stream '%s' with restricted source type (echo/expr/exec)",
+ stream_name,
+ )
+ return JSONResponse(
+ content={
+ "success": False,
+ "message": "Restricted stream source type",
+ },
+ status_code=400,
+ )
+
try:
params = {"name": stream_name}
if src:
try:
- params["src"] = substitute_frigate_vars(src)
+ resolved_src = substitute_frigate_vars(src)
except KeyError:
- params["src"] = src
+ resolved_src = src
+
+ if is_restricted_go2rtc_source(resolved_src):
+ logger.warning(
+ "Rejected go2rtc stream '%s' with restricted source type (echo/expr/exec)",
+ stream_name,
+ )
+ return JSONResponse(
+ content={
+ "success": False,
+ "message": "Restricted stream source type",
+ },
+ status_code=400,
+ )
+
+ params["src"] = resolved_src
r = requests.put(
"http://127.0.0.1:1984/api/streams",
@@ -326,6 +379,48 @@ def ffprobe(request: Request, paths: str = "", detailed: bool = False):
return JSONResponse(content=output)
+@router.get("/keyframe_analysis", dependencies=[Depends(require_role(["admin"]))])
+async def keyframe_analysis(request: Request, camera: str = ""):
+ """Probe a camera's record stream and classify its keyframe spacing.
+
+ Detects smart/+ codecs and long/variable GOPs that degrade recording.
+ """
+ config: FrigateConfig = request.app.frigate_config
+
+ if camera not in config.cameras:
+ return JSONResponse(
+ content={"success": False, "message": f"{camera} is not a valid camera."},
+ status_code=404,
+ )
+
+ camera_config = config.cameras[camera]
+
+ if not camera_config.enabled:
+ return JSONResponse(
+ content={"success": False, "message": f"{camera} is not enabled."},
+ status_code=404,
+ )
+
+ # keyframe spacing only matters when this camera is recording
+ if not camera_config.record.enabled:
+ return JSONResponse(content={"severity": "record_disabled"})
+
+ # recording guarantees an input carries the record role; its index matches
+ # the "Stream N" numbering the ffprobe endpoint surfaces (same input order)
+ record_index, record_input = next(
+ (idx, i)
+ for idx, i in enumerate(camera_config.ffmpeg.inputs)
+ if "record" in i.roles
+ )
+
+ segment_time = get_record_segment_time(camera_config)
+ result = await analyze_record_keyframes(
+ config.ffmpeg, record_input.path, segment_time
+ )
+ result["stream_index"] = record_index
+ return JSONResponse(content=result)
+
+
@router.get("/ffprobe/snapshot", dependencies=[Depends(require_role(["admin"]))])
def ffprobe_snapshot(request: Request, url: str = "", timeout: int = 10):
"""Get a snapshot from a stream URL using ffmpeg."""
@@ -493,6 +588,68 @@ def _extract_fps(r_frame_rate: str) -> float | None:
return None
+def _build_digest_transport(username: str, password: str) -> AsyncTransport:
+ """Build a zeep transport backed by an httpx client using HTTP digest auth."""
+ auth = httpx.DigestAuth(username, password)
+ client = httpx.AsyncClient(auth=auth, timeout=10.0)
+ return AsyncTransport(client=client)
+
+
+async def _connect_onvif_camera(
+ host: str,
+ port: int,
+ username: str,
+ password: str,
+ wsdl_base: str | None,
+ auth_type: str,
+) -> ONVIFCamera:
+ """Connect to an ONVIF device, trying both WS-Security password encodings.
+
+ Cameras disagree on whether the WS-Security UsernameToken should carry a
+ hashed PasswordDigest or a plaintext PasswordText. The wizard can't know
+ which a given camera expects, so we try PasswordDigest first (the common
+ case) and fall back to PasswordText when the device rejects the token. This
+ is independent of auth_type, which controls HTTP transport-level auth.
+ """
+ first_error: Fault | None = None
+
+ # encrypt=True -> PasswordDigest, encrypt=False -> PasswordText
+ for encrypt in (True, False):
+ onvif_camera = ONVIFCamera(
+ host,
+ port,
+ username or "",
+ password or "",
+ wsdl_dir=wsdl_base,
+ encrypt=encrypt,
+ )
+
+ try:
+ await onvif_camera.update_xaddrs()
+ except Fault as e:
+ # A SOAP fault here is how a camera signals the wrong password
+ # encoding, so retry with the other encoding before giving up.
+ logger.debug(
+ "ONVIF connect with %s rejected, trying alternate encoding",
+ "PasswordDigest" if encrypt else "PasswordText",
+ )
+ if first_error is None:
+ first_error = e
+ continue
+
+ if auth_type == "digest" and username and password:
+ transport = _build_digest_transport(username, password)
+ for service in ("devicemgmt", "media", "ptz"):
+ if hasattr(onvif_camera, service):
+ getattr(onvif_camera, service).zeep_client.transport = transport
+ logger.debug("Configured digest authentication")
+
+ return onvif_camera
+
+ # Both encodings failed authentication; surface the original fault.
+ raise first_error
+
+
@router.get(
"/onvif/probe",
dependencies=[Depends(require_role(["admin"]))],
@@ -569,34 +726,10 @@ async def onvif_probe(
except Exception:
wsdl_base = None
- onvif_camera = ONVIFCamera(
- host, port, username or "", password or "", wsdl_dir=wsdl_base
+ onvif_camera = await _connect_onvif_camera(
+ host, port, username, password, wsdl_base, auth_type
)
- # Configure digest authentication if requested
- if auth_type == "digest" and username and password:
- # Create httpx client with digest auth
- auth = httpx.DigestAuth(username, password)
- client = httpx.AsyncClient(auth=auth, timeout=10.0)
-
- # Replace the transport in the zeep client
- transport = AsyncTransport(client=client)
-
- # Update the xaddr before setting transport
- await onvif_camera.update_xaddrs()
-
- # Replace transport in all services
- if hasattr(onvif_camera, "devicemgmt"):
- onvif_camera.devicemgmt.zeep_client.transport = transport
- if hasattr(onvif_camera, "media"):
- onvif_camera.media.zeep_client.transport = transport
- if hasattr(onvif_camera, "ptz"):
- onvif_camera.ptz.zeep_client.transport = transport
-
- logger.debug("Configured digest authentication")
- else:
- await onvif_camera.update_xaddrs()
-
# Get device information
device_info = {
"manufacturer": "Unknown",
@@ -608,10 +741,9 @@ async def onvif_probe(
# Update transport for device service if digest auth
if auth_type == "digest" and username and password:
- auth = httpx.DigestAuth(username, password)
- client = httpx.AsyncClient(auth=auth, timeout=10.0)
- transport = AsyncTransport(client=client)
- device_service.zeep_client.transport = transport
+ device_service.zeep_client.transport = _build_digest_transport(
+ username, password
+ )
device_info_resp = await device_service.GetDeviceInformation()
manufacturer = getattr(device_info_resp, "Manufacturer", None) or (
@@ -649,10 +781,9 @@ async def onvif_probe(
# Update transport for media service if digest auth
if auth_type == "digest" and username and password:
- auth = httpx.DigestAuth(username, password)
- client = httpx.AsyncClient(auth=auth, timeout=10.0)
- transport = AsyncTransport(client=client)
- media_service.zeep_client.transport = transport
+ media_service.zeep_client.transport = _build_digest_transport(
+ username, password
+ )
profiles = await media_service.GetProfiles()
profiles_count = len(profiles) if profiles else 0
@@ -684,10 +815,9 @@ async def onvif_probe(
# Update transport for PTZ service if digest auth
if auth_type == "digest" and username and password:
- auth = httpx.DigestAuth(username, password)
- client = httpx.AsyncClient(auth=auth, timeout=10.0)
- transport = AsyncTransport(client=client)
- ptz_service.zeep_client.transport = transport
+ ptz_service.zeep_client.transport = _build_digest_transport(
+ username, password
+ )
# Check if PTZ service is available
try:
@@ -840,10 +970,9 @@ async def onvif_probe(
# Update transport for media service if digest auth
if auth_type == "digest" and username and password:
- auth = httpx.DigestAuth(username, password)
- client = httpx.AsyncClient(auth=auth, timeout=10.0)
- transport = AsyncTransport(client=client)
- media_service.zeep_client.transport = transport
+ media_service.zeep_client.transport = _build_digest_transport(
+ username, password
+ )
if profiles_count and media_service:
for p in profiles or []:
@@ -966,7 +1095,6 @@ async def onvif_probe(
probe = ffprobe_stream(
request.app.frigate_config.ffmpeg, test_uri, detailed=False
)
- print(probe)
ok = probe is not None and getattr(probe, "returncode", 1) == 0
tested_candidates.append(
{
@@ -1059,14 +1187,14 @@ async def delete_camera(
try:
with lock:
- with open(config_file, "r") as f:
+ with open(config_file) as f:
old_raw_config = f.read()
try:
yaml = YAML()
yaml.indent(mapping=2, sequence=4, offset=2)
- with open(config_file, "r") as f:
+ with open(config_file) as f:
data = yaml.load(f)
# Remove camera from config
@@ -1095,7 +1223,7 @@ async def delete_camera(
with open(config_file, "w") as f:
yaml.dump(data, f)
- with open(config_file, "r") as f:
+ with open(config_file) as f:
new_raw_config = f.read()
try:
@@ -1157,7 +1285,8 @@ async def delete_camera(
# Best-effort go2rtc stream removal
try:
- requests.delete(
+ await asyncio.to_thread(
+ requests.delete,
"http://127.0.0.1:1984/api/streams",
params={"src": camera_name},
timeout=5,
diff --git a/frigate/api/chat.py b/frigate/api/chat.py
index 0543d5f8a6..fa4510cf14 100644
--- a/frigate/api/chat.py
+++ b/frigate/api/chat.py
@@ -7,10 +7,10 @@ import operator
import time
from datetime import datetime
from functools import reduce
-from typing import Any, Dict, List, Optional
+from typing import Any, Literal
import cv2
-from fastapi import APIRouter, Body, Depends, Request
+from fastapi import APIRouter, Body, Depends, HTTPException, Request
from fastapi.responses import JSONResponse, StreamingResponse
from pydantic import BaseModel
@@ -35,7 +35,14 @@ from frigate.api.defs.response.chat_response import (
ToolCall,
)
from frigate.api.defs.tags import Tags
-from frigate.api.event import events
+from frigate.api.event import _build_attribute_filter_clause, events
+from frigate.config import FrigateConfig
+from frigate.config.classification import SemanticSearchModelEnum
+from frigate.genai.prompts import (
+ build_chat_system_prompt,
+ get_attribute_classifications,
+ get_tool_definitions,
+)
from frigate.genai.utils import build_assistant_message_for_conversation
from frigate.jobs.vlm_watch import (
get_vlm_watch_job,
@@ -53,7 +60,7 @@ class ToolExecuteRequest(BaseModel):
"""Request model for tool execution."""
tool_name: str
- arguments: Dict[str, Any]
+ arguments: dict[str, Any]
class VLMMonitorRequest(BaseModel):
@@ -62,331 +69,8 @@ class VLMMonitorRequest(BaseModel):
camera: str
condition: str
max_duration_minutes: int = 60
- labels: List[str] = []
- zones: List[str] = []
-
-
-def get_tool_definitions() -> List[Dict[str, Any]]:
- """
- Get OpenAI-compatible tool definitions for Frigate.
-
- Returns a list of tool definitions that can be used with OpenAI-compatible
- function calling APIs.
- """
- return [
- {
- "type": "function",
- "function": {
- "name": "search_objects",
- "description": (
- "Search the historical record of detected objects in Frigate. "
- "Use this ONLY for questions about the PAST — e.g. 'did anyone come by today?', "
- "'when was the last car?', 'show me detections from yesterday'. "
- "Do NOT use this for monitoring or alerting requests about future events — "
- "use start_camera_watch instead for those. "
- "An 'object' in Frigate represents a tracked detection (e.g., a person, package, car). "
- "When the user asks about a specific name (person, delivery company, animal, etc.), "
- "filter by sub_label only and do not set label."
- ),
- "parameters": {
- "type": "object",
- "properties": {
- "camera": {
- "type": "string",
- "description": "Camera name to filter by (optional).",
- },
- "label": {
- "type": "string",
- "description": "Object label to filter by (e.g., 'person', 'package', 'car').",
- },
- "sub_label": {
- "type": "string",
- "description": "Name of a person, delivery company, animal, etc. When filtering by a specific name, use only sub_label; do not set label.",
- },
- "after": {
- "type": "string",
- "description": "Start time in ISO 8601 format (e.g., '2024-01-01T00:00:00Z').",
- },
- "before": {
- "type": "string",
- "description": "End time in ISO 8601 format (e.g., '2024-01-01T23:59:59Z').",
- },
- "zones": {
- "type": "array",
- "items": {"type": "string"},
- "description": "List of zone names to filter by.",
- },
- "limit": {
- "type": "integer",
- "description": "Maximum number of objects to return (default: 25).",
- "default": 25,
- },
- },
- },
- "required": [],
- },
- },
- {
- "type": "function",
- "function": {
- "name": "find_similar_objects",
- "description": (
- "Find tracked objects that are visually and semantically similar "
- "to a specific past event. Use this when the user references a "
- "particular object they have seen and wants to find other "
- "sightings of the same or similar one ('that green car', 'the "
- "person in the red jacket', 'the package that was delivered'). "
- "Prefer this over search_objects whenever the user's intent is "
- "'find more like this specific one.' Use search_objects first "
- "only if you need to locate the anchor event. Requires semantic "
- "search to be enabled."
- ),
- "parameters": {
- "type": "object",
- "properties": {
- "event_id": {
- "type": "string",
- "description": "The id of the anchor event to find similar objects to.",
- },
- "after": {
- "type": "string",
- "description": "Start time in ISO 8601 format (e.g., '2024-01-01T00:00:00Z').",
- },
- "before": {
- "type": "string",
- "description": "End time in ISO 8601 format (e.g., '2024-01-01T23:59:59Z').",
- },
- "cameras": {
- "type": "array",
- "items": {"type": "string"},
- "description": "Optional list of cameras to restrict to. Defaults to all.",
- },
- "labels": {
- "type": "array",
- "items": {"type": "string"},
- "description": "Optional list of labels to restrict to. Defaults to the anchor event's label.",
- },
- "sub_labels": {
- "type": "array",
- "items": {"type": "string"},
- "description": "Optional list of sub_labels (names) to restrict to.",
- },
- "zones": {
- "type": "array",
- "items": {"type": "string"},
- "description": "Optional list of zones. An event matches if any of its zones overlap.",
- },
- "similarity_mode": {
- "type": "string",
- "enum": ["visual", "semantic", "fused"],
- "description": "Which similarity signal(s) to use. 'fused' (default) combines visual and semantic.",
- "default": "fused",
- },
- "min_score": {
- "type": "number",
- "description": "Drop matches with a similarity score below this threshold (0.0-1.0).",
- },
- "limit": {
- "type": "integer",
- "description": "Maximum number of matches to return (default: 10).",
- "default": 10,
- },
- },
- "required": ["event_id"],
- },
- },
- },
- {
- "type": "function",
- "function": {
- "name": "set_camera_state",
- "description": (
- "Change a camera's feature state (e.g., turn detection on/off, enable/disable recordings). "
- "Use camera='*' to apply to all cameras at once. "
- "Only call this tool when the user explicitly asks to change a camera setting. "
- "Requires admin privileges."
- ),
- "parameters": {
- "type": "object",
- "properties": {
- "camera": {
- "type": "string",
- "description": "Camera name to target, or '*' to target all cameras.",
- },
- "feature": {
- "type": "string",
- "enum": [
- "detect",
- "record",
- "snapshots",
- "audio",
- "motion",
- "enabled",
- "birdseye",
- "birdseye_mode",
- "improve_contrast",
- "ptz_autotracker",
- "motion_contour_area",
- "motion_threshold",
- "notifications",
- "audio_transcription",
- "review_alerts",
- "review_detections",
- "object_descriptions",
- "review_descriptions",
- "profile",
- ],
- "description": (
- "The feature to change. Most features accept ON or OFF. "
- "birdseye_mode accepts CONTINUOUS, MOTION, or OBJECTS. "
- "motion_contour_area and motion_threshold accept a number. "
- "profile accepts a profile name or 'none' to deactivate (requires camera='*')."
- ),
- },
- "value": {
- "type": "string",
- "description": "The value to set. ON or OFF for toggles, a number for thresholds, a profile name or 'none' for profile.",
- },
- },
- "required": ["camera", "feature", "value"],
- },
- },
- },
- {
- "type": "function",
- "function": {
- "name": "get_live_context",
- "description": (
- "Get the current live image and detection information for a camera: objects being tracked, "
- "zones, timestamps. Use this to understand what is visible in the live view. "
- "Call this when answering questions about what is happening right now on a specific camera."
- ),
- "parameters": {
- "type": "object",
- "properties": {
- "camera": {
- "type": "string",
- "description": "Camera name to get live context for.",
- },
- },
- "required": ["camera"],
- },
- },
- },
- {
- "type": "function",
- "function": {
- "name": "start_camera_watch",
- "description": (
- "Start a continuous VLM watch job that monitors a camera and sends a notification "
- "when a specified condition is met. Use this when the user wants to be alerted about "
- "a future event, e.g. 'tell me when guests arrive' or 'notify me when the package is picked up'. "
- "Only one watch job can run at a time. Returns a job ID."
- ),
- "parameters": {
- "type": "object",
- "properties": {
- "camera": {
- "type": "string",
- "description": "Camera ID to monitor.",
- },
- "condition": {
- "type": "string",
- "description": (
- "Natural-language description of the condition to watch for, "
- "e.g. 'a person arrives at the front door'."
- ),
- },
- "max_duration_minutes": {
- "type": "integer",
- "description": "Maximum time to watch before giving up (minutes, default 60).",
- "default": 60,
- },
- "labels": {
- "type": "array",
- "items": {"type": "string"},
- "description": "Object labels that should trigger a VLM check (e.g. ['person', 'car']). If omitted, any detection on the camera triggers a check.",
- },
- "zones": {
- "type": "array",
- "items": {"type": "string"},
- "description": "Zone names to filter by. If specified, only detections in these zones trigger a VLM check.",
- },
- },
- "required": ["camera", "condition"],
- },
- },
- },
- {
- "type": "function",
- "function": {
- "name": "stop_camera_watch",
- "description": (
- "Cancel the currently running VLM watch job. Use this when the user wants to "
- "stop a previously started watch, e.g. 'stop watching the front door'."
- ),
- "parameters": {
- "type": "object",
- "properties": {},
- "required": [],
- },
- },
- },
- {
- "type": "function",
- "function": {
- "name": "get_profile_status",
- "description": (
- "Get the current profile status including the active profile and "
- "timestamps of when each profile was last activated. Use this to "
- "determine time periods for recap requests — e.g. when the user asks "
- "'what happened while I was away?', call this first to find the relevant "
- "time window based on profile activation history."
- ),
- "parameters": {
- "type": "object",
- "properties": {},
- "required": [],
- },
- },
- },
- {
- "type": "function",
- "function": {
- "name": "get_recap",
- "description": (
- "Get a recap of all activity (alerts and detections) for a given time period. "
- "Use this after calling get_profile_status to retrieve what happened during "
- "a specific window — e.g. 'what happened while I was away?'. Returns a "
- "chronological list of activity with camera, objects, zones, and GenAI-generated "
- "descriptions when available. Summarize the results for the user."
- ),
- "parameters": {
- "type": "object",
- "properties": {
- "after": {
- "type": "string",
- "description": "Start of the time period in ISO 8601 format (e.g. '2025-03-15T08:00:00').",
- },
- "before": {
- "type": "string",
- "description": "End of the time period in ISO 8601 format (e.g. '2025-03-15T17:00:00').",
- },
- "cameras": {
- "type": "string",
- "description": "Comma-separated camera IDs to include, or 'all' for all cameras. Default is 'all'.",
- },
- "severity": {
- "type": "string",
- "enum": ["alert", "detection"],
- "description": "Filter by severity level. Omit to include both alerts and detections.",
- },
- },
- "required": ["after", "before"],
- },
- },
- },
- ]
+ labels: list[str] = []
+ zones: list[str] = []
@router.get(
@@ -395,22 +79,89 @@ def get_tool_definitions() -> List[Dict[str, Any]]:
summary="Get available tools",
description="Returns OpenAI-compatible tool definitions for function calling.",
)
-def get_tools() -> JSONResponse:
+def get_tools(request: Request) -> JSONResponse:
"""Get list of available tools for LLM function calling."""
- tools = get_tool_definitions()
+ config = request.app.frigate_config
+ semantic_search_enabled = bool(getattr(config.semantic_search, "enabled", False))
+ attribute_classifications = get_attribute_classifications(config)
+ tools = get_tool_definitions(
+ semantic_search_enabled=semantic_search_enabled,
+ attribute_classifications=attribute_classifications,
+ embeddings_language=_embeddings_language(config),
+ )
return JSONResponse(content={"tools": tools})
+def _embeddings_language(config: FrigateConfig) -> Literal["english", "multi"]:
+ """Return the language capability of the configured embeddings model.
+
+ JinaV1 is English-only; every other option (JinaV2 or a GenAI embeddings
+ provider) handles multiple languages.
+ """
+ if config.semantic_search.model == SemanticSearchModelEnum.jinav1:
+ return "english"
+
+ return "multi"
+
+
+def _resolve_zones(
+ zones: list[str],
+ config: FrigateConfig,
+ target_cameras: list[str],
+) -> list[str]:
+ """Map zone names to their canonical config keys, case-insensitively.
+
+ LLMs frequently echo a user's casing ("Front Yard") instead of the
+ configured key ("front_yard"), or fall back to a zone's friendly name
+ ("Front Walkway") instead of its ID ("front_walk"). The downstream zone
+ filter is a SQLite GLOB over the JSON-encoded zones column, which stores
+ config keys and is case-sensitive — so an unnormalized name silently
+ returns zero matches. Build a lookup over the relevant cameras' configured
+ zones, keyed by both the config key and the friendly name, and substitute
+ when we find a match; unknown names pass through so behavior matches what
+ the model asked for.
+ """
+ if not zones:
+ return zones
+
+ lookup: dict[str, str] = {}
+ for camera_id in target_cameras:
+ camera_config = config.cameras.get(camera_id)
+ if camera_config is None:
+ continue
+ for zone_name, zone_config in camera_config.zones.items():
+ lookup.setdefault(zone_name.lower(), zone_name)
+ lookup.setdefault(
+ zone_config.get_formatted_name(zone_name).lower(), zone_name
+ )
+
+ return [lookup.get(z.lower(), z) for z in zones]
+
+
async def _execute_search_objects(
- arguments: Dict[str, Any],
- allowed_cameras: List[str],
+ request: Request,
+ arguments: dict[str, Any],
+ allowed_cameras: list[str],
) -> JSONResponse:
"""
Execute the search_objects tool.
- This searches for detected objects (events) in Frigate using the same
- logic as the events API endpoint.
+ Routes to the semantic path when the LLM supplied a `semantic_query`
+ and semantic search is enabled; otherwise delegates to the standard
+ events API logic.
"""
+ config = request.app.frigate_config
+ semantic_query = arguments.get("semantic_query")
+ if isinstance(semantic_query, str):
+ semantic_query = semantic_query.strip() or None
+ else:
+ semantic_query = None
+
+ if semantic_query and getattr(config.semantic_search, "enabled", False):
+ return await _execute_search_objects_semantic(
+ request, arguments, allowed_cameras, semantic_query
+ )
+
# Parse after/before as server local time; convert to Unix timestamp
after = arguments.get("after")
before = arguments.get("before")
@@ -437,15 +188,23 @@ async def _execute_search_objects(
# Convert zones array to comma-separated string if provided
zones = arguments.get("zones")
if isinstance(zones, list):
+ camera_arg = arguments.get("camera")
+ target_cameras = (
+ [camera_arg] if camera_arg and camera_arg != "all" else allowed_cameras
+ )
+ zones = _resolve_zones(zones, config, target_cameras)
zones = ",".join(zones)
elif zones is None:
zones = "all"
+ attribute = arguments.get("attribute")
+
# Build query parameters compatible with EventsQueryParams
query_params = EventsQueryParams(
cameras=arguments.get("camera", "all"),
labels=arguments.get("label", "all"),
sub_labels=arguments.get("sub_label", "all"), # case-insensitive on the backend
+ attributes=attribute if attribute else "all",
zones=zones,
zone=zones,
after=after,
@@ -462,7 +221,7 @@ async def _execute_search_objects(
# Return it as-is for the LLM
return response
except Exception as e:
- logger.error(f"Error executing search_objects: {e}", exc_info=True)
+ logger.exception(f"Error executing search_objects: {e}")
return JSONResponse(
content={
"success": False,
@@ -472,11 +231,129 @@ async def _execute_search_objects(
)
+async def _execute_search_objects_semantic(
+ request: Request,
+ arguments: dict[str, Any],
+ allowed_cameras: list[str],
+ semantic_query: str,
+) -> JSONResponse:
+ """Search objects via fused thumbnail + description embeddings.
+
+ Runs both visual and description vec searches against `semantic_query`,
+ intersects the candidates with the structured filters (camera, label,
+ sub_label, zones, time window) the LLM supplied, and ranks the survivors
+ by fused similarity. Mirrors the candidate-then-filter pattern used by
+ find_similar_objects since sqlite-vec's IN filter is unreliable.
+ """
+ from peewee import fn
+
+ config = request.app.frigate_config
+ context = request.app.embeddings
+ if context is None:
+ logger.warning(
+ "semantic_query supplied but embeddings context is unavailable; "
+ "returning empty results."
+ )
+ return JSONResponse(content=[])
+
+ after = parse_iso_to_timestamp(arguments.get("after"))
+ before = parse_iso_to_timestamp(arguments.get("before"))
+
+ camera_arg = arguments.get("camera")
+ if camera_arg and camera_arg != "all":
+ if camera_arg not in allowed_cameras:
+ return JSONResponse(content=[])
+ cameras = [camera_arg]
+ else:
+ cameras = list(allowed_cameras) if allowed_cameras else []
+
+ if not cameras:
+ return JSONResponse(content=[])
+
+ label = arguments.get("label")
+ sub_label = arguments.get("sub_label")
+ attribute = arguments.get("attribute")
+
+ zones = arguments.get("zones")
+ if isinstance(zones, list) and zones:
+ zones = _resolve_zones(zones, config, cameras)
+ else:
+ zones = None
+
+ limit = int(arguments.get("limit", 25))
+ limit = max(1, min(limit, 100))
+
+ visual_distances: dict[str, float] = {}
+ description_distances: dict[str, float] = {}
+ try:
+ rows = context.search_thumbnail(semantic_query)
+ visual_distances = {row[0]: row[1] for row in rows}
+ except Exception:
+ logger.exception(
+ "search_thumbnail failed for semantic_query: %s", semantic_query
+ )
+
+ try:
+ rows = context.search_description(semantic_query)
+ description_distances = {row[0]: row[1] for row in rows}
+ except Exception:
+ logger.exception(
+ "search_description failed for semantic_query: %s", semantic_query
+ )
+
+ vec_ids = set(visual_distances) | set(description_distances)
+ if not vec_ids:
+ return JSONResponse(content=[])
+
+ clauses = [Event.id.in_(list(vec_ids)), Event.camera.in_(cameras)]
+ if after is not None:
+ clauses.append(Event.start_time >= after)
+ if before is not None:
+ clauses.append(Event.start_time <= before)
+ if label:
+ clauses.append(Event.label == label)
+ if sub_label:
+ # case-insensitive match to mirror events() behavior
+ clauses.append(fn.LOWER(Event.sub_label.cast("text")) == sub_label.lower())
+ if attribute:
+ attribute_clause = _build_attribute_filter_clause(attribute)
+ if attribute_clause is not None:
+ clauses.append(attribute_clause)
+ if zones:
+ zone_clauses = [Event.zones.cast("text") % f'*"{zone}"*' for zone in zones]
+ clauses.append(reduce(operator.or_, zone_clauses))
+
+ eligible = {e.id: e for e in Event.select().where(reduce(operator.and_, clauses))}
+
+ scored: list[tuple[str, float]] = []
+ for eid in eligible:
+ v_score = (
+ distance_to_score(visual_distances[eid], context.thumb_stats)
+ if eid in visual_distances
+ else None
+ )
+ d_score = (
+ distance_to_score(description_distances[eid], context.desc_stats)
+ if eid in description_distances
+ else None
+ )
+ fused = fuse_scores(v_score, d_score)
+ if fused is None:
+ continue
+ scored.append((eid, fused))
+
+ scored.sort(key=lambda pair: pair[1], reverse=True)
+ scored = scored[:limit]
+
+ results = [hydrate_event(eligible[eid], score=score) for eid, score in scored]
+ return JSONResponse(content=results)
+
+
async def _execute_find_similar_objects(
request: Request,
- arguments: Dict[str, Any],
- allowed_cameras: List[str],
-) -> Dict[str, Any]:
+ arguments: dict[str, Any],
+ allowed_cameras: list[str],
+) -> dict[str, Any]:
"""Execute the find_similar_objects tool.
Returns a plain dict (not JSONResponse) so the chat loop can embed it
@@ -528,6 +405,11 @@ async def _execute_find_similar_objects(
sub_labels = arguments.get("sub_labels")
zones = arguments.get("zones")
+ if zones:
+ zones = _resolve_zones(
+ zones, request.app.frigate_config, cameras or list(allowed_cameras)
+ )
+
similarity_mode = arguments.get("similarity_mode", "fused")
if similarity_mode not in ("visual", "semantic", "fused"):
similarity_mode = "fused"
@@ -541,8 +423,8 @@ async def _execute_find_similar_objects(
# version (see frigate/embeddings/__init__.py). Mirror the pattern used by
# frigate/api/event.py events_search: fetch top-k globally, then intersect
# with the structured filters via Peewee.
- visual_distances: Dict[str, float] = {}
- description_distances: Dict[str, float] = {}
+ visual_distances: dict[str, float] = {}
+ description_distances: dict[str, float] = {}
try:
if similarity_mode in ("visual", "fused"):
@@ -600,7 +482,7 @@ async def _execute_find_similar_objects(
eligible = {e.id: e for e in Event.select().where(reduce(operator.and_, clauses))}
# 6. Fuse and rank.
- scored: List[tuple[str, float]] = []
+ scored: list[tuple[str, float]] = []
for eid in eligible:
v_score = (
distance_to_score(visual_distances[eid], context.thumb_stats)
@@ -641,7 +523,7 @@ async def _execute_find_similar_objects(
async def execute_tool(
request: Request,
body: ToolExecuteRequest = Body(...),
- allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
+ allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
) -> JSONResponse:
"""
Execute a tool function call.
@@ -655,7 +537,7 @@ async def execute_tool(
logger.debug(f"Executing tool: {tool_name} with arguments: {arguments}")
if tool_name == "search_objects":
- return await _execute_search_objects(arguments, allowed_cameras)
+ return await _execute_search_objects(request, arguments, allowed_cameras)
if tool_name == "find_similar_objects":
result = await _execute_find_similar_objects(
@@ -683,11 +565,23 @@ async def execute_tool(
async def _execute_get_live_context(
request: Request,
camera: str,
- allowed_cameras: List[str],
-) -> Dict[str, Any]:
+ allowed_cameras: list[str],
+) -> dict[str, Any]:
+ # Reject wildcards explicitly so models retry with a real camera name
+ # instead of silently fanning out across every camera.
+ if camera in ("*", "all"):
+ return {
+ "error": (
+ "get_live_context requires a single camera name; wildcards "
+ "are not supported. Call this tool once per camera."
+ ),
+ "available_cameras": allowed_cameras,
+ }
+
if camera not in allowed_cameras:
return {
"error": f"Camera '{camera}' not found or access denied",
+ "available_cameras": allowed_cameras,
}
if camera not in request.app.frigate_config.cameras:
@@ -719,7 +613,7 @@ async def _execute_get_live_context(
"stationary": obj_dict.get("stationary", False),
}
- result: Dict[str, Any] = {
+ result: dict[str, Any] = {
"camera": camera,
"timestamp": frame_time,
"detections": list(tracked_objects_dict.values()),
@@ -737,7 +631,7 @@ async def _execute_get_live_context(
return result
except Exception as e:
- logger.error(f"Error executing get_live_context: {e}", exc_info=True)
+ logger.exception(f"Error executing get_live_context: {e}")
return {
"error": "Error getting live context",
}
@@ -746,8 +640,8 @@ async def _execute_get_live_context(
async def _get_live_frame_image_url(
request: Request,
camera: str,
- allowed_cameras: List[str],
-) -> Optional[str]:
+ allowed_cameras: list[str],
+) -> str | None:
"""
Fetch the current live frame for a camera as a base64 data URL.
@@ -785,8 +679,8 @@ async def _get_live_frame_image_url(
async def _execute_set_camera_state(
request: Request,
- arguments: Dict[str, Any],
-) -> Dict[str, Any]:
+ arguments: dict[str, Any],
+) -> dict[str, Any]:
role = request.headers.get("remote-role", "")
if "admin" not in [r.strip() for r in role.split(",")]:
return {"error": "Admin privileges required to change camera settings."}
@@ -825,17 +719,17 @@ async def _execute_set_camera_state(
async def _execute_tool_internal(
tool_name: str,
- arguments: Dict[str, Any],
+ arguments: dict[str, Any],
request: Request,
- allowed_cameras: List[str],
-) -> Dict[str, Any]:
+ allowed_cameras: list[str],
+) -> dict[str, Any]:
"""
Internal helper to execute a tool and return the result as a dict.
This is used by the chat completion endpoint to execute tools.
"""
if tool_name == "search_objects":
- response = await _execute_search_objects(arguments, allowed_cameras)
+ response = await _execute_search_objects(request, arguments, allowed_cameras)
try:
if hasattr(response, "body"):
body_str = response.body.decode("utf-8")
@@ -859,7 +753,14 @@ async def _execute_tool_internal(
"Arguments: %s",
json.dumps(arguments),
)
- return {"error": "Camera parameter is required"}
+ return {
+ "error": (
+ "get_live_context requires a single camera name; "
+ "wildcards and empty values are not supported. "
+ "Call this tool once per camera."
+ ),
+ "available_cameras": allowed_cameras,
+ }
return await _execute_get_live_context(request, camera, allowed_cameras)
elif tool_name == "start_camera_watch":
return await _execute_start_camera_watch(request, arguments)
@@ -882,8 +783,8 @@ async def _execute_tool_internal(
async def _execute_start_camera_watch(
request: Request,
- arguments: Dict[str, Any],
-) -> Dict[str, Any]:
+ arguments: dict[str, Any],
+) -> dict[str, Any]:
camera = arguments.get("camera", "").strip()
condition = arguments.get("condition", "").strip()
max_duration_minutes = int(arguments.get("max_duration_minutes", 60))
@@ -899,6 +800,9 @@ async def _execute_start_camera_watch(
await require_camera_access(camera, request=request)
+ if zones:
+ zones = _resolve_zones(zones, config, [camera])
+
genai_manager = request.app.genai_manager
chat_client = genai_manager.chat_client
if chat_client is None or not chat_client.supports_vision:
@@ -917,7 +821,7 @@ async def _execute_start_camera_watch(
zones=zones,
)
except RuntimeError as e:
- logger.error("Failed to start VLM watch job: %s", e, exc_info=True)
+ logger.exception("Failed to start VLM watch job: %s", e)
return {"error": "Failed to start VLM watch job."}
return {
@@ -930,14 +834,14 @@ async def _execute_start_camera_watch(
}
-def _execute_stop_camera_watch() -> Dict[str, Any]:
+def _execute_stop_camera_watch() -> dict[str, Any]:
cancelled = stop_vlm_watch_job()
if cancelled:
return {"success": True, "message": "Watch job cancelled."}
return {"success": False, "message": "No active watch job to cancel."}
-def _execute_get_profile_status(request: Request) -> Dict[str, Any]:
+def _execute_get_profile_status(request: Request) -> dict[str, Any]:
"""Return profile status including active profile and activation timestamps."""
profile_manager = getattr(request.app, "profile_manager", None)
if profile_manager is None:
@@ -962,9 +866,9 @@ def _execute_get_profile_status(request: Request) -> Dict[str, Any]:
def _execute_get_recap(
- arguments: Dict[str, Any],
- allowed_cameras: List[str],
-) -> Dict[str, Any]:
+ arguments: dict[str, Any],
+ allowed_cameras: list[str],
+) -> dict[str, Any]:
"""Fetch review segments with GenAI metadata for a time period."""
from functools import reduce
@@ -1025,7 +929,7 @@ def _execute_get_recap(
.iterator()
)
- events: List[Dict[str, Any]] = []
+ events: list[dict[str, Any]] = []
for row in rows:
data = row.get("data") or {}
@@ -1036,7 +940,7 @@ def _execute_get_recap(
data = {}
camera = row["camera"]
- event: Dict[str, Any] = {
+ event: dict[str, Any] = {
"camera": camera.replace("_", " ").title(),
"severity": row.get("severity", "detection"),
}
@@ -1095,15 +999,15 @@ def _execute_get_recap(
return {"events": events}
except Exception as e:
- logger.error("Error executing get_recap: %s", e, exc_info=True)
+ logger.exception("Error executing get_recap: %s", e)
return {"error": "Failed to fetch recap data."}
async def _execute_pending_tools(
- pending_tool_calls: List[Dict[str, Any]],
+ pending_tool_calls: list[dict[str, Any]],
request: Request,
- allowed_cameras: List[str],
-) -> tuple[List[ToolCall], List[Dict[str, Any]], List[Dict[str, Any]]]:
+ allowed_cameras: list[str],
+) -> tuple[list[ToolCall], list[dict[str, Any]], list[dict[str, Any]]]:
"""
Execute a list of tool calls.
@@ -1112,9 +1016,9 @@ async def _execute_pending_tools(
tool result dicts for conversation,
extra messages to inject after tool results — e.g. user messages with images)
"""
- tool_calls_out: List[ToolCall] = []
- tool_results: List[Dict[str, Any]] = []
- extra_messages: List[Dict[str, Any]] = []
+ tool_calls_out: list[ToolCall] = []
+ tool_results: list[dict[str, Any]] = []
+ extra_messages: list[dict[str, Any]] = []
for tool_call in pending_tool_calls:
tool_name = tool_call["name"]
tool_args = tool_call.get("arguments") or {}
@@ -1188,13 +1092,12 @@ async def _execute_pending_tools(
}
)
except Exception as e:
- logger.error(
+ logger.exception(
"Error executing tool %s (id: %s): %s. Arguments: %s",
tool_name,
tool_call_id,
e,
json.dumps(tool_args),
- exc_info=True,
)
error_content = json.dumps({"error": f"Tool execution failed: {str(e)}"})
tool_calls_out.append(
@@ -1222,7 +1125,7 @@ async def _execute_pending_tools(
async def chat_completion(
request: Request,
body: ChatCompletionRequest = Body(...),
- allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
+ allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
):
"""
Chat completion endpoint with tool calling support.
@@ -1245,59 +1148,33 @@ async def chat_completion(
status_code=400,
)
- tools = get_tool_definitions()
+ config = request.app.frigate_config
+ semantic_search_enabled = bool(getattr(config.semantic_search, "enabled", False))
+ attribute_classifications = get_attribute_classifications(config)
+ tools = get_tool_definitions(
+ semantic_search_enabled=semantic_search_enabled,
+ attribute_classifications=attribute_classifications,
+ embeddings_language=_embeddings_language(config),
+ )
conversation = []
- current_datetime = datetime.now()
- current_date_str = current_datetime.strftime("%Y-%m-%d")
- current_time_str = current_datetime.strftime("%I:%M:%S %p")
-
- cameras_info = []
- config = request.app.frigate_config
- for camera_id in allowed_cameras:
- if camera_id not in config.cameras:
- continue
- camera_config = config.cameras[camera_id]
- friendly_name = (
- camera_config.friendly_name
- if camera_config.friendly_name
- else camera_id.replace("_", " ").title()
+ # Build the system message only when the client hasn't already pinned one.
+ # The first turn has no system message; we generate it (with the current
+ # timestamp) and return the whole chain so the client persists it. Later
+ # turns send it back verbatim, freezing the timestamp so the prompt prefix
+ # stays byte-identical and the model server's prompt cache keeps hitting.
+ if not body.messages or body.messages[0].role != "system":
+ conversation.append(
+ {
+ "role": "system",
+ "content": build_chat_system_prompt(
+ config=config,
+ allowed_cameras=allowed_cameras,
+ semantic_search_enabled=semantic_search_enabled,
+ attribute_classifications=attribute_classifications,
+ ),
+ }
)
- zone_names = list(camera_config.zones.keys())
- if zone_names:
- cameras_info.append(
- f" - {friendly_name} (ID: {camera_id}, zones: {', '.join(zone_names)})"
- )
- else:
- cameras_info.append(f" - {friendly_name} (ID: {camera_id})")
-
- cameras_section = ""
- if cameras_info:
- cameras_section = (
- "\n\nAvailable cameras:\n"
- + "\n".join(cameras_info)
- + "\n\nWhen users refer to cameras by their friendly name (e.g., 'Back Deck Camera'), use the corresponding camera ID (e.g., 'back_deck_cam') in tool calls."
- )
-
- system_prompt = f"""You are a helpful assistant for Frigate, a security camera NVR system. You help users answer questions about their cameras, detected objects, and events.
-
-Current server local date and time: {current_date_str} at {current_time_str}
-
-Do not start your response with phrases like "I will check...", "Let me see...", or "Let me look...". Answer directly.
-
-Always present times to the user in the server's local timezone. When tool results include start_time_local and end_time_local, use those exact strings when listing or describing detection times—do not convert or invent timestamps. Do not use UTC or ISO format with Z for the user-facing answer unless the tool result only provides Unix timestamps without local time fields.
-When users ask about "today", "yesterday", "this week", etc., use the current date above as reference.
-When searching for objects or events, use ISO 8601 format for dates (e.g., {current_date_str}T00:00:00Z for the start of today).
-Always be accurate with time calculations based on the current date provided.
-
-When a user refers to a specific object they have seen or describe with identifying details ("that green car", "the person in the red jacket", "a package left today"), prefer the find_similar_objects tool over search_objects. Use search_objects first only to locate the anchor event, then pass its id to find_similar_objects. For generic queries like "show me all cars today", keep using search_objects. If a user message begins with [attached_event:], treat that event id as the anchor for any similarity or "tell me more" request in the same message and call find_similar_objects with that id.{cameras_section}"""
-
- conversation.append(
- {
- "role": "system",
- "content": system_prompt,
- }
- )
for msg in body.messages:
msg_dict = {
@@ -1308,11 +1185,13 @@ When a user refers to a specific object they have seen or describe with identify
msg_dict["tool_call_id"] = msg.tool_call_id
if msg.name:
msg_dict["name"] = msg.name
+ if msg.tool_calls is not None:
+ msg_dict["tool_calls"] = msg.tool_calls
conversation.append(msg_dict)
tool_iterations = 0
- tool_calls: List[ToolCall] = []
+ tool_calls: list[ToolCall] = []
max_iterations = body.max_tool_iterations
logger.debug(
@@ -1322,11 +1201,20 @@ When a user refers to a specific object they have seen or describe with identify
# True LLM streaming when client supports it and stream requested
if body.stream and hasattr(genai_client, "chat_with_tools_stream"):
- stream_tool_calls: List[ToolCall] = []
stream_iterations = 0
async def stream_body_llm():
- nonlocal conversation, stream_tool_calls, stream_iterations
+ nonlocal conversation, stream_iterations
+
+ def _emit_chain(extra: list[dict[str, Any]] | None = None):
+ # Return the full conversation (including the system message) so
+ # the client persists and replays it verbatim next turn.
+ chain = conversation + (extra or [])
+ return (
+ json.dumps({"type": "messages", "messages": chain}).encode("utf-8")
+ + b"\n"
+ )
+
while stream_iterations < max_iterations:
if await request.is_disconnected():
logger.debug("Client disconnected, stopping chat stream")
@@ -1339,6 +1227,7 @@ When a user refers to a specific object they have seen or describe with identify
messages=conversation,
tools=tools if tools else None,
tool_choice="auto",
+ enable_thinking=body.enable_thinking,
):
if await request.is_disconnected():
logger.debug("Client disconnected, stopping chat stream")
@@ -1351,6 +1240,18 @@ When a user refers to a specific object they have seen or describe with identify
)
+ b"\n"
)
+ elif kind == "reasoning_delta":
+ yield (
+ json.dumps({"type": "reasoning", "delta": value}).encode(
+ "utf-8"
+ )
+ + b"\n"
+ )
+ elif kind == "stats":
+ yield (
+ json.dumps({"type": "stats", **value}).encode("utf-8")
+ + b"\n"
+ )
elif kind == "message":
msg = value
if msg.get("finish_reason") == "error":
@@ -1378,31 +1279,33 @@ When a user refers to a specific object they have seen or describe with identify
)
return
(
- executed_calls,
+ _executed_calls,
tool_results,
extra_msgs,
) = await _execute_pending_tools(
pending, request, allowed_cameras
)
- stream_tool_calls.extend(executed_calls)
conversation.extend(tool_results)
conversation.extend(extra_msgs)
- yield (
- json.dumps(
- {
- "type": "tool_calls",
- "tool_calls": [
- tc.model_dump() for tc in stream_tool_calls
- ],
- }
- ).encode("utf-8")
- + b"\n"
- )
+ # Emit the running chain so the client can render tool
+ # calls live and replay them verbatim next turn.
+ yield _emit_chain()
break
else:
+ # Streaming never appends the final assistant message
+ # to the conversation, so add it to the chain.
+ yield _emit_chain(
+ extra=[
+ {
+ "role": "assistant",
+ "content": msg.get("content"),
+ }
+ ]
+ )
yield (json.dumps({"type": "done"}).encode("utf-8") + b"\n")
return
else:
+ yield _emit_chain()
yield json.dumps({"type": "done"}).encode("utf-8") + b"\n"
return StreamingResponse(
@@ -1421,6 +1324,7 @@ When a user refers to a specific object they have seen or describe with identify
messages=conversation,
tools=tools if tools else None,
tool_choice="auto",
+ enable_thinking=body.enable_thinking,
)
if response.get("finish_reason") == "error":
@@ -1446,17 +1350,23 @@ When a user refers to a specific object they have seen or describe with identify
final_content = response.get("content") or ""
if body.stream:
+ final_reasoning = response.get("reasoning")
+
+ chain = list(conversation)
async def stream_body() -> Any:
- if tool_calls:
+ yield (
+ json.dumps({"type": "messages", "messages": chain}).encode(
+ "utf-8"
+ )
+ + b"\n"
+ )
+ # Emit the full reasoning trace up front when the
+ # underlying client did not stream it
+ if final_reasoning:
yield (
json.dumps(
- {
- "type": "tool_calls",
- "tool_calls": [
- tc.model_dump() for tc in tool_calls
- ],
- }
+ {"type": "reasoning", "delta": final_reasoning}
).encode("utf-8")
+ b"\n"
)
@@ -1480,11 +1390,13 @@ When a user refers to a specific object they have seen or describe with identify
message=ChatMessageResponse(
role="assistant",
content=final_content,
+ reasoning=response.get("reasoning"),
tool_calls=None,
),
finish_reason=response.get("finish_reason", "stop"),
tool_iterations=tool_iterations,
tool_calls=tool_calls,
+ messages=list(conversation),
).model_dump(),
)
@@ -1517,11 +1429,12 @@ When a user refers to a specific object they have seen or describe with identify
finish_reason="length",
tool_iterations=tool_iterations,
tool_calls=tool_calls,
+ messages=list(conversation),
).model_dump(),
)
except Exception as e:
- logger.error(f"Error in chat completion: {e}", exc_info=True)
+ logger.exception(f"Error in chat completion: {e}")
return JSONResponse(
content={
"error": "An error occurred while processing your request.",
@@ -1581,9 +1494,10 @@ async def start_vlm_monitor(
dispatcher=request.app.dispatcher,
labels=body.labels,
zones=body.zones,
+ username=request.headers.get("remote-user", ""),
)
except RuntimeError as e:
- logger.error("Failed to start VLM watch job: %s", e, exc_info=True)
+ logger.exception("Failed to start VLM watch job: %s", e)
return JSONResponse(
content={"success": False, "message": "Failed to start VLM watch job."},
status_code=409,
@@ -1601,10 +1515,22 @@ async def start_vlm_monitor(
summary="Get current VLM watch job",
description="Returns the current (or most recently completed) VLM watch job.",
)
-async def get_vlm_monitor() -> JSONResponse:
+async def get_vlm_monitor(request: Request) -> JSONResponse:
job = get_vlm_watch_job()
if job is None:
return JSONResponse(content={"active": False}, status_code=200)
+
+ role = request.headers.get("remote-role", "viewer")
+ username = request.headers.get("remote-user", "")
+
+ # Admin and the job's creator always see the job. Other users only see it
+ # if they have access to the camera being watched; otherwise hide it.
+ if role != "admin" and username != job.username:
+ try:
+ await require_camera_access(job.camera, request=request)
+ except HTTPException:
+ return JSONResponse(content={"active": False}, status_code=200)
+
return JSONResponse(content={"active": True, **job.to_dict()}, status_code=200)
@@ -1614,7 +1540,27 @@ async def get_vlm_monitor() -> JSONResponse:
summary="Cancel the current VLM watch job",
description="Cancels the running watch job if one exists.",
)
-async def cancel_vlm_monitor() -> JSONResponse:
+async def cancel_vlm_monitor(request: Request) -> JSONResponse:
+ job = get_vlm_watch_job()
+ if job is None:
+ return JSONResponse(
+ content={"success": False, "message": "No active watch job to cancel."},
+ status_code=404,
+ )
+
+ role = request.headers.get("remote-role", "viewer")
+ username = request.headers.get("remote-user", "")
+
+ # Admin can cancel any job; other users can only cancel jobs they started.
+ if role != "admin" and username != job.username:
+ return JSONResponse(
+ content={
+ "success": False,
+ "message": "Not authorized to cancel this watch job.",
+ },
+ status_code=403,
+ )
+
cancelled = stop_vlm_watch_job()
if not cancelled:
return JSONResponse(
diff --git a/frigate/api/chat_util.py b/frigate/api/chat_util.py
index 743c38e57c..a2f29c75b2 100644
--- a/frigate/api/chat_util.py
+++ b/frigate/api/chat_util.py
@@ -9,8 +9,9 @@ loop state — all inputs and outputs are plain data.
import logging
import math
import time
+from collections.abc import Generator
from datetime import datetime
-from typing import Any, Dict, Generator, List, Optional
+from typing import Any
from frigate.embeddings.util import ZScoreNormalization
from frigate.models import Event
@@ -30,7 +31,7 @@ def chunk_content(content: str, chunk_size: int = 80) -> Generator[str, None, No
if not content:
return
words = content.split(" ")
- current: List[str] = []
+ current: list[str] = []
current_len = 0
for w in words:
current.append(w)
@@ -44,8 +45,8 @@ def chunk_content(content: str, chunk_size: int = 80) -> Generator[str, None, No
def format_events_with_local_time(
- events_list: List[Dict[str, Any]],
-) -> List[Dict[str, Any]]:
+ events_list: list[dict[str, Any]],
+) -> list[dict[str, Any]]:
"""Add human-readable local start/end times to each event for the LLM."""
result = []
for evt in events_list:
@@ -84,9 +85,9 @@ def distance_to_score(distance: float, stats: ZScoreNormalization) -> float:
def fuse_scores(
- visual_score: Optional[float],
- description_score: Optional[float],
-) -> Optional[float]:
+ visual_score: float | None,
+ description_score: float | None,
+) -> float | None:
"""Weighted fusion of visual and description similarity scores.
If one side is missing (e.g., no description embedding for this event),
@@ -102,7 +103,7 @@ def fuse_scores(
return VISUAL_WEIGHT * visual_score + DESCRIPTION_WEIGHT * description_score
-def parse_iso_to_timestamp(value: Optional[str]) -> Optional[float]:
+def parse_iso_to_timestamp(value: str | None) -> float | None:
"""Parse an ISO-8601 string as server-local time -> unix timestamp.
Mirrors the parsing _execute_search_objects uses so both tools accept the
@@ -119,9 +120,9 @@ def parse_iso_to_timestamp(value: Optional[str]) -> Optional[float]:
return None
-def hydrate_event(event: Event, score: Optional[float] = None) -> Dict[str, Any]:
+def hydrate_event(event: Event, score: float | None = None) -> dict[str, Any]:
"""Convert an Event row into the dict shape returned by find_similar_objects."""
- data: Dict[str, Any] = {
+ data: dict[str, Any] = {
"id": event.id,
"camera": event.camera,
"label": event.label,
diff --git a/frigate/api/classification.py b/frigate/api/classification.py
index dea4d28b21..6dd7055092 100644
--- a/frigate/api/classification.py
+++ b/frigate/api/classification.py
@@ -280,7 +280,7 @@ async def create_face(request: Request, name: str):
success response with details about the registration, or an error if face recognition
is not enabled or the image cannot be processed.""",
)
-async def register_face(request: Request, name: str, file: UploadFile):
+def register_face(request: Request, name: str, file: UploadFile):
if not request.app.frigate_config.face_recognition.enabled:
return JSONResponse(
status_code=400,
@@ -288,7 +288,7 @@ async def register_face(request: Request, name: str, file: UploadFile):
)
context: EmbeddingsContext = request.app.embeddings
- result = None if context is None else context.register_face(name, await file.read())
+ result = None if context is None else context.register_face(name, file.file.read())
if not isinstance(result, dict):
return JSONResponse(
@@ -313,7 +313,7 @@ async def register_face(request: Request, name: str, file: UploadFile):
registered faces in the system. Returns the recognized face name and confidence score,
or an error if face recognition is not enabled or the image cannot be processed.""",
)
-async def recognize_face(request: Request, file: UploadFile):
+def recognize_face(request: Request, file: UploadFile):
if not request.app.frigate_config.face_recognition.enabled:
return JSONResponse(
status_code=400,
@@ -321,7 +321,7 @@ async def recognize_face(request: Request, file: UploadFile):
)
context: EmbeddingsContext = request.app.embeddings
- result = context.recognize_face(await file.read())
+ result = context.recognize_face(file.file.read())
if not isinstance(result, dict):
return JSONResponse(
diff --git a/frigate/api/debug_replay.py b/frigate/api/debug_replay.py
index 027d4e50c7..034da3845d 100644
--- a/frigate/api/debug_replay.py
+++ b/frigate/api/debug_replay.py
@@ -6,10 +6,18 @@ from datetime import datetime
from fastapi import APIRouter, Depends, Request
from fastapi.responses import JSONResponse
+from peewee import DoesNotExist
from pydantic import BaseModel, Field
from frigate.api.auth import require_role
from frigate.api.defs.tags import Tags
+from frigate.jobs.debug_replay import (
+ ExportDebugReplaySource,
+ RecordingDebugReplaySource,
+ start_debug_replay_job,
+)
+from frigate.models import Export
+from frigate.util.services import get_video_properties
logger = logging.getLogger(__name__)
@@ -24,15 +32,28 @@ class DebugReplayStartBody(BaseModel):
end_time: float = Field(title="End timestamp")
+class DebugReplayStartFromExportBody(BaseModel):
+ """Request body for starting a debug replay session from an export."""
+
+ export_id: str = Field(title="Export id")
+
+
class DebugReplayStartResponse(BaseModel):
"""Response for starting a debug replay session."""
success: bool
replay_camera: str
+ job_id: str
class DebugReplayStatusResponse(BaseModel):
- """Response for debug replay status."""
+ """Response for debug replay status.
+
+ Returns only session-presence fields. Startup progress and error
+ details flow through the job_state WebSocket topic via the
+ debug_replay job (see frigate.jobs.debug_replay); the
+ Replay page subscribes there with useJobStatus("debug_replay").
+ """
active: bool
replay_camera: str | None = None
@@ -51,15 +72,40 @@ class DebugReplayStopResponse(BaseModel):
@router.post(
"/debug_replay/start",
response_model=DebugReplayStartResponse,
+ status_code=202,
+ responses={
+ 400: {"description": "Invalid camera, time range, or no recordings"},
+ 409: {"description": "A replay session is already active"},
+ },
dependencies=[Depends(require_role(["admin"]))],
summary="Start debug replay",
- description="Start a debug replay session from camera recordings.",
+ description="Start a debug replay session from camera recordings. Returns "
+ "immediately while clip generation runs as a background job; subscribe "
+ "to the 'debug_replay' job_state WS topic to track progress.",
)
async def start_debug_replay(request: Request, body: DebugReplayStartBody):
- """Start a debug replay session."""
+ """Start a debug replay session asynchronously."""
replay_manager = request.app.replay_manager
+ internal_port = request.app.frigate_config.networking.listen.internal
+ if type(internal_port) is str:
+ internal_port = int(internal_port.split(":")[-1])
- if replay_manager.active:
+ source = RecordingDebugReplaySource(
+ source_camera=body.camera,
+ start_ts=body.start_time,
+ end_ts=body.end_time,
+ internal_port=internal_port,
+ )
+
+ try:
+ job_id = await asyncio.to_thread(
+ start_debug_replay_job,
+ source=source,
+ frigate_config=request.app.frigate_config,
+ config_publisher=request.app.config_publisher,
+ replay_manager=replay_manager,
+ )
+ except RuntimeError:
return JSONResponse(
content={
"success": False,
@@ -67,38 +113,102 @@ async def start_debug_replay(request: Request, body: DebugReplayStartBody):
},
status_code=409,
)
-
- try:
- replay_camera = await asyncio.to_thread(
- replay_manager.start,
- source_camera=body.camera,
- start_ts=body.start_time,
- end_ts=body.end_time,
- frigate_config=request.app.frigate_config,
- config_publisher=request.app.config_publisher,
- )
except ValueError:
- logger.exception("Invalid parameters for debug replay start request")
+ logger.exception("Rejected debug replay start request")
return JSONResponse(
content={
"success": False,
- "message": "Invalid debug replay request parameters",
+ "message": "Invalid debug replay parameters",
},
status_code=400,
)
- except RuntimeError:
- logger.exception("Error while starting debug replay session")
+
+ return JSONResponse(
+ content={
+ "success": True,
+ "replay_camera": replay_manager.replay_camera_name,
+ "job_id": job_id,
+ },
+ status_code=202,
+ )
+
+
+@router.post(
+ "/debug_replay/start_from_export",
+ response_model=DebugReplayStartResponse,
+ status_code=202,
+ responses={
+ 400: {"description": "Invalid export, time range, or no recordings"},
+ 404: {"description": "Export not found"},
+ 409: {"description": "A replay session is already active"},
+ },
+ dependencies=[Depends(require_role(["admin"]))],
+ summary="Start debug replay from an export",
+ description="Start a debug replay session covering an existing export's "
+ "time range. The end time is derived from the export's video duration.",
+)
+async def start_debug_replay_from_export(
+ request: Request, body: DebugReplayStartFromExportBody
+):
+ """Start a debug replay session from an existing export."""
+ try:
+ export: Export = Export.get(Export.id == body.export_id)
+ except DoesNotExist:
+ return JSONResponse(
+ content={"success": False, "message": "Export not found"},
+ status_code=404,
+ )
+
+ properties = await get_video_properties(
+ request.app.frigate_config.ffmpeg, export.video_path, get_duration=True
+ )
+ duration = properties.get("duration", -1)
+
+ if duration is None or duration <= 0:
return JSONResponse(
content={
"success": False,
- "message": "An internal error occurred while starting debug replay",
+ "message": "Could not determine export duration",
},
- status_code=500,
+ status_code=400,
)
- return DebugReplayStartResponse(
- success=True,
- replay_camera=replay_camera,
+ replay_manager = request.app.replay_manager
+ source = ExportDebugReplaySource(export=export, duration=float(duration))
+
+ try:
+ job_id = await asyncio.to_thread(
+ start_debug_replay_job,
+ source=source,
+ frigate_config=request.app.frigate_config,
+ config_publisher=request.app.config_publisher,
+ replay_manager=replay_manager,
+ )
+ except RuntimeError:
+ return JSONResponse(
+ content={
+ "success": False,
+ "message": "A replay session is already active",
+ },
+ status_code=409,
+ )
+ except ValueError:
+ logger.exception("Rejected debug replay start request")
+ return JSONResponse(
+ content={
+ "success": False,
+ "message": "Invalid debug replay parameters",
+ },
+ status_code=400,
+ )
+
+ return JSONResponse(
+ content={
+ "success": True,
+ "replay_camera": replay_manager.replay_camera_name,
+ "job_id": job_id,
+ },
+ status_code=202,
)
@@ -118,12 +228,16 @@ def get_debug_replay_status(request: Request):
if replay_manager.active and replay_camera:
frame_processor = request.app.detected_frames_processor
- frame = frame_processor.get_current_frame(replay_camera)
+ frame = (
+ frame_processor.get_current_frame(replay_camera)
+ if frame_processor is not None
+ else None
+ )
if frame is not None:
frame_time = frame_processor.get_current_frame_time(replay_camera)
camera_config = request.app.frigate_config.cameras.get(replay_camera)
- retry_interval = 10
+ retry_interval = 10.0
if camera_config is not None:
retry_interval = float(camera_config.ffmpeg.retry_interval or 10)
diff --git a/frigate/api/defs/query/app_query_parameters.py b/frigate/api/defs/query/app_query_parameters.py
index e182a6afda..626d39679b 100644
--- a/frigate/api/defs/query/app_query_parameters.py
+++ b/frigate/api/defs/query/app_query_parameters.py
@@ -1,12 +1,10 @@
-from typing import Optional
-
from pydantic import BaseModel
class AppTimelineHourlyQueryParameters(BaseModel):
- cameras: Optional[str] = "all"
- labels: Optional[str] = "all"
- after: Optional[float] = None
- before: Optional[float] = None
- limit: Optional[int] = 200
- timezone: Optional[str] = "utc"
+ cameras: str | None = "all"
+ labels: str | None = "all"
+ after: float | None = None
+ before: float | None = None
+ limit: int | None = 200
+ timezone: str | None = "utc"
diff --git a/frigate/api/defs/query/events_query_parameters.py b/frigate/api/defs/query/events_query_parameters.py
index 8e5a5391a9..06d0dfc3af 100644
--- a/frigate/api/defs/query/events_query_parameters.py
+++ b/frigate/api/defs/query/events_query_parameters.py
@@ -1,28 +1,26 @@
-from typing import Optional
-
from pydantic import BaseModel, Field
DEFAULT_TIME_RANGE = "00:00,24:00"
class EventsQueryParams(BaseModel):
- camera: Optional[str] = "all"
- cameras: Optional[str] = "all"
- label: Optional[str] = "all"
- labels: Optional[str] = "all"
- sub_label: Optional[str] = "all"
- sub_labels: Optional[str] = "all"
- attributes: Optional[str] = "all"
- zone: Optional[str] = "all"
- zones: Optional[str] = "all"
- limit: Optional[int] = 100
- after: Optional[float] = None
- before: Optional[float] = None
- time_range: Optional[str] = DEFAULT_TIME_RANGE
- has_clip: Optional[int] = None
- has_snapshot: Optional[int] = None
- in_progress: Optional[int] = None
- include_thumbnails: Optional[int] = Field(
+ camera: str | None = "all"
+ cameras: str | None = "all"
+ label: str | None = "all"
+ labels: str | None = "all"
+ sub_label: str | None = "all"
+ sub_labels: str | None = "all"
+ attributes: str | None = "all"
+ zone: str | None = "all"
+ zones: str | None = "all"
+ limit: int | None = 100
+ after: float | None = None
+ before: float | None = None
+ time_range: str | None = DEFAULT_TIME_RANGE
+ has_clip: int | None = None
+ has_snapshot: int | None = None
+ in_progress: int | None = None
+ include_thumbnails: int | None = Field(
1,
description=(
"Deprecated. Thumbnail data is no longer included in the response. "
@@ -30,25 +28,25 @@ class EventsQueryParams(BaseModel):
),
deprecated=True,
)
- favorites: Optional[int] = None
- min_score: Optional[float] = None
- max_score: Optional[float] = None
- min_speed: Optional[float] = None
- max_speed: Optional[float] = None
- recognized_license_plate: Optional[str] = "all"
- is_submitted: Optional[int] = None
- min_length: Optional[float] = None
- max_length: Optional[float] = None
- event_id: Optional[str] = None
- sort: Optional[str] = None
- timezone: Optional[str] = "utc"
+ favorites: int | None = None
+ min_score: float | None = None
+ max_score: float | None = None
+ min_speed: float | None = None
+ max_speed: float | None = None
+ recognized_license_plate: str | None = "all"
+ is_submitted: int | None = None
+ min_length: float | None = None
+ max_length: float | None = None
+ event_id: str | None = None
+ sort: str | None = None
+ timezone: str | None = "utc"
class EventsSearchQueryParams(BaseModel):
- query: Optional[str] = None
- event_id: Optional[str] = None
- search_type: Optional[str] = "thumbnail"
- include_thumbnails: Optional[int] = Field(
+ query: str | None = None
+ event_id: str | None = None
+ search_type: str | None = "thumbnail"
+ include_thumbnails: int | None = Field(
1,
description=(
"Deprecated. Thumbnail data is no longer included in the response. "
@@ -56,28 +54,28 @@ class EventsSearchQueryParams(BaseModel):
),
deprecated=True,
)
- limit: Optional[int] = 50
- cameras: Optional[str] = "all"
- labels: Optional[str] = "all"
- sub_labels: Optional[str] = "all"
- attributes: Optional[str] = "all"
- zones: Optional[str] = "all"
- after: Optional[float] = None
- before: Optional[float] = None
- time_range: Optional[str] = DEFAULT_TIME_RANGE
- has_clip: Optional[bool] = None
- has_snapshot: Optional[bool] = None
- is_submitted: Optional[bool] = None
- timezone: Optional[str] = "utc"
- min_score: Optional[float] = None
- max_score: Optional[float] = None
- min_speed: Optional[float] = None
- max_speed: Optional[float] = None
- recognized_license_plate: Optional[str] = "all"
- sort: Optional[str] = None
+ limit: int | None = 50
+ cameras: str | None = "all"
+ labels: str | None = "all"
+ sub_labels: str | None = "all"
+ attributes: str | None = "all"
+ zones: str | None = "all"
+ after: float | None = None
+ before: float | None = None
+ time_range: str | None = DEFAULT_TIME_RANGE
+ has_clip: bool | None = None
+ has_snapshot: bool | None = None
+ is_submitted: bool | None = None
+ timezone: str | None = "utc"
+ min_score: float | None = None
+ max_score: float | None = None
+ min_speed: float | None = None
+ max_speed: float | None = None
+ recognized_license_plate: str | None = "all"
+ sort: str | None = None
class EventsSummaryQueryParams(BaseModel):
- timezone: Optional[str] = "utc"
- has_clip: Optional[int] = None
- has_snapshot: Optional[int] = None
+ timezone: str | None = "utc"
+ has_clip: int | None = None
+ has_snapshot: int | None = None
diff --git a/frigate/api/defs/query/media_query_parameters.py b/frigate/api/defs/query/media_query_parameters.py
index 2d4fae1760..f115685970 100644
--- a/frigate/api/defs/query/media_query_parameters.py
+++ b/frigate/api/defs/query/media_query_parameters.py
@@ -1,5 +1,4 @@
from enum import Enum
-from typing import Optional
from pydantic import BaseModel
@@ -17,33 +16,33 @@ class Extension(str, Enum):
class MediaLatestFrameQueryParams(BaseModel):
- bbox: Optional[int] = None
- timestamp: Optional[int] = None
- zones: Optional[int] = None
- mask: Optional[int] = None
- motion: Optional[int] = None
- paths: Optional[int] = None
- regions: Optional[int] = None
- quality: Optional[int] = 70
- height: Optional[int] = None
- store: Optional[int] = None
+ bbox: int | None = None
+ timestamp: int | None = None
+ zones: int | None = None
+ mask: int | None = None
+ motion: int | None = None
+ paths: int | None = None
+ regions: int | None = None
+ quality: int | None = 70
+ height: int | None = None
+ store: int | None = None
class MediaEventsSnapshotQueryParams(BaseModel):
- download: Optional[bool] = False
- timestamp: Optional[int] = None
- bbox: Optional[int] = None
- crop: Optional[int] = None
- height: Optional[int] = None
- quality: Optional[int] = None
+ download: bool | None = False
+ timestamp: int | None = None
+ bbox: int | None = None
+ crop: int | None = None
+ height: int | None = None
+ quality: int | None = None
class MediaMjpegFeedQueryParams(BaseModel):
fps: int = 3
height: int = 360
- bbox: Optional[int] = None
- timestamp: Optional[int] = None
- zones: Optional[int] = None
- mask: Optional[int] = None
- motion: Optional[int] = None
- regions: Optional[int] = None
+ bbox: int | None = None
+ timestamp: int | None = None
+ zones: int | None = None
+ mask: int | None = None
+ motion: int | None = None
+ regions: int | None = None
diff --git a/frigate/api/defs/query/recordings_query_parameters.py b/frigate/api/defs/query/recordings_query_parameters.py
index d4f1b0a7be..770da96bb8 100644
--- a/frigate/api/defs/query/recordings_query_parameters.py
+++ b/frigate/api/defs/query/recordings_query_parameters.py
@@ -1,21 +1,19 @@
-from typing import Optional, Union
-
from pydantic import BaseModel
from pydantic.json_schema import SkipJsonSchema
class MediaRecordingsSummaryQueryParams(BaseModel):
timezone: str = "utc"
- cameras: Optional[str] = "all"
+ cameras: str | None = "all"
class MediaRecordingsAvailabilityQueryParams(BaseModel):
cameras: str = "all"
- before: Union[float, SkipJsonSchema[None]] = None
- after: Union[float, SkipJsonSchema[None]] = None
+ before: float | SkipJsonSchema[None] = None
+ after: float | SkipJsonSchema[None] = None
scale: int = 30
class RecordingsDeleteQueryParams(BaseModel):
- keep: Optional[str] = None
- cameras: Optional[str] = "all"
+ keep: str | None = None
+ cameras: str | None = "all"
diff --git a/frigate/api/defs/query/regenerate_query_parameters.py b/frigate/api/defs/query/regenerate_query_parameters.py
index af50ada2c4..20d1016c41 100644
--- a/frigate/api/defs/query/regenerate_query_parameters.py
+++ b/frigate/api/defs/query/regenerate_query_parameters.py
@@ -1,13 +1,11 @@
-from typing import Optional
-
from pydantic import BaseModel, Field
from frigate.events.types import RegenerateDescriptionEnum
class RegenerateQueryParameters(BaseModel):
- source: Optional[RegenerateDescriptionEnum] = RegenerateDescriptionEnum.thumbnails
- force: Optional[bool] = Field(
+ source: RegenerateDescriptionEnum | None = RegenerateDescriptionEnum.thumbnails
+ force: bool | None = Field(
default=False,
description="Force (re)generating the description even if GenAI is disabled for this camera.",
)
diff --git a/frigate/api/defs/query/review_query_parameters.py b/frigate/api/defs/query/review_query_parameters.py
index 6d01d824d6..b16146a9bc 100644
--- a/frigate/api/defs/query/review_query_parameters.py
+++ b/frigate/api/defs/query/review_query_parameters.py
@@ -1,5 +1,3 @@
-from typing import Union
-
from pydantic import BaseModel
from pydantic.json_schema import SkipJsonSchema
@@ -10,11 +8,11 @@ class ReviewQueryParams(BaseModel):
cameras: str = "all"
labels: str = "all"
zones: str = "all"
- reviewed: Union[int, SkipJsonSchema[None]] = None
- limit: Union[int, SkipJsonSchema[None]] = None
- severity: Union[SeverityEnum, SkipJsonSchema[None]] = None
- before: Union[float, SkipJsonSchema[None]] = None
- after: Union[float, SkipJsonSchema[None]] = None
+ reviewed: int | SkipJsonSchema[None] = None
+ limit: int | SkipJsonSchema[None] = None
+ severity: SeverityEnum | SkipJsonSchema[None] = None
+ before: float | SkipJsonSchema[None] = None
+ after: float | SkipJsonSchema[None] = None
class ReviewSummaryQueryParams(BaseModel):
@@ -26,6 +24,6 @@ class ReviewSummaryQueryParams(BaseModel):
class ReviewActivityMotionQueryParams(BaseModel):
cameras: str = "all"
- before: Union[float, SkipJsonSchema[None]] = None
- after: Union[float, SkipJsonSchema[None]] = None
+ before: float | SkipJsonSchema[None] = None
+ after: float | SkipJsonSchema[None] = None
scale: int = 30
diff --git a/frigate/api/defs/request/app_body.py b/frigate/api/defs/request/app_body.py
index d9d11fd019..b0b85c7ab9 100644
--- a/frigate/api/defs/request/app_body.py
+++ b/frigate/api/defs/request/app_body.py
@@ -1,24 +1,33 @@
-from typing import Any, Dict, List, Optional
+from typing import Any
from pydantic import BaseModel, Field
+from frigate.config import GenAIProviderEnum
+
class AppConfigSetBody(BaseModel):
requires_restart: int = 1
update_topic: str | None = None
- config_data: Optional[Dict[str, Any]] = None
+ config_data: dict[str, Any] | None = None
skip_save: bool = False
+class GenAIProbeBody(BaseModel):
+ provider: GenAIProviderEnum
+ api_key: str | None = None
+ base_url: str | None = None
+ provider_options: dict[str, Any] = Field(default_factory=dict)
+
+
class AppPutPasswordBody(BaseModel):
password: str
- old_password: Optional[str] = None
+ old_password: str | None = None
class AppPostUsersBody(BaseModel):
username: str
password: str
- role: Optional[str] = "viewer"
+ role: str | None = "viewer"
class AppPostLoginBody(BaseModel):
@@ -38,7 +47,7 @@ class MediaSyncBody(BaseModel):
dry_run: bool = Field(
default=True, description="If True, only report orphans without deleting them"
)
- media_types: List[str] = Field(
+ media_types: list[str] = Field(
default=["all"],
description="Types of media to sync: 'all', 'event_snapshots', 'event_thumbnails', 'review_thumbnails', 'previews', 'exports', 'recordings'",
)
diff --git a/frigate/api/defs/request/batch_export_body.py b/frigate/api/defs/request/batch_export_body.py
index c0863c8857..24078bcc7c 100644
--- a/frigate/api/defs/request/batch_export_body.py
+++ b/frigate/api/defs/request/batch_export_body.py
@@ -1,5 +1,3 @@
-from typing import List, Optional
-
from pydantic import BaseModel, Field, model_validator
MAX_BATCH_EXPORT_ITEMS = 50
@@ -9,18 +7,18 @@ class BatchExportItem(BaseModel):
camera: str = Field(title="Camera name")
start_time: float = Field(title="Start time")
end_time: float = Field(title="End time")
- image_path: Optional[str] = Field(
+ image_path: str | None = Field(
default=None,
title="Existing thumbnail path",
description="Optional existing image to use as the export thumbnail",
)
- friendly_name: Optional[str] = Field(
+ friendly_name: str | None = Field(
default=None,
title="Friendly name",
max_length=256,
description="Optional friendly name for this specific export item",
)
- client_item_id: Optional[str] = Field(
+ client_item_id: str | None = Field(
default=None,
title="Client item ID",
max_length=128,
@@ -29,13 +27,13 @@ class BatchExportItem(BaseModel):
class BatchExportBody(BaseModel):
- items: List[BatchExportItem] = Field(
+ items: list[BatchExportItem] = Field(
title="Items",
min_length=1,
max_length=MAX_BATCH_EXPORT_ITEMS,
description="List of export items. Each item has its own camera and time range.",
)
- export_case_id: Optional[str] = Field(
+ export_case_id: str | None = Field(
default=None,
title="Export case ID",
max_length=30,
@@ -44,13 +42,13 @@ class BatchExportBody(BaseModel):
"existing case is temporarily admin-only until case-level ACLs exist."
),
)
- new_case_name: Optional[str] = Field(
+ new_case_name: str | None = Field(
default=None,
title="New case name",
max_length=100,
description="Name of a new export case to create when export_case_id is omitted",
)
- new_case_description: Optional[str] = Field(
+ new_case_description: str | None = Field(
default=None,
title="New case description",
description="Optional description for a newly created export case",
diff --git a/frigate/api/defs/request/chat_body.py b/frigate/api/defs/request/chat_body.py
index 79ca3a6fef..5ca674fbc8 100644
--- a/frigate/api/defs/request/chat_body.py
+++ b/frigate/api/defs/request/chat_body.py
@@ -1,6 +1,6 @@
"""Chat API request models."""
-from typing import Optional
+from typing import Any
from pydantic import BaseModel, Field
@@ -11,13 +11,29 @@ class ChatMessage(BaseModel):
role: str = Field(
description="Message role: 'user', 'assistant', 'system', or 'tool'"
)
- content: str = Field(description="Message content")
- tool_call_id: Optional[str] = Field(
+ content: Any | None = Field(
+ default=None,
+ description=(
+ "Message content. Usually a string, but may be a multimodal content "
+ "list (e.g. text + image_url) or null for assistant turns that only "
+ "request tool calls."
+ ),
+ )
+ tool_call_id: str | None = Field(
default=None, description="For tool messages, the ID of the tool call"
)
- name: Optional[str] = Field(
+ name: str | None = Field(
default=None, description="For tool messages, the tool name"
)
+ tool_calls: list[dict[str, Any]] | None = Field(
+ default=None,
+ description=(
+ "For assistant messages replayed from prior turns, the OpenAI-format "
+ "tool calls the model previously requested. Replaying these verbatim "
+ "keeps the conversation prefix byte-for-byte identical so the model "
+ "server's prompt cache hits on follow-up turns."
+ ),
+ )
class ChatCompletionRequest(BaseModel):
@@ -36,3 +52,10 @@ class ChatCompletionRequest(BaseModel):
default=False,
description="If true, stream the final assistant response in the body as newline-delimited JSON.",
)
+ enable_thinking: bool | None = Field(
+ default=None,
+ description=(
+ "Per-request thinking toggle. None means use the provider default. "
+ "Ignored by providers that do not expose a per-request thinking switch."
+ ),
+ )
diff --git a/frigate/api/defs/request/classification_body.py b/frigate/api/defs/request/classification_body.py
index fb6a7dd0fd..2aa90f4580 100644
--- a/frigate/api/defs/request/classification_body.py
+++ b/frigate/api/defs/request/classification_body.py
@@ -1,5 +1,3 @@
-from typing import Dict, List, Tuple
-
from pydantic import BaseModel, Field
@@ -12,14 +10,14 @@ class AudioTranscriptionBody(BaseModel):
class DeleteFaceImagesBody(BaseModel):
- ids: List[str] = Field(
+ ids: list[str] = Field(
description="List of image filenames to delete from the face folder"
)
class GenerateStateExamplesBody(BaseModel):
model_name: str = Field(description="Name of the classification model")
- cameras: Dict[str, Tuple[float, float, float, float]] = Field(
+ cameras: dict[str, tuple[float, float, float, float]] = Field(
description="Dictionary mapping camera names to normalized crop coordinates in [x1, y1, x2, y2] format (values 0-1)"
)
diff --git a/frigate/api/defs/request/events_body.py b/frigate/api/defs/request/events_body.py
index d844c31ca7..c7920ae897 100644
--- a/frigate/api/defs/request/events_body.py
+++ b/frigate/api/defs/request/events_body.py
@@ -1,5 +1,3 @@
-from typing import List, Optional, Union
-
from pydantic import BaseModel, Field
from frigate.config.classification import TriggerType
@@ -7,49 +5,47 @@ from frigate.config.classification import TriggerType
class EventsSubLabelBody(BaseModel):
subLabel: str = Field(title="Sub label", max_length=100)
- subLabelScore: Optional[float] = Field(
+ subLabelScore: float | None = Field(
title="Score for sub label", default=None, gt=0.0, le=1.0
)
- camera: Optional[str] = Field(
- title="Camera this object is detected on.", default=None
- )
+ camera: str | None = Field(title="Camera this object is detected on.", default=None)
class EventsLPRBody(BaseModel):
recognizedLicensePlate: str = Field(
title="Recognized License Plate", max_length=100
)
- recognizedLicensePlateScore: Optional[float] = Field(
+ recognizedLicensePlateScore: float | None = Field(
title="Score for recognized license plate", default=None, gt=0.0, le=1.0
)
class EventsAttributesBody(BaseModel):
- attributes: List[str] = Field(
+ attributes: list[str] = Field(
title="Selected classification attributes for the event",
default_factory=list,
)
class EventsDescriptionBody(BaseModel):
- description: Union[str, None] = Field(title="The description of the event")
+ description: str | None = Field(title="The description of the event")
class EventsCreateBody(BaseModel):
- sub_label: Optional[str] = None
- score: Optional[float] = 0
- duration: Optional[int] = 30
- include_recording: Optional[bool] = True
- draw: Optional[dict] = {}
- pre_capture: Optional[int] = None
+ sub_label: str | None = None
+ score: float | None = 0
+ duration: int | None = 30
+ include_recording: bool | None = True
+ draw: dict | None = {}
+ pre_capture: int | None = None
class EventsEndBody(BaseModel):
- end_time: Optional[float] = None
+ end_time: float | None = None
class EventsDeleteBody(BaseModel):
- event_ids: List[str] = Field(title="The event IDs to delete")
+ event_ids: list[str] = Field(title="The event IDs to delete")
class SubmitPlusBody(BaseModel):
diff --git a/frigate/api/defs/request/export_bulk_body.py b/frigate/api/defs/request/export_bulk_body.py
index 004c67d90c..07283f81d4 100644
--- a/frigate/api/defs/request/export_bulk_body.py
+++ b/frigate/api/defs/request/export_bulk_body.py
@@ -1,7 +1,5 @@
"""Request bodies for bulk export operations."""
-from typing import Optional
-
from pydantic import BaseModel, Field, conlist, constr
@@ -17,7 +15,7 @@ class ExportBulkReassignBody(BaseModel):
# List of export IDs with at least one element and each element with at least one char
ids: conlist(constr(min_length=1), min_length=1)
- export_case_id: Optional[str] = Field(
+ export_case_id: str | None = Field(
default=None,
max_length=30,
description="Case ID to assign to, or null to unassign from current case",
diff --git a/frigate/api/defs/request/export_case_body.py b/frigate/api/defs/request/export_case_body.py
index 66cba58ea0..c7b436f14b 100644
--- a/frigate/api/defs/request/export_case_body.py
+++ b/frigate/api/defs/request/export_case_body.py
@@ -1,5 +1,3 @@
-from typing import Optional
-
from pydantic import BaseModel, Field
@@ -7,7 +5,7 @@ class ExportCaseCreateBody(BaseModel):
"""Request body for creating a new export case."""
name: str = Field(max_length=100, description="Friendly name of the export case")
- description: Optional[str] = Field(
+ description: str | None = Field(
default=None, description="Optional description of the export case"
)
@@ -15,11 +13,11 @@ class ExportCaseCreateBody(BaseModel):
class ExportCaseUpdateBody(BaseModel):
"""Request body for updating an existing export case."""
- name: Optional[str] = Field(
+ name: str | None = Field(
default=None,
max_length=100,
description="Updated friendly name of the export case",
)
- description: Optional[str] = Field(
+ description: str | None = Field(
default=None, description="Updated description of the export case"
)
diff --git a/frigate/api/defs/request/export_recordings_body.py b/frigate/api/defs/request/export_recordings_body.py
index 96ecccaa4c..beb8f39962 100644
--- a/frigate/api/defs/request/export_recordings_body.py
+++ b/frigate/api/defs/request/export_recordings_body.py
@@ -1,23 +1,32 @@
-from typing import Optional, Union
-
from pydantic import BaseModel, Field
from pydantic.json_schema import SkipJsonSchema
-from frigate.record.export import PlaybackSourceEnum
+from frigate.record.export import (
+ ChaptersEnum,
+ PlaybackSourceEnum,
+)
class ExportRecordingsBody(BaseModel):
source: PlaybackSourceEnum = Field(
default=PlaybackSourceEnum.recordings, title="Playback source"
)
- name: Optional[str] = Field(title="Friendly name", default=None, max_length=256)
- image_path: Union[str, SkipJsonSchema[None]] = None
- export_case_id: Optional[str] = Field(
+ name: str | None = Field(title="Friendly name", default=None, max_length=256)
+ image_path: str | SkipJsonSchema[None] = None
+ export_case_id: str | None = Field(
default=None,
title="Export case ID",
max_length=30,
description="ID of the export case to assign this export to",
)
+ chapters: ChaptersEnum | None = Field(
+ default=None,
+ title="Chapter mode",
+ description=(
+ "Optional chapter metadata to embed in the export. When omitted, "
+ "the camera's configured export chapter mode is used."
+ ),
+ )
class ExportRecordingsCustomBody(BaseModel):
@@ -25,19 +34,19 @@ class ExportRecordingsCustomBody(BaseModel):
default=PlaybackSourceEnum.recordings, title="Playback source"
)
name: str = Field(title="Friendly name", default=None, max_length=256)
- image_path: Union[str, SkipJsonSchema[None]] = None
- export_case_id: Optional[str] = Field(
+ image_path: str | SkipJsonSchema[None] = None
+ export_case_id: str | None = Field(
default=None,
title="Export case ID",
max_length=30,
description="ID of the export case to assign this export to",
)
- ffmpeg_input_args: Optional[str] = Field(
+ ffmpeg_input_args: str | None = Field(
default=None,
title="FFmpeg input arguments",
description="Custom FFmpeg input arguments. If not provided, defaults to timelapse input args.",
)
- ffmpeg_output_args: Optional[str] = Field(
+ ffmpeg_output_args: str | None = Field(
default=None,
title="FFmpeg output arguments",
description="Custom FFmpeg output arguments. If not provided, defaults to timelapse output args.",
diff --git a/frigate/api/defs/response/chat_response.py b/frigate/api/defs/response/chat_response.py
index 0bc864ba68..3007c4c0f4 100644
--- a/frigate/api/defs/response/chat_response.py
+++ b/frigate/api/defs/response/chat_response.py
@@ -1,6 +1,6 @@
"""Chat API response models."""
-from typing import Any, Optional
+from typing import Any
from pydantic import BaseModel, Field
@@ -17,10 +17,14 @@ class ChatMessageResponse(BaseModel):
"""A message in the chat response."""
role: str = Field(description="Message role")
- content: Optional[str] = Field(
+ content: str | None = Field(
default=None, description="Message content (None if tool calls present)"
)
- tool_calls: Optional[list[ToolCallInvocation]] = Field(
+ reasoning: str | None = Field(
+ default=None,
+ description="Separated reasoning/thinking trace if the model emitted one",
+ )
+ tool_calls: list[ToolCallInvocation] | None = Field(
default=None, description="Tool calls if LLM wants to call tools"
)
@@ -52,3 +56,12 @@ class ChatCompletionResponse(BaseModel):
default_factory=list,
description="List of tool calls that were executed during this completion",
)
+ messages: list[dict[str, Any]] = Field(
+ default_factory=list,
+ description=(
+ "The full conversation chain, including the system message. Persist "
+ "and replay this verbatim on the next request so the prompt prefix "
+ "stays byte-identical and the model server's prompt cache keeps "
+ "hitting."
+ ),
+ )
diff --git a/frigate/api/defs/response/classification_response.py b/frigate/api/defs/response/classification_response.py
index 92d354f242..09cdfbc176 100644
--- a/frigate/api/defs/response/classification_response.py
+++ b/frigate/api/defs/response/classification_response.py
@@ -1,9 +1,7 @@
-from typing import Dict, List, Optional
-
from pydantic import BaseModel, Field, RootModel
-class FacesResponse(RootModel[Dict[str, List[str]]]):
+class FacesResponse(RootModel[dict[str, list[str]]]):
"""Response model for the get_faces endpoint.
Returns a mapping of face names to lists of image filenames.
@@ -17,7 +15,7 @@ class FacesResponse(RootModel[Dict[str, List[str]]]):
}
"""
- root: Dict[str, List[str]] = Field(
+ root: dict[str, list[str]] = Field(
default_factory=dict,
description="Dictionary mapping face names to lists of image filenames",
)
@@ -30,9 +28,9 @@ class FaceRecognitionResponse(BaseModel):
"""
success: bool = Field(description="Whether the face recognition was successful")
- score: Optional[float] = Field(
+ score: float | None = Field(
default=None, description="Confidence score of the recognition (0-1)"
)
- face_name: Optional[str] = Field(
+ face_name: str | None = Field(
default=None, description="The recognized face name if successful"
)
diff --git a/frigate/api/defs/response/event_response.py b/frigate/api/defs/response/event_response.py
index 083849706a..a366af8d1e 100644
--- a/frigate/api/defs/response/event_response.py
+++ b/frigate/api/defs/response/event_response.py
@@ -1,4 +1,4 @@
-from typing import Any, Optional
+from typing import Any
from pydantic import BaseModel, ConfigDict
@@ -6,20 +6,20 @@ from pydantic import BaseModel, ConfigDict
class EventResponse(BaseModel):
id: str
label: str
- sub_label: Optional[str]
+ sub_label: str | None
camera: str
start_time: float
- end_time: Optional[float]
- false_positive: Optional[bool]
+ end_time: float | None
+ false_positive: bool | None
zones: list[str]
- thumbnail: Optional[str]
+ thumbnail: str | None
has_clip: bool
has_snapshot: bool
retain_indefinitely: bool
- plus_id: Optional[str]
- model_hash: Optional[str]
- detector_type: Optional[str]
- model_type: Optional[str]
+ plus_id: str | None
+ model_hash: str | None
+ detector_type: str | None
+ model_type: str | None
data: dict[str, Any]
model_config = ConfigDict(protected_namespaces=())
diff --git a/frigate/api/defs/response/export_case_response.py b/frigate/api/defs/response/export_case_response.py
index 713e166837..199d5b6b76 100644
--- a/frigate/api/defs/response/export_case_response.py
+++ b/frigate/api/defs/response/export_case_response.py
@@ -1,5 +1,3 @@
-from typing import List, Optional
-
from pydantic import BaseModel, Field
@@ -8,7 +6,7 @@ class ExportCaseModel(BaseModel):
id: str = Field(description="Unique identifier for the export case")
name: str = Field(description="Friendly name of the export case")
- description: Optional[str] = Field(
+ description: str | None = Field(
default=None, description="Optional description of the export case"
)
created_at: float = Field(
@@ -19,4 +17,4 @@ class ExportCaseModel(BaseModel):
)
-ExportCasesResponse = List[ExportCaseModel]
+ExportCasesResponse = list[ExportCaseModel]
diff --git a/frigate/api/defs/response/export_response.py b/frigate/api/defs/response/export_response.py
index 10b4a7e643..5e900599d3 100644
--- a/frigate/api/defs/response/export_response.py
+++ b/frigate/api/defs/response/export_response.py
@@ -1,4 +1,4 @@
-from typing import Any, List, Optional
+from typing import Any
from pydantic import BaseModel, Field
@@ -15,7 +15,7 @@ class ExportModel(BaseModel):
in_progress: bool = Field(
description="Whether the export is currently being processed"
)
- export_case_id: Optional[str] = Field(
+ export_case_id: str | None = Field(
default=None, description="ID of the export case this export belongs to"
)
@@ -25,10 +25,10 @@ class StartExportResponse(BaseModel):
success: bool = Field(description="Whether the export was started successfully")
message: str = Field(description="Status or error message")
- export_id: Optional[str] = Field(
+ export_id: str | None = Field(
default=None, description="The export ID if successfully started"
)
- status: Optional[str] = Field(
+ status: str | None = Field(
default=None,
description="Queue status for the export job",
)
@@ -38,24 +38,24 @@ class BatchExportResultModel(BaseModel):
"""Per-item result for a batch export request."""
camera: str = Field(description="Camera name for this export attempt")
- export_id: Optional[str] = Field(
+ export_id: str | None = Field(
default=None,
description="The export ID when the export was successfully queued",
)
success: bool = Field(description="Whether the export was successfully queued")
- status: Optional[str] = Field(
+ status: str | None = Field(
default=None,
description="Queue status for this camera export",
)
- error: Optional[str] = Field(
+ error: str | None = Field(
default=None,
description="Validation or queueing error for this item, if any",
)
- item_index: Optional[int] = Field(
+ item_index: int | None = Field(
default=None,
description="Zero-based index of this result within the request items list",
)
- client_item_id: Optional[str] = Field(
+ client_item_id: str | None = Field(
default=None,
description="Opaque client-supplied item identifier echoed from the request",
)
@@ -64,12 +64,12 @@ class BatchExportResultModel(BaseModel):
class BatchExportResponse(BaseModel):
"""Response model for starting an export batch."""
- export_case_id: Optional[str] = Field(
+ export_case_id: str | None = Field(
default=None,
description="Export case ID associated with the batch",
)
- export_ids: List[str] = Field(description="Export IDs successfully queued")
- results: List[BatchExportResultModel] = Field(
+ export_ids: list[str] = Field(description="Export IDs successfully queued")
+ results: list[BatchExportResultModel] = Field(
description="Per-item batch export results"
)
@@ -81,29 +81,29 @@ class ExportJobModel(BaseModel):
job_type: str = Field(description="Job type")
status: str = Field(description="Current job status")
camera: str = Field(description="Camera associated with this export job")
- name: Optional[str] = Field(
+ name: str | None = Field(
default=None,
description="Friendly name for the export",
)
- export_case_id: Optional[str] = Field(
+ export_case_id: str | None = Field(
default=None,
description="ID of the export case this export belongs to",
)
request_start_time: float = Field(description="Requested export start time")
request_end_time: float = Field(description="Requested export end time")
- start_time: Optional[float] = Field(
+ start_time: float | None = Field(
default=None,
description="Unix timestamp when execution started",
)
- end_time: Optional[float] = Field(
+ end_time: float | None = Field(
default=None,
description="Unix timestamp when execution completed",
)
- error_message: Optional[str] = Field(
+ error_message: str | None = Field(
default=None,
description="Error message for failed jobs",
)
- results: Optional[dict[str, Any]] = Field(
+ results: dict[str, Any] | None = Field(
default=None,
description="Result metadata for completed jobs",
)
@@ -117,7 +117,7 @@ class ExportJobModel(BaseModel):
)
-ExportJobsResponse = List[ExportJobModel]
+ExportJobsResponse = list[ExportJobModel]
-ExportsResponse = List[ExportModel]
+ExportsResponse = list[ExportModel]
diff --git a/frigate/api/defs/response/preview_response.py b/frigate/api/defs/response/preview_response.py
index d320a865da..70d2cac171 100644
--- a/frigate/api/defs/response/preview_response.py
+++ b/frigate/api/defs/response/preview_response.py
@@ -1,5 +1,3 @@
-from typing import List
-
from pydantic import BaseModel, Field
@@ -13,5 +11,5 @@ class PreviewModel(BaseModel):
end: float = Field(description="Unix timestamp when the preview ends")
-PreviewsResponse = List[PreviewModel]
-PreviewFramesResponse = List[str]
+PreviewsResponse = list[PreviewModel]
+PreviewFramesResponse = list[str]
diff --git a/frigate/api/defs/response/review_response.py b/frigate/api/defs/response/review_response.py
index b2fed3b1a6..a0b755bd60 100644
--- a/frigate/api/defs/response/review_response.py
+++ b/frigate/api/defs/response/review_response.py
@@ -1,5 +1,4 @@
from datetime import datetime
-from typing import Dict
from pydantic import BaseModel, Json
@@ -34,7 +33,7 @@ class DayReview(BaseModel):
class ReviewSummaryResponse(BaseModel):
last24Hours: Last24HoursReview
- root: Dict[str, DayReview]
+ root: dict[str, DayReview]
class ReviewActivityMotionResponse(BaseModel):
diff --git a/frigate/api/event.py b/frigate/api/event.py
index a7d1cffc87..d37a547ba4 100644
--- a/frigate/api/event.py
+++ b/frigate/api/event.py
@@ -10,7 +10,6 @@ import random
import string
from functools import reduce
from pathlib import Path
-from typing import List
from urllib.parse import unquote
import numpy as np
@@ -97,7 +96,7 @@ def _build_attribute_filter_clause(attributes: str):
)
def events(
params: EventsQueryParams = Depends(),
- allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
+ allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
):
camera = params.camera
cameras = params.cameras
@@ -171,7 +170,7 @@ def events(
]
if camera != "all":
- clauses.append((Event.camera == camera))
+ clauses.append(Event.camera == camera)
if cameras != "all":
requested = set(cameras.split(","))
@@ -181,11 +180,11 @@ def events(
camera_list = list(filtered)
else:
camera_list = allowed_cameras
- clauses.append((Event.camera << camera_list))
+ clauses.append(Event.camera << camera_list)
if labels != "all":
label_list = labels.split(",")
- clauses.append((Event.label << label_list))
+ clauses.append(Event.label << label_list)
if sub_labels != "all":
# use matching so joined sub labels are included
@@ -196,24 +195,24 @@ def events(
if "None" in filtered_sub_labels:
filtered_sub_labels.remove("None")
- sub_label_clauses.append((Event.sub_label.is_null()))
+ sub_label_clauses.append(Event.sub_label.is_null())
for label in filtered_sub_labels:
lowered = label.lower()
sub_label_clauses.append(
- (fn.LOWER(Event.sub_label.cast("text")) == lowered)
+ fn.LOWER(Event.sub_label.cast("text")) == lowered
) # include exact matches (case-insensitive)
# include this label when part of a list (LIKE is case-insensitive in sqlite for ASCII)
sub_label_clauses.append(
- (fn.LOWER(Event.sub_label.cast("text")) % f"*{lowered},*")
+ fn.LOWER(Event.sub_label.cast("text")) % f"*{lowered},*"
)
sub_label_clauses.append(
- (fn.LOWER(Event.sub_label.cast("text")) % f"*, {lowered}*")
+ fn.LOWER(Event.sub_label.cast("text")) % f"*, {lowered}*"
)
sub_label_clause = reduce(operator.or_, sub_label_clauses)
- clauses.append((sub_label_clause))
+ clauses.append(sub_label_clause)
if attributes != "all":
# Custom classification results are stored as data[model_name] = result_value
@@ -257,19 +256,19 @@ def events(
if "None" in filtered_zones:
filtered_zones.remove("None")
- zone_clauses.append((Event.zones.length() == 0))
+ zone_clauses.append(Event.zones.length() == 0)
for zone in filtered_zones:
- zone_clauses.append((Event.zones.cast("text") % f'*"{zone}"*'))
+ zone_clauses.append(Event.zones.cast("text") % f'*"{zone}"*')
zone_clause = reduce(operator.or_, zone_clauses)
- clauses.append((zone_clause))
+ clauses.append(zone_clause)
if after:
- clauses.append((Event.start_time > after))
+ clauses.append(Event.start_time > after)
if before:
- clauses.append((Event.start_time < before))
+ clauses.append(Event.start_time < before)
if time_range != DEFAULT_TIME_RANGE:
# get timezone arg to ensure browser times are used
@@ -289,62 +288,60 @@ def events(
# should use or operator
if time_after > time_before:
clauses.append(
- (
- reduce(
- operator.or_,
- [(start_hour_fun > time_after), (start_hour_fun < time_before)],
- )
+ reduce(
+ operator.or_,
+ [(start_hour_fun > time_after), (start_hour_fun < time_before)],
)
)
# all other cases should be and operator
else:
- clauses.append((start_hour_fun > time_after))
- clauses.append((start_hour_fun < time_before))
+ clauses.append(start_hour_fun > time_after)
+ clauses.append(start_hour_fun < time_before)
if has_clip is not None:
- clauses.append((Event.has_clip == has_clip))
+ clauses.append(Event.has_clip == has_clip)
if has_snapshot is not None:
- clauses.append((Event.has_snapshot == has_snapshot))
+ clauses.append(Event.has_snapshot == has_snapshot)
if in_progress is not None:
- clauses.append((Event.end_time.is_null(in_progress)))
+ clauses.append(Event.end_time.is_null(in_progress))
if include_thumbnails:
selected_columns.append(Event.thumbnail)
if favorites:
- clauses.append((Event.retain_indefinitely == favorites))
+ clauses.append(Event.retain_indefinitely == favorites)
if max_score is not None:
- clauses.append((Event.data["score"] <= max_score))
+ clauses.append(Event.data["score"] <= max_score)
if min_score is not None:
- clauses.append((Event.data["score"] >= min_score))
+ clauses.append(Event.data["score"] >= min_score)
if max_speed is not None:
- clauses.append((Event.data["average_estimated_speed"] <= max_speed))
+ clauses.append(Event.data["average_estimated_speed"] <= max_speed)
if min_speed is not None:
- clauses.append((Event.data["average_estimated_speed"] >= min_speed))
+ clauses.append(Event.data["average_estimated_speed"] >= min_speed)
if min_length is not None:
- clauses.append(((Event.end_time - Event.start_time) >= min_length))
+ clauses.append((Event.end_time - Event.start_time) >= min_length)
if max_length is not None:
- clauses.append(((Event.end_time - Event.start_time) <= max_length))
+ clauses.append((Event.end_time - Event.start_time) <= max_length)
if is_submitted is not None:
if is_submitted == 0:
- clauses.append((Event.plus_id.is_null()))
+ clauses.append(Event.plus_id.is_null())
elif is_submitted > 0:
- clauses.append((Event.plus_id != ""))
+ clauses.append(Event.plus_id != "")
if event_id is not None:
- clauses.append((Event.id == event_id))
+ clauses.append(Event.id == event_id)
if len(clauses) == 0:
- clauses.append((True))
+ clauses.append(True)
if sort:
if sort == "score_asc":
@@ -387,7 +384,7 @@ def events(
)
def events_explore(
limit: int = 10,
- allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
+ allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
):
# get distinct labels for all events
distinct_labels = (
@@ -515,7 +512,7 @@ async def event_ids(ids: str, request: Request):
def events_search(
request: Request,
params: EventsSearchQueryParams = Depends(),
- allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
+ allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
):
query = params.query
search_type = params.search_type
@@ -595,12 +592,12 @@ def events_search(
filtered = requested.intersection(allowed_cameras)
if not filtered:
return JSONResponse(content=[])
- event_filters.append((Event.camera << list(filtered)))
+ event_filters.append(Event.camera << list(filtered))
else:
- event_filters.append((Event.camera << allowed_cameras))
+ event_filters.append(Event.camera << allowed_cameras)
if labels != "all":
- event_filters.append((Event.label << labels.split(",")))
+ event_filters.append(Event.label << labels.split(","))
if sub_labels != "all":
# use matching so joined sub labels are included
@@ -611,23 +608,23 @@ def events_search(
if "None" in filtered_sub_labels:
filtered_sub_labels.remove("None")
- sub_label_clauses.append((Event.sub_label.is_null()))
+ sub_label_clauses.append(Event.sub_label.is_null())
for label in filtered_sub_labels:
lowered = label.lower()
sub_label_clauses.append(
- (fn.LOWER(Event.sub_label.cast("text")) == lowered)
+ fn.LOWER(Event.sub_label.cast("text")) == lowered
) # include exact matches (case-insensitive)
# include this label when part of a list (LIKE is case-insensitive in sqlite for ASCII)
sub_label_clauses.append(
- (fn.LOWER(Event.sub_label.cast("text")) % f"*{lowered},*")
+ fn.LOWER(Event.sub_label.cast("text")) % f"*{lowered},*"
)
sub_label_clauses.append(
- (fn.LOWER(Event.sub_label.cast("text")) % f"*, {lowered}*")
+ fn.LOWER(Event.sub_label.cast("text")) % f"*, {lowered}*"
)
- event_filters.append((reduce(operator.or_, sub_label_clauses)))
+ event_filters.append(reduce(operator.or_, sub_label_clauses))
if attributes != "all":
# Custom classification results are stored as data[model_name] = result_value
@@ -641,12 +638,12 @@ def events_search(
if "None" in filtered_zones:
filtered_zones.remove("None")
- zone_clauses.append((Event.zones.length() == 0))
+ zone_clauses.append(Event.zones.length() == 0)
for zone in filtered_zones:
- zone_clauses.append((Event.zones.cast("text") % f'*"{zone}"*'))
+ zone_clauses.append(Event.zones.cast("text") % f'*"{zone}"*')
- event_filters.append((reduce(operator.or_, zone_clauses)))
+ event_filters.append(reduce(operator.or_, zone_clauses))
if recognized_license_plate != "all":
filtered_recognized_license_plates = recognized_license_plate.split(",")
@@ -674,43 +671,43 @@ def events_search(
)
recognized_license_plate_clause = reduce(operator.or_, clauses_for_plates)
- event_filters.append((recognized_license_plate_clause))
+ event_filters.append(recognized_license_plate_clause)
if after:
- event_filters.append((Event.start_time > after))
+ event_filters.append(Event.start_time > after)
if before:
- event_filters.append((Event.start_time < before))
+ event_filters.append(Event.start_time < before)
if has_clip is not None:
- event_filters.append((Event.has_clip == has_clip))
+ event_filters.append(Event.has_clip == has_clip)
if has_snapshot is not None:
- event_filters.append((Event.has_snapshot == has_snapshot))
+ event_filters.append(Event.has_snapshot == has_snapshot)
if is_submitted is not None:
if is_submitted == 0:
- event_filters.append((Event.plus_id.is_null()))
+ event_filters.append(Event.plus_id.is_null())
elif is_submitted > 0:
- event_filters.append((Event.plus_id != ""))
+ event_filters.append(Event.plus_id != "")
if min_score is not None and max_score is not None:
- event_filters.append((Event.data["score"].between(min_score, max_score)))
+ event_filters.append(Event.data["score"].between(min_score, max_score))
else:
if min_score is not None:
- event_filters.append((Event.data["score"] >= min_score))
+ event_filters.append(Event.data["score"] >= min_score)
if max_score is not None:
- event_filters.append((Event.data["score"] <= max_score))
+ event_filters.append(Event.data["score"] <= max_score)
if min_speed is not None and max_speed is not None:
event_filters.append(
- (Event.data["average_estimated_speed"].between(min_speed, max_speed))
+ Event.data["average_estimated_speed"].between(min_speed, max_speed)
)
else:
if min_speed is not None:
- event_filters.append((Event.data["average_estimated_speed"] >= min_speed))
+ event_filters.append(Event.data["average_estimated_speed"] >= min_speed)
if max_speed is not None:
- event_filters.append((Event.data["average_estimated_speed"] <= max_speed))
+ event_filters.append(Event.data["average_estimated_speed"] <= max_speed)
if time_range != DEFAULT_TIME_RANGE:
tz_name = params.timezone
@@ -728,17 +725,15 @@ def events_search(
# should use or operator
if time_after > time_before:
event_filters.append(
- (
- reduce(
- operator.or_,
- [(start_hour_fun > time_after), (start_hour_fun < time_before)],
- )
+ reduce(
+ operator.or_,
+ [(start_hour_fun > time_after), (start_hour_fun < time_before)],
)
)
# all other cases should be and operator
else:
- event_filters.append((start_hour_fun > time_after))
- event_filters.append((start_hour_fun < time_before))
+ event_filters.append(start_hour_fun > time_after)
+ event_filters.append(start_hour_fun < time_before)
# Perform semantic search
search_results = {}
@@ -754,6 +749,15 @@ def events_search(
status_code=404,
)
+ if search_event.camera not in allowed_cameras:
+ return JSONResponse(
+ content={
+ "success": False,
+ "message": "Event not found",
+ },
+ status_code=404,
+ )
+
thumb_result = context.search_thumbnail(search_event)
thumb_ids = {result[0]: result[1] for result in thumb_result}
search_results = {
@@ -885,7 +889,7 @@ def events_search(
@router.get("/events/summary", dependencies=[Depends(allow_any_authenticated())])
def events_summary(
params: EventsSummaryQueryParams = Depends(),
- allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
+ allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
):
tz_name = params.timezone
has_clip = params.has_clip
@@ -894,13 +898,13 @@ def events_summary(
clauses = []
if has_clip is not None:
- clauses.append((Event.has_clip == has_clip))
+ clauses.append(Event.has_clip == has_clip)
if has_snapshot is not None:
- clauses.append((Event.has_snapshot == has_snapshot))
+ clauses.append(Event.has_snapshot == has_snapshot)
if len(clauses) == 0:
- clauses.append((True))
+ clauses.append(True)
time_range_query = (
Event.select(
diff --git a/frigate/api/export.py b/frigate/api/export.py
index 714420903b..741a0bb5dc 100644
--- a/frigate/api/export.py
+++ b/frigate/api/export.py
@@ -5,13 +5,15 @@ import logging
import random
import string
import time
+import zipfile
+from collections import deque
+from collections.abc import Iterator
from pathlib import Path
-from typing import List, Optional
import psutil
from fastapi import APIRouter, Depends, Query, Request
-from fastapi.responses import JSONResponse
-from pathvalidate import sanitize_filepath
+from fastapi.responses import JSONResponse, StreamingResponse
+from pathvalidate import sanitize_filename, sanitize_filepath
from peewee import DoesNotExist
from playhouse.shortcuts import model_to_dict
@@ -66,6 +68,7 @@ from frigate.jobs.export import (
from frigate.models import Export, ExportCase, Previews, Recordings
from frigate.record.export import (
DEFAULT_TIME_LAPSE_FFMPEG_ARGS,
+ ChaptersEnum,
PlaybackSourceEnum,
validate_ffmpeg_args,
)
@@ -86,7 +89,7 @@ def _generate_export_id(camera_name: str) -> str:
def _create_export_case_record(
name: str,
- description: Optional[str],
+ description: str | None,
) -> ExportCase:
now = datetime.datetime.fromtimestamp(time.time())
return ExportCase.create(
@@ -98,7 +101,7 @@ def _create_export_case_record(
)
-def _validate_camera_name(request: Request, camera_name: str) -> Optional[JSONResponse]:
+def _validate_camera_name(request: Request, camera_name: str) -> JSONResponse | None:
if camera_name and request.app.frigate_config.cameras.get(camera_name):
return None
@@ -108,7 +111,7 @@ def _validate_camera_name(request: Request, camera_name: str) -> Optional[JSONRe
)
-def _validate_export_case(export_case_id: Optional[str]) -> Optional[JSONResponse]:
+def _validate_export_case(export_case_id: str | None) -> JSONResponse | None:
if export_case_id is None:
return None
@@ -124,8 +127,17 @@ def _validate_export_case(export_case_id: Optional[str]) -> Optional[JSONRespons
def _sanitize_existing_image(
- image_path: Optional[str],
-) -> tuple[Optional[str], Optional[JSONResponse]]:
+ image_path: str | None,
+) -> tuple[str | None, JSONResponse | None]:
+ # sanitize_filepath normalizes "\" to "/" but leaves ".." intact, so a path
+ # like "clips\..\..\etc/passwd" passes the CLIPS_DIR prefix check yet still
+ # escapes the directory once resolved. A valid snapshot path never uses "..".
+ if image_path and ".." in image_path:
+ return None, JSONResponse(
+ content={"success": False, "message": "Invalid image path"},
+ status_code=400,
+ )
+
existing_image = sanitize_filepath(image_path) if image_path else None
if existing_image and not existing_image.startswith(CLIPS_DIR):
@@ -142,7 +154,7 @@ def _validate_export_source(
start_time: float,
end_time: float,
playback_source: PlaybackSourceEnum,
-) -> Optional[str]:
+) -> str | None:
if playback_source == PlaybackSourceEnum.recordings:
recordings_count = (
Recordings.select()
@@ -245,13 +257,14 @@ def _build_export_job(
camera_name: str,
start_time: float,
end_time: float,
- friendly_name: Optional[str],
- existing_image: Optional[str],
+ friendly_name: str | None,
+ existing_image: str | None,
playback_source: PlaybackSourceEnum,
- export_case_id: Optional[str],
- ffmpeg_input_args: Optional[str] = None,
- ffmpeg_output_args: Optional[str] = None,
+ export_case_id: str | None,
+ ffmpeg_input_args: str | None = None,
+ ffmpeg_output_args: str | None = None,
cpu_fallback: bool = False,
+ chapters: ChaptersEnum | None = None,
) -> ExportJob:
return ExportJob(
id=_generate_export_id(camera_name),
@@ -265,6 +278,7 @@ def _build_export_job(
ffmpeg_input_args=ffmpeg_input_args,
ffmpeg_output_args=ffmpeg_output_args,
cpu_fallback=cpu_fallback,
+ chapters=chapters,
)
@@ -288,11 +302,11 @@ def _export_case_to_dict(case: ExportCase) -> dict[str, object]:
Returns a list of exports ordered by date (most recent first).""",
)
def get_exports(
- allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
- export_case_id: Optional[str] = None,
- cameras: Optional[str] = Query(default="all"),
- start_date: Optional[float] = None,
- end_date: Optional[float] = None,
+ allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
+ export_case_id: str | None = None,
+ cameras: str | None = Query(default="all"),
+ start_date: float | None = None,
+ end_date: float | None = None,
):
query = Export.select().where(Export.camera << allowed_cameras)
@@ -361,6 +375,136 @@ def get_export_case(case_id: str):
)
+_ZIP_STREAM_CHUNK_SIZE = 1024 * 1024 # 1 MiB
+
+
+class _StreamingZipBuffer:
+ """File-like sink for ZipFile that exposes written bytes via drain().
+
+ ZipFile writes synchronously into this buffer; the generator drains the
+ queue between writes so StreamingResponse can yield bytes without
+ materializing the whole archive in memory.
+ """
+
+ def __init__(self) -> None:
+ self._queue: deque[bytes] = deque()
+ self._offset = 0
+
+ def write(self, data: bytes) -> int:
+ if data:
+ self._queue.append(bytes(data))
+ self._offset += len(data)
+ return len(data)
+
+ def tell(self) -> int:
+ return self._offset
+
+ def flush(self) -> None:
+ pass
+
+ def drain(self) -> Iterator[bytes]:
+ while self._queue:
+ yield self._queue.popleft()
+
+
+def _unique_archive_name(export: Export, used: set[str]) -> str:
+ base = sanitize_filename(export.name) if export.name else None
+ if not base:
+ base = f"{export.camera}_{int(export.date)}"
+
+ candidate = f"{base}.mp4"
+ counter = 1
+ while candidate in used:
+ candidate = f"{base}_{counter}.mp4"
+ counter += 1
+
+ used.add(candidate)
+ return candidate
+
+
+def _stream_case_archive(exports: list[Export]) -> Iterator[bytes]:
+ """Yield bytes of a zip archive built from the given exports' mp4 files."""
+ buffer = _StreamingZipBuffer()
+ used_names: set[str] = set()
+
+ # ZIP_STORED: mp4 is already compressed, recompressing wastes CPU for ~0% size win.
+ with zipfile.ZipFile(
+ buffer,
+ mode="w",
+ compression=zipfile.ZIP_STORED,
+ allowZip64=True,
+ ) as archive:
+ for export in exports:
+ source = Path(export.video_path)
+ if not source.exists():
+ continue
+
+ arcname = _unique_archive_name(export, used_names)
+
+ with (
+ archive.open(arcname, mode="w", force_zip64=True) as entry,
+ source.open("rb") as src,
+ ):
+ while True:
+ chunk = src.read(_ZIP_STREAM_CHUNK_SIZE)
+ if not chunk:
+ break
+
+ entry.write(chunk)
+ yield from buffer.drain()
+
+ yield from buffer.drain()
+
+ yield from buffer.drain()
+
+
+@router.get(
+ "/cases/{case_id}/download",
+ dependencies=[Depends(allow_any_authenticated())],
+ summary="Download export case as zip",
+ description="Streams a zip archive containing every completed export's mp4 for the given case.",
+)
+def download_export_case(
+ case_id: str,
+ allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
+):
+ try:
+ case = ExportCase.get(ExportCase.id == case_id)
+ except DoesNotExist:
+ return JSONResponse(
+ content={"success": False, "message": "Export case not found"},
+ status_code=404,
+ )
+
+ exports = list(
+ Export.select()
+ .where(
+ Export.export_case == case_id,
+ ~Export.in_progress,
+ Export.camera << allowed_cameras,
+ )
+ .order_by(Export.date.asc())
+ )
+
+ if not exports:
+ return JSONResponse(
+ content={"success": False, "message": "No exports available to download."},
+ status_code=404,
+ )
+
+ archive_base = sanitize_filename(case.name) if case.name else ""
+ if not archive_base:
+ archive_base = case_id
+
+ return StreamingResponse(
+ _stream_case_archive(exports),
+ media_type="application/zip",
+ headers={
+ "Content-Disposition": f'attachment; filename="{archive_base}.zip"',
+ },
+ )
+
+
@router.patch(
"/cases/{case_id}",
response_model=GenericResponse,
@@ -436,7 +580,7 @@ def delete_export_case(case_id: str, request: Request, delete_exports: bool = Fa
)
def get_active_export_jobs(
request: Request,
- allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
+ allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
):
jobs = list_active_export_jobs(request.app.frigate_config)
return JSONResponse(
@@ -478,7 +622,7 @@ async def get_export_job_status(export_id: str, request: Request):
def export_recordings_batch(
request: Request,
body: BatchExportBody,
- allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
+ allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
current_user: dict = Depends(get_current_user),
):
if isinstance(current_user, JSONResponse):
@@ -518,7 +662,7 @@ def export_recordings_batch(
# Sanitize each item's image_path up front. A bad path in any item
# kills the whole request, consistent with single-export behavior.
- sanitized_images: list[Optional[str]] = []
+ sanitized_images: list[str | None] = []
for item in body.items:
existing_image, image_validation_error = _sanitize_existing_image(
item.image_path
@@ -569,7 +713,7 @@ def export_recordings_batch(
export_case_id = export_case.id
export_ids: list[str] = []
- results: list[dict[str, Optional[str] | bool | int]] = []
+ results: list[dict[str, str | None | bool | int]] = []
for index, item in enumerate(body.items):
if index in item_errors:
results.append(
@@ -593,6 +737,9 @@ def export_recordings_batch(
sanitized_images[index],
PlaybackSourceEnum.recordings,
export_case_id,
+ chapters=request.app.frigate_config.cameras[
+ item.camera
+ ].record.export.chapters,
)
try:
start_export_job(request.app.frigate_config, export_job)
@@ -671,6 +818,14 @@ def export_recording(
export_case_id = body.export_case_id
+ # a chapters value in the request body overrides the camera's export config
+ camera_config = request.app.frigate_config.cameras[camera_name]
+ chapters = (
+ body.chapters
+ if body.chapters is not None
+ else camera_config.record.export.chapters
+ )
+
# Attaching to an existing case requires admin. Single-export for
# cameras the user can access is otherwise non-admin; we only gate
# the case-attachment side effect.
@@ -707,6 +862,7 @@ def export_recording(
existing_image,
playback_source,
export_case_id,
+ chapters=chapters,
)
try:
start_export_job(request.app.frigate_config, export_job)
diff --git a/frigate/api/fastapi_app.py b/frigate/api/fastapi_app.py
index f201ab7135..5e8323b58f 100644
--- a/frigate/api/fastapi_app.py
+++ b/frigate/api/fastapi_app.py
@@ -1,6 +1,6 @@
+import asyncio
import logging
import re
-from typing import Optional
from fastapi import Depends, FastAPI, Request
from fastapi.responses import JSONResponse
@@ -36,7 +36,7 @@ from frigate.comms.event_metadata_updater import (
from frigate.config import FrigateConfig
from frigate.config.camera.updater import CameraConfigUpdatePublisher
from frigate.config.profile_manager import ProfileManager
-from frigate.debug_replay import DebugReplayManager
+from frigate.debug_replay import DebugReplayManager, debug_replay_auto_stop_watchdog
from frigate.embeddings import EmbeddingsContext
from frigate.genai import GenAIClientManager
from frigate.ptz.onvif import OnvifController
@@ -63,7 +63,7 @@ class RemoteUserPlugin(Plugin):
def create_fastapi_app(
frigate_config: FrigateConfig,
database: SqliteQueueDatabase,
- embeddings: Optional[EmbeddingsContext],
+ embeddings: EmbeddingsContext | None,
detected_frames_processor,
storage_maintainer: StorageMaintainer,
onvif: OnvifController,
@@ -71,8 +71,8 @@ def create_fastapi_app(
event_metadata_updater: EventMetadataPublisher,
config_publisher: CameraConfigUpdatePublisher,
replay_manager: DebugReplayManager,
- dispatcher: Optional[Dispatcher] = None,
- profile_manager: Optional[ProfileManager] = None,
+ dispatcher: Dispatcher | None = None,
+ profile_manager: ProfileManager | None = None,
enforce_default_admin: bool = True,
):
logger.info("Starting FastAPI app")
@@ -116,6 +116,11 @@ def create_fastapi_app(
@app.on_event("startup")
async def startup():
logger.info("FastAPI started")
+ asyncio.create_task(
+ debug_replay_auto_stop_watchdog(
+ replay_manager, frigate_config, config_publisher
+ )
+ )
# Rate limiter (used for login endpoint)
if frigate_config.auth.failed_login_rate_limit is None:
diff --git a/frigate/api/media.py b/frigate/api/media.py
index 489c008b41..8d56475c59 100644
--- a/frigate/api/media.py
+++ b/frigate/api/media.py
@@ -7,7 +7,7 @@ import math
import os
import subprocess as sp
import time
-from datetime import datetime, timedelta, timezone
+from datetime import UTC, datetime, timedelta
from pathlib import Path as FilePath
from typing import Any
from urllib.parse import unquote
@@ -60,6 +60,19 @@ logger = logging.getLogger(__name__)
router = APIRouter(tags=[Tags.media])
+def _resolve_cache_age(max_cache_age: int) -> int:
+ """Return max_cache_age as an int.
+
+ When a media handler is invoked directly by another handler instead of
+ through its route, FastAPI doesn't resolve the Query() default and
+ max_cache_age arrives as the Query object; fall back to its int default.
+ """
+ if isinstance(max_cache_age, int):
+ return max_cache_age
+
+ return max_cache_age.default
+
+
@router.get("/{camera_name}", dependencies=[Depends(require_camera_access)])
async def mjpeg_feed(
request: Request,
@@ -174,12 +187,10 @@ async def latest_frame(
}
quality_params = get_image_quality_params(extension.value, params.quality)
- if camera_name in request.app.frigate_config.cameras:
+ camera_config = request.app.frigate_config.cameras.get(camera_name)
+ if camera_config is not None:
frame = frame_processor.get_current_frame(camera_name, draw_options)
- retry_interval = float(
- request.app.frigate_config.cameras.get(camera_name).ffmpeg.retry_interval
- or 10
- )
+ retry_interval = float(camera_config.ffmpeg.retry_interval or 10)
is_offline = False
if frame is None or datetime.now().timestamp() > (
@@ -303,10 +314,8 @@ async def get_snapshot_from_recording(
Recordings.start_time,
)
.where(
- (
- (frame_time >= Recordings.start_time)
- & (frame_time <= Recordings.end_time)
- )
+ (frame_time >= Recordings.start_time)
+ & (frame_time <= Recordings.end_time)
)
.where(Recordings.camera == camera_name)
.order_by(Recordings.start_time.desc())
@@ -324,10 +333,8 @@ async def get_snapshot_from_recording(
Recordings.start_time,
)
.where(
- (
- (frame_time >= Recordings.start_time)
- & (frame_time <= Recordings.end_time)
- )
+ (frame_time >= Recordings.start_time)
+ & (frame_time <= Recordings.end_time)
)
.where(Recordings.camera == camera_name)
.order_by(Recordings.start_time.desc())
@@ -387,10 +394,7 @@ async def submit_recording_snapshot_to_plus(
Recordings.start_time,
)
.where(
- (
- (frame_time >= Recordings.start_time)
- & (frame_time <= Recordings.end_time)
- )
+ (frame_time >= Recordings.start_time) & (frame_time <= Recordings.end_time)
)
.where(Recordings.camera == camera_name)
.order_by(Recordings.start_time.desc())
@@ -415,7 +419,9 @@ async def submit_recording_snapshot_to_plus(
)
nd = cv2.imdecode(np.frombuffer(image_data, dtype=np.int8), cv2.IMREAD_COLOR)
- request.app.frigate_config.plus_api.upload_image(nd, camera_name)
+ await asyncio.to_thread(
+ request.app.frigate_config.plus_api.upload_image, nd, camera_name
+ )
return JSONResponse(
content={
@@ -706,7 +712,7 @@ async def vod_hour(
):
parts = year_month.split("-")
start_date = (
- datetime(int(parts[0]), int(parts[1]), day, hour, tzinfo=timezone.utc)
+ datetime(int(parts[0]), int(parts[1]), day, hour, tzinfo=UTC)
- datetime.now(pytz.timezone(tz_name.replace(",", "/"))).utcoffset()
)
end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1)
@@ -938,7 +944,7 @@ async def event_thumbnail(
thumbnail_bytes,
media_type=extension.get_mime_type(),
headers={
- "Cache-Control": f"private, max-age={max_cache_age}"
+ "Cache-Control": f"private, max-age={_resolve_cache_age(max_cache_age)}"
if event_complete
else "no-store",
},
@@ -1272,14 +1278,14 @@ async def event_preview(request: Request, event_id: str):
end_ts = start_ts + (
min(event.end_time - event.start_time, 20) if event.end_time else 20
)
- return preview_gif(request, event.camera, start_ts, end_ts)
+ return await preview_gif(request, event.camera, start_ts, end_ts)
@router.get(
"/{camera_name}/start/{start_ts}/end/{end_ts}/preview.gif",
dependencies=[Depends(require_camera_access)],
)
-def preview_gif(
+async def preview_gif(
request: Request,
camera_name: str,
start_ts: float,
@@ -1342,7 +1348,8 @@ def preview_gif(
"-",
]
- process = sp.run(
+ process = await asyncio.to_thread(
+ sp.run,
ffmpeg_cmd,
capture_output=True,
)
@@ -1368,12 +1375,17 @@ def preview_gif(
file_start = f"preview_{camera_name}-"
start_file = f"{file_start}{start_ts}.{PREVIEW_FRAME_TYPE}"
end_file = f"{file_start}{end_ts}.{PREVIEW_FRAME_TYPE}"
+
+ camera_files = [
+ entry.name
+ for entry in os.scandir(preview_dir)
+ if entry.name.startswith(file_start)
+ ]
+ camera_files.sort()
+
selected_previews = []
- for file in sorted(os.listdir(preview_dir)):
- if not file.startswith(file_start):
- continue
-
+ for file in camera_files:
if file < start_file:
continue
@@ -1416,7 +1428,8 @@ def preview_gif(
"-",
]
- process = sp.run(
+ process = await asyncio.to_thread(
+ sp.run,
ffmpeg_cmd,
input=str.encode("\n".join(selected_previews)),
capture_output=True,
@@ -1435,7 +1448,7 @@ def preview_gif(
gif_bytes,
media_type="image/gif",
headers={
- "Cache-Control": f"private, max-age={max_cache_age}",
+ "Cache-Control": f"private, max-age={_resolve_cache_age(max_cache_age)}",
"Content-Type": "image/gif",
},
)
@@ -1445,7 +1458,7 @@ def preview_gif(
"/{camera_name}/start/{start_ts}/end/{end_ts}/preview.mp4",
dependencies=[Depends(require_camera_access)],
)
-def preview_mp4(
+async def preview_mp4(
request: Request,
camera_name: str,
start_ts: float,
@@ -1525,7 +1538,8 @@ def preview_mp4(
path,
]
- process = sp.run(
+ process = await asyncio.to_thread(
+ sp.run,
ffmpeg_cmd,
capture_output=True,
)
@@ -1550,12 +1564,17 @@ def preview_mp4(
file_start = f"preview_{camera_name}-"
start_file = f"{file_start}{start_ts}.{PREVIEW_FRAME_TYPE}"
end_file = f"{file_start}{end_ts}.{PREVIEW_FRAME_TYPE}"
+
+ camera_files = [
+ entry.name
+ for entry in os.scandir(preview_dir)
+ if entry.name.startswith(file_start)
+ ]
+ camera_files.sort()
+
selected_previews = []
- for file in sorted(os.listdir(preview_dir)):
- if not file.startswith(file_start):
- continue
-
+ for file in camera_files:
if file < start_file:
continue
@@ -1596,7 +1615,8 @@ def preview_mp4(
path,
]
- process = sp.run(
+ process = await asyncio.to_thread(
+ sp.run,
ffmpeg_cmd,
input=str.encode("\n".join(selected_previews)),
capture_output=True,
@@ -1611,7 +1631,7 @@ def preview_mp4(
headers = {
"Content-Description": "File Transfer",
- "Cache-Control": f"private, max-age={max_cache_age}",
+ "Cache-Control": f"private, max-age={_resolve_cache_age(max_cache_age)}",
"Content-Type": "video/mp4",
"Content-Length": str(os.path.getsize(path)),
# nginx: https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ignore_headers
@@ -1649,9 +1669,9 @@ async def review_preview(
)
if format == "gif":
- return preview_gif(request, review.camera, start_ts, end_ts)
+ return await preview_gif(request, review.camera, start_ts, end_ts)
else:
- return preview_mp4(request, review.camera, start_ts, end_ts)
+ return await preview_mp4(request, review.camera, start_ts, end_ts)
@router.get(
diff --git a/frigate/api/media_auth.py b/frigate/api/media_auth.py
new file mode 100644
index 0000000000..0630e3604f
--- /dev/null
+++ b/frigate/api/media_auth.py
@@ -0,0 +1,290 @@
+"""URI-aware authorization for nginx-served static media.
+
+The `/auth` endpoint (used as nginx `auth_request` target) calls into this
+module to classify the requested URI from the `X-Original-URL` header and, for
+camera-scoped resources, decide whether the current role may access them.
+
+Without this, `auth_request` only verifies the JWT — every authenticated user
+could read clips, recordings, and exports for *any* camera, bypassing the
+per-camera authorization the regular API enforces via `require_camera_access`.
+"""
+
+from __future__ import annotations
+
+import logging
+import os
+from enum import Enum
+from urllib.parse import unquote, urlparse
+
+from peewee import DoesNotExist
+
+from frigate.config import FrigateConfig
+from frigate.const import EXPORT_DIR
+from frigate.models import Export, User
+
+logger = logging.getLogger(__name__)
+
+
+class MediaAuthResolution(str, Enum):
+ """Classification of an `X-Original-URL` path for media-auth purposes."""
+
+ CAMERA = "camera"
+ ADMIN_ONLY = "admin_only"
+ LISTING_MULTI_CAMERA = "listing_multi_camera"
+ LISTING_NEUTRAL = "listing_neutral"
+ # Under a recognized media root (/clips, /recordings, /exports) but
+ # unclassifiable (unknown subtree, no matching DB row, DB error).
+ # Restricted users are denied; admins/full-access roles are allowed
+ # (nginx will likely return 404 if the file genuinely doesn't exist).
+ UNRESOLVED_MEDIA = "unresolved_media"
+ # Not a media URI at all (e.g. /api/events, /login).
+ UNKNOWN = "unknown"
+
+
+def extract_path(original_url: str | None) -> str | None:
+ """Return the decoded path component of nginx's `X-Original-URL` header.
+
+ nginx forwards the *raw* request URI (with `..` segments intact) via
+ `$request_uri`. nginx normalizes the path before serving the file, so a
+ request like `/recordings/.../allowed_cam/../forbidden_cam/file.mp4`
+ would (1) parse as the allowed camera in our auth check, (2) be served
+ as the forbidden camera by nginx. To close the bypass we reject any URI
+ whose path contains `.` or `..` segments outright.
+ """
+ if not original_url:
+ return None
+
+ parsed = urlparse(original_url)
+ raw_path = parsed.path or original_url
+ decoded = unquote(raw_path)
+ if not decoded:
+ return None
+
+ if not decoded.startswith("/"):
+ decoded = "/" + decoded
+
+ segments = decoded.split("/")
+ if ".." in segments or "." in segments:
+ return None
+
+ return decoded
+
+
+def resolve_media_uri(
+ uri: str, frigate_config: FrigateConfig | None = None
+) -> tuple[MediaAuthResolution, str | None]:
+ """Classify a URI and return the owning camera if applicable.
+
+ `frigate_config` is used to disambiguate clip/review filenames whose
+ camera name contains hyphens by matching against the longest configured
+ camera-name prefix.
+ """
+ if not uri:
+ return MediaAuthResolution.UNKNOWN, None
+
+ parts = [p for p in uri.split("/") if p]
+ if not parts:
+ return MediaAuthResolution.UNKNOWN, None
+
+ root = parts[0]
+ if root == "recordings":
+ return _resolve_recording(parts)
+ if root == "clips":
+ return _resolve_clip(parts, frigate_config)
+ if root == "exports":
+ return _resolve_export(parts)
+
+ return MediaAuthResolution.UNKNOWN, None
+
+
+def _resolve_recording(
+ parts: list[str],
+) -> tuple[MediaAuthResolution, str | None]:
+ # /recordings → neutral
+ # /recordings/{date} → neutral
+ # /recordings/{date}/{hour} → multi-camera listing
+ # /recordings/{date}/{hour}/{cam}/... → camera
+ if len(parts) <= 2:
+ return MediaAuthResolution.LISTING_NEUTRAL, None
+ if len(parts) == 3:
+ return MediaAuthResolution.LISTING_MULTI_CAMERA, None
+ return MediaAuthResolution.CAMERA, parts[3]
+
+
+def _resolve_clip(
+ parts: list[str], frigate_config: FrigateConfig | None
+) -> tuple[MediaAuthResolution, str | None]:
+ # /clips → multi-camera listing
+ # /clips/thumbs/{cam}/... → camera
+ # /clips/previews/{cam}/... → camera
+ # /clips/review/thumb-{cam}-{review_id}.webp → camera (parsed)
+ # /clips/faces/... → admin-only
+ # /clips/genai-requests/... → admin-only
+ # /clips/preview_restart_cache/... → admin-only
+ # /clips/{model}/train|dataset/... → admin-only
+ # /clips/{cam}-{event_id}[-clean].{ext} → camera (parsed)
+ # other /clips/{subdir}/... → unresolved (deny restricted)
+ if len(parts) == 1:
+ return MediaAuthResolution.LISTING_MULTI_CAMERA, None
+
+ second = parts[1]
+
+ if second in ("thumbs", "previews"):
+ if len(parts) == 2:
+ return MediaAuthResolution.LISTING_MULTI_CAMERA, None
+ return MediaAuthResolution.CAMERA, parts[2]
+
+ if second == "review":
+ if len(parts) == 2:
+ return MediaAuthResolution.LISTING_MULTI_CAMERA, None
+ camera = _camera_from_thumb_filename(parts[2], frigate_config)
+ if camera:
+ return MediaAuthResolution.CAMERA, camera
+ return MediaAuthResolution.UNRESOLVED_MEDIA, None
+
+ if second in ("faces", "genai-requests", "preview_restart_cache"):
+ return MediaAuthResolution.ADMIN_ONLY, None
+
+ if len(parts) >= 3 and parts[2] in ("train", "dataset"):
+ return MediaAuthResolution.ADMIN_ONLY, None
+
+ if len(parts) == 2:
+ camera = _camera_from_clip_filename(second, frigate_config)
+ if camera:
+ return MediaAuthResolution.CAMERA, camera
+ return MediaAuthResolution.UNRESOLVED_MEDIA, None
+
+ return MediaAuthResolution.UNRESOLVED_MEDIA, None
+
+
+def _longest_prefix_camera(
+ stem: str, frigate_config: FrigateConfig | None
+) -> str | None:
+ if frigate_config is None:
+ return None
+ for cam in sorted(frigate_config.cameras.keys(), key=len, reverse=True):
+ if stem.startswith(cam + "-"):
+ return cam
+ return None
+
+
+def _camera_from_clip_filename(
+ filename: str, frigate_config: FrigateConfig | None
+) -> str | None:
+ """Match a flat clip filename `{camera}-{event_id}[-clean].{ext}` against
+ configured camera names. Longest-prefix wins so camera names containing
+ hyphens (e.g. `front-door`) resolve correctly.
+ """
+ dot = filename.rfind(".")
+ stem = filename[:dot] if dot > 0 else filename
+ return _longest_prefix_camera(stem, frigate_config)
+
+
+def _camera_from_thumb_filename(
+ filename: str, frigate_config: FrigateConfig | None
+) -> str | None:
+ """Match a review thumbnail filename `thumb-{camera}-{review_id}.webp`."""
+ if not filename.startswith("thumb-"):
+ return None
+ dot = filename.rfind(".")
+ stem = filename[len("thumb-") : dot] if dot > 0 else filename[len("thumb-") :]
+ return _longest_prefix_camera(stem, frigate_config)
+
+
+def _resolve_export(
+ parts: list[str],
+) -> tuple[MediaAuthResolution, str | None]:
+ # /exports → multi-camera listing
+ # /exports/{filename}.mp4 → camera (DB lookup by exact path)
+ if len(parts) == 1:
+ return MediaAuthResolution.LISTING_MULTI_CAMERA, None
+ if len(parts) != 2:
+ return MediaAuthResolution.UNRESOLVED_MEDIA, None
+
+ filename = parts[1]
+ full_path = os.path.join(EXPORT_DIR, filename)
+ try:
+ export = Export.get(Export.video_path == full_path)
+ return MediaAuthResolution.CAMERA, export.camera
+ except DoesNotExist:
+ return MediaAuthResolution.UNRESOLVED_MEDIA, None
+ except Exception as e:
+ logger.warning("Export DB lookup failed for %s: %s", filename, e)
+ return MediaAuthResolution.UNRESOLVED_MEDIA, None
+
+
+def check_camera_access(role: str, camera: str, frigate_config: FrigateConfig) -> bool:
+ """Return True iff `role` may access `camera`.
+
+ Mirrors the gating logic in `require_camera_access`: admin and any role
+ without a non-empty allow-list bypass the check.
+ """
+ if role == "admin":
+ return True
+
+ roles_dict = frigate_config.auth.roles
+ if not roles_dict.get(role):
+ return True
+
+ all_camera_names = set(frigate_config.cameras.keys())
+ allowed = User.get_allowed_cameras(role, roles_dict, all_camera_names)
+ return camera in allowed
+
+
+def is_role_restricted(role: str, frigate_config: FrigateConfig) -> bool:
+ """True if `role` has a non-empty allow-list (i.e. not full-access)."""
+ if role == "admin":
+ return False
+ return bool(frigate_config.auth.roles.get(role))
+
+
+def deny_response_for_media_uri(
+ original_url: str | None, role: str | None, frigate_config: FrigateConfig
+) -> int | None:
+ """Decide whether the current role should be blocked from `original_url`.
+
+ Returns an HTTP status code (403) when access should be denied, or `None`
+ when the request is allowed.
+ """
+ if not original_url:
+ return None
+
+ path = extract_path(original_url)
+
+ # `extract_path` returns None for URIs containing `.` or `..` segments.
+ # For media-root URIs that's a traversal attempt — deny outright. For
+ # non-media URIs, pass through (nginx / the backend handle them).
+ if path is None:
+ raw = urlparse(original_url).path or original_url
+ decoded = unquote(raw)
+ first = decoded.lstrip("/").split("/", 1)[0] if decoded else ""
+ if first in ("clips", "recordings", "exports"):
+ return 403
+ return None
+
+ resolution, camera = resolve_media_uri(path, frigate_config)
+ if resolution == MediaAuthResolution.UNKNOWN:
+ return None
+
+ if not role or role == "admin":
+ return None
+
+ if not is_role_restricted(role, frigate_config):
+ return None
+
+ if resolution == MediaAuthResolution.LISTING_NEUTRAL:
+ return None
+
+ if resolution in (
+ MediaAuthResolution.LISTING_MULTI_CAMERA,
+ MediaAuthResolution.ADMIN_ONLY,
+ MediaAuthResolution.UNRESOLVED_MEDIA,
+ ):
+ return 403
+
+ if resolution == MediaAuthResolution.CAMERA:
+ if camera and check_camera_access(role, camera, frigate_config):
+ return None
+ return 403
+
+ return 403
diff --git a/frigate/api/motion_search.py b/frigate/api/motion_search.py
index 09bf8026da..8a5f175457 100644
--- a/frigate/api/motion_search.py
+++ b/frigate/api/motion_search.py
@@ -1,7 +1,7 @@
"""Motion search API for detecting changes within a region of interest."""
import logging
-from typing import Any, List, Optional
+from typing import Any
from fastapi import APIRouter, Depends, Request
from fastapi.responses import JSONResponse
@@ -26,7 +26,7 @@ class MotionSearchRequest(BaseModel):
start_time: float = Field(description="Start timestamp for the search range")
end_time: float = Field(description="End timestamp for the search range")
- polygon_points: List[List[float]] = Field(
+ polygon_points: list[list[float]] = Field(
description="List of [x, y] normalized coordinates (0-1) defining the ROI polygon"
)
threshold: int = Field(
@@ -41,12 +41,6 @@ class MotionSearchRequest(BaseModel):
le=100.0,
description="Minimum change area as a percentage of the ROI",
)
- frame_skip: int = Field(
- default=5,
- ge=1,
- le=30,
- description="Process every Nth frame (1=all frames, 5=every 5th frame)",
- )
parallel: bool = Field(
default=False,
description="Enable parallel scanning across segments",
@@ -93,10 +87,12 @@ class MotionSearchStatusResponse(BaseModel):
success: bool
message: str
status: str # "queued", "running", "success", "failed", or "cancelled"
- results: Optional[List[MotionSearchResult]] = None
- total_frames_processed: Optional[int] = None
- error_message: Optional[str] = None
- metrics: Optional[MotionSearchMetricsResponse] = None
+ results: list[MotionSearchResult] | None = None
+ total_frames_processed: int | None = None
+ error_message: str | None = None
+ metrics: MotionSearchMetricsResponse | None = None
+ scanning_timestamp: float | None = None
+ progress: float | None = None
@router.post(
@@ -151,7 +147,6 @@ async def start_motion_search(
polygon_points=body.polygon_points,
threshold=body.threshold,
min_area=body.min_area,
- frame_skip=body.frame_skip,
parallel=body.parallel,
max_results=body.max_results,
)
@@ -231,6 +226,9 @@ async def get_motion_search_status_endpoint(
if job.metrics:
response_content["metrics"] = job.metrics.to_dict()
+ response_content["scanning_timestamp"] = job.scanning_timestamp
+ response_content["progress"] = job.progress
+
return JSONResponse(content=response_content)
diff --git a/frigate/api/preview.py b/frigate/api/preview.py
index a5e30764de..1047591434 100644
--- a/frigate/api/preview.py
+++ b/frigate/api/preview.py
@@ -1,8 +1,10 @@
"""Preview apis."""
+import bisect
import logging
import os
-from datetime import datetime, timedelta, timezone
+import threading
+from datetime import UTC, datetime, timedelta
import pytz
from fastapi import APIRouter, Depends, HTTPException
@@ -123,7 +125,7 @@ def preview_hour(
"""Get all mp4 previews relevant for time period given the timezone"""
parts = year_month.split("-")
start_date = (
- datetime(int(parts[0]), int(parts[1]), int(day), int(hour), tzinfo=timezone.utc)
+ datetime(int(parts[0]), int(parts[1]), int(day), int(hour), tzinfo=UTC)
- datetime.now(pytz.timezone(tz_name.replace(",", "/"))).utcoffset()
)
end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1)
@@ -133,6 +135,32 @@ def preview_hour(
return preview_ts(camera_name, start_ts, end_ts, allowed_cameras)
+# cache one sorted listing of the shared preview_frames dir
+_preview_listing_lock = threading.Lock()
+_preview_listing_cache: tuple[float, list[str]] = (-1.0, [])
+
+
+def _get_preview_frame_listing(preview_dir: str) -> list[str]:
+ """Return the sorted preview_frames listing, cached until the dir changes."""
+ global _preview_listing_cache
+
+ # mtime bumps when a frame is added or removed, invalidating the cache
+ mtime = os.stat(preview_dir).st_mtime
+ cached_mtime, files = _preview_listing_cache
+ if mtime == cached_mtime:
+ return files
+
+ with _preview_listing_lock:
+ # another thread may have refreshed the cache while we waited
+ cached_mtime, files = _preview_listing_cache
+ if mtime == cached_mtime:
+ return files
+
+ files = sorted(entry.name for entry in os.scandir(preview_dir))
+ _preview_listing_cache = (mtime, files)
+ return files
+
+
@router.get(
"/preview/{camera_name}/start/{start_ts}/end/{end_ts}/frames",
response_model=PreviewFramesResponse,
@@ -148,19 +176,16 @@ def get_preview_frames_from_cache(camera_name: str, start_ts: float, end_ts: flo
file_start = f"preview_{camera_name}-"
start_file = f"{file_start}{start_ts}.{PREVIEW_FRAME_TYPE}"
end_file = f"{file_start}{end_ts}.{PREVIEW_FRAME_TYPE}"
- selected_previews = []
- for file in sorted(os.listdir(preview_dir)):
- if not file.startswith(file_start):
- continue
+ files = _get_preview_frame_listing(preview_dir)
- if file < start_file:
- continue
-
- if file > end_file:
- break
-
- selected_previews.append(file)
+ # a camera's frames form a contiguous slice of the sorted listing;
+ # bisect locates it without scanning the whole directory
+ left = bisect.bisect_left(files, start_file)
+ right = bisect.bisect_right(files, end_file)
+ selected_previews = [
+ file for file in files[left:right] if file.startswith(file_start)
+ ]
return JSONResponse(
content=selected_previews,
diff --git a/frigate/api/record.py b/frigate/api/record.py
index 4ab4b0af16..5db257f482 100644
--- a/frigate/api/record.py
+++ b/frigate/api/record.py
@@ -5,7 +5,6 @@ import logging
from datetime import datetime, timedelta
from functools import reduce
from pathlib import Path
-from typing import List
from urllib.parse import unquote
from fastapi import APIRouter, Depends, Request
@@ -35,7 +34,7 @@ logger = logging.getLogger(__name__)
router = APIRouter(tags=[Tags.recordings])
-@router.get("/recordings/storage", dependencies=[Depends(allow_any_authenticated())])
+@router.get("/recordings/storage", dependencies=[Depends(require_role(["admin"]))])
def get_recordings_storage_usage(request: Request):
recording_stats = request.app.stats_emitter.get_latest_stats()["service"][
"storage"
@@ -63,7 +62,7 @@ def get_recordings_storage_usage(request: Request):
def all_recordings_summary(
request: Request,
params: MediaRecordingsSummaryQueryParams = Depends(),
- allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
+ allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
):
"""Returns true/false by day indicating if recordings exist"""
@@ -263,18 +262,18 @@ async def recordings(
async def no_recordings(
request: Request,
params: MediaRecordingsAvailabilityQueryParams = Depends(),
- allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
+ allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
):
"""Get time ranges with no recordings."""
cameras = params.cameras
if cameras != "all":
requested = set(unquote(cameras).split(","))
- filtered = requested.intersection(allowed_cameras)
- if not filtered:
- return JSONResponse(content=[])
- cameras = ",".join(filtered)
+ camera_list = list(requested.intersection(allowed_cameras))
else:
- cameras = allowed_cameras
+ camera_list = list(allowed_cameras)
+
+ if not camera_list:
+ return JSONResponse(content=[])
before = params.before or datetime.datetime.now().timestamp()
after = (
@@ -283,12 +282,10 @@ async def no_recordings(
)
scale = params.scale
- clauses = [(Recordings.end_time >= after) & (Recordings.start_time <= before)]
- if cameras != "all":
- camera_list = cameras.split(",")
- clauses.append((Recordings.camera << camera_list))
- else:
- camera_list = allowed_cameras
+ clauses = [
+ (Recordings.end_time >= after) & (Recordings.start_time <= before),
+ (Recordings.camera << camera_list),
+ ]
# Get recording start times
data: list[Recordings] = (
@@ -299,22 +296,36 @@ async def no_recordings(
.iterator()
)
- # Convert recordings to list of (start, end) tuples
+ # Convert recordings to list of (start, end) tuples, ordered by start_time
recordings = [(r["start_time"], r["end_time"]) for r in data]
+ # Merge overlapping/adjacent recordings into covered intervals. The query
+ # orders by start_time, so a single pass merges them
+ covered: list[tuple[float, float]] = []
+ for rec_start, rec_end in recordings:
+ if covered and rec_start <= covered[-1][1]:
+ covered[-1] = (covered[-1][0], max(covered[-1][1], rec_end))
+ else:
+ covered.append((rec_start, rec_end))
+
# Iterate through time segments and check if each has any recording
no_recording_segments = []
current = after
current_gap_start = None
+ idx = 0
+ covered_count = len(covered)
while current < before:
segment_end = min(current + scale, before)
- # Check if this segment overlaps with any recording
- has_recording = any(
- rec_start < segment_end and rec_end > current
- for rec_start, rec_end in recordings
- )
+ # Advance past covered intervals that end before this segment begins;
+ # they cannot overlap this or any later segment.
+ while idx < covered_count and covered[idx][1] <= current:
+ idx += 1
+
+ # A covered interval overlaps the segment when it starts before the
+ # segment ends (its end is already known to be > current).
+ has_recording = idx < covered_count and covered[idx][0] < segment_end
if not has_recording:
# This segment has no recordings
@@ -353,7 +364,7 @@ async def delete_recordings(
start: float = PathParam(..., description="Start timestamp (unix)"),
end: float = PathParam(..., description="End timestamp (unix)"),
params: RecordingsDeleteQueryParams = Depends(),
- allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
+ allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
):
"""Delete recordings in the specified time range."""
if start >= end:
diff --git a/frigate/api/review.py b/frigate/api/review.py
index cb114db2a0..08705e3f9e 100644
--- a/frigate/api/review.py
+++ b/frigate/api/review.py
@@ -4,7 +4,6 @@ import datetime
import logging
from functools import reduce
from pathlib import Path
-from typing import List
import pandas as pd
from fastapi import APIRouter, Request
@@ -51,7 +50,7 @@ router = APIRouter(tags=[Tags.review])
async def review(
params: ReviewQueryParams = Depends(),
current_user: dict = Depends(get_current_user),
- allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
+ allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
):
if isinstance(current_user, JSONResponse):
return current_user
@@ -83,7 +82,7 @@ async def review(
camera_list = list(filtered)
else:
camera_list = allowed_cameras
- clauses.append((ReviewSegment.camera << camera_list))
+ clauses.append(ReviewSegment.camera << camera_list)
if labels != "all":
# use matching so segments with multiple labels
@@ -106,12 +105,12 @@ async def review(
for zone in filtered_zones:
zone_clauses.append(
- (ReviewSegment.data["zones"].cast("text") % f'*"{zone}"*')
+ ReviewSegment.data["zones"].cast("text") % f'*"{zone}"*'
)
clauses.append(reduce(operator.or_, zone_clauses))
if severity:
- clauses.append((ReviewSegment.severity == severity))
+ clauses.append(ReviewSegment.severity == severity)
# Join with UserReviewStatus to get per-user review status
review_query = (
@@ -204,7 +203,7 @@ async def review_ids(request: Request, ids: str):
async def review_summary(
params: ReviewSummaryQueryParams = Depends(),
current_user: dict = Depends(get_current_user),
- allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
+ allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
):
if isinstance(current_user, JSONResponse):
return current_user
@@ -227,7 +226,7 @@ async def review_summary(
camera_list = list(filtered)
else:
camera_list = allowed_cameras
- clauses.append((ReviewSegment.camera << camera_list))
+ clauses.append(ReviewSegment.camera << camera_list)
if labels != "all":
# use matching so segments with multiple labels
@@ -328,7 +327,7 @@ async def review_summary(
camera_list = list(filtered)
else:
camera_list = allowed_cameras
- clauses.append((ReviewSegment.camera << camera_list))
+ clauses.append(ReviewSegment.camera << camera_list)
if labels != "all":
# use matching so segments with multiple labels
@@ -584,7 +583,7 @@ def delete_reviews(body: ReviewModifyMultipleBody):
)
def motion_activity(
params: ReviewActivityMotionQueryParams = Depends(),
- allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
+ allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
):
"""Get motion and audio activity."""
cameras = params.cameras
@@ -597,7 +596,7 @@ def motion_activity(
scale = params.scale
clauses = [(Recordings.start_time > after) & (Recordings.end_time < before)]
- clauses.append((Recordings.motion > 0))
+ clauses.append(Recordings.motion > 0)
if cameras != "all":
requested = set(cameras.split(","))
@@ -605,9 +604,10 @@ def motion_activity(
if not filtered:
return JSONResponse(content=[])
camera_list = list(filtered)
- clauses.append((Recordings.camera << camera_list))
else:
- clauses.append((Recordings.camera << allowed_cameras))
+ camera_list = list(allowed_cameras)
+
+ clauses.append(Recordings.camera << camera_list)
data: list[Recordings] = (
Recordings.select(
@@ -635,14 +635,12 @@ def motion_activity(
df.set_index(["start_time"], inplace=True)
# normalize data
- motion = (
- df["motion"]
- .resample(f"{scale}s")
- .apply(lambda x: max(x, key=abs, default=0.0))
- .fillna(0.0)
- .to_frame()
- )
- cameras = df["camera"].resample(f"{scale}s").agg(lambda x: ",".join(set(x)))
+ motion = df["motion"].resample(f"{scale}s").max().fillna(0.0).to_frame()
+
+ if len(camera_list) == 1:
+ cameras = df["camera"].resample(f"{scale}s").first().fillna("")
+ else:
+ cameras = df["camera"].resample(f"{scale}s").agg(lambda x: ",".join(set(x)))
df = motion.join(cameras)
length = df.shape[0]
@@ -658,6 +656,11 @@ def motion_activity(
else:
df.iloc[i : i + chunk, 0] = 0.0
+ # Drop resample gap-fill buckets. The resample above emits a row for every
+ # {scale}s bucket spanning the range, and buckets with no recording get a
+ # motion of 0 (from fillna) and an empty camera (from joining an empty set).
+ df = df[df["camera"] != ""]
+
# change types for output
df.index = df.index.astype(int) // (10**9)
normalized = df.reset_index().to_dict("records")
diff --git a/frigate/app.py b/frigate/app.py
index 0ead742685..b0e29eab2a 100644
--- a/frigate/app.py
+++ b/frigate/app.py
@@ -4,11 +4,11 @@ import multiprocessing as mp
import os
import secrets
import shutil
+from collections.abc import Callable
from multiprocessing import Queue
from multiprocessing.managers import DictProxy, SyncManager
from multiprocessing.synchronize import Event as MpEvent
from pathlib import Path
-from typing import Callable, Optional
import psutil
import uvicorn
@@ -95,7 +95,7 @@ class FrigateApp:
self, config: FrigateConfig, manager: SyncManager, stop_event: MpEvent
) -> None:
self.metrics_manager = manager
- self.audio_process: Optional[mp.Process] = None
+ self.audio_process: mp.Process | None = None
self.stop_event = stop_event
self.detection_queue: Queue = mp.Queue()
self.detectors: dict[str, ObjectDetectProcess] = {}
@@ -120,8 +120,8 @@ class FrigateApp:
)
self.ptz_metrics: dict[str, PTZMetrics] = {}
self.processes: dict[str, int] = {}
- self.embeddings: Optional[EmbeddingsContext] = None
- self.profile_manager: Optional[ProfileManager] = None
+ self.embeddings: EmbeddingsContext | None = None
+ self.profile_manager: ProfileManager | None = None
self.config = config
def ensure_dirs(self) -> None:
@@ -144,7 +144,7 @@ class FrigateApp:
for d in dirs:
if not os.path.exists(d) and not os.path.islink(d):
logger.info(f"Creating directory: {d}")
- os.makedirs(d)
+ os.makedirs(d, exist_ok=True)
else:
logger.debug(f"Skipping directory: {d}")
@@ -189,17 +189,6 @@ class FrigateApp:
except PermissionError:
logger.error("Unable to write to /config to save DB state")
- def cleanup_timeline_db(db: SqliteExtDatabase) -> None:
- db.execute_sql(
- "DELETE FROM timeline WHERE source_id NOT IN (SELECT id FROM event);"
- )
-
- try:
- with open(f"{CONFIG_DIR}/.timeline", "w") as f:
- f.write(str(datetime.datetime.now().timestamp()))
- except PermissionError:
- logger.error("Unable to write to /config to save DB state")
-
# Migrate DB schema
migrate_db = SqliteExtDatabase(self.config.database.path)
@@ -216,11 +205,6 @@ class FrigateApp:
router.run()
- # this is a temporary check to clean up user DB from beta
- # will be removed before final release
- if not os.path.exists(f"{CONFIG_DIR}/.timeline"):
- cleanup_timeline_db(migrate_db)
-
# check if vacuum needs to be run
if os.path.exists(f"{CONFIG_DIR}/.vacuum"):
with open(f"{CONFIG_DIR}/.vacuum") as f:
@@ -359,12 +343,24 @@ class FrigateApp:
)
self.dispatcher.profile_manager = self.profile_manager
+ def restore_active_profile(self) -> None:
+ """Re-activate the persisted profile after subscribers are connected.
+
+ ZMQ PUB/SUB drops messages with no subscribers, so activation must
+ run after every config_updater subscriber is up.
+ """
+ if self.profile_manager is None:
+ return
+
persisted = ProfileManager.load_persisted_profile()
if persisted and any(
persisted in cam.profiles for cam in self.config.cameras.values()
):
logger.info("Restoring persisted profile '%s'", persisted)
- self.profile_manager.activate_profile(persisted)
+ # runtime overrides are layered on top via restore_runtime_state()
+ self.profile_manager.activate_profile(
+ persisted, clear_runtime_overrides=False
+ )
def start_detectors(self) -> None:
for name in self.config.cameras.keys():
@@ -444,18 +440,11 @@ class FrigateApp:
self.camera_maintainer.start()
def start_audio_processor(self) -> None:
- audio_cameras = [
- c
- for c in self.config.cameras.values()
- if c.enabled and c.audio.enabled_in_config
- ]
-
- if audio_cameras:
- self.audio_process = AudioProcessor(
- self.config, audio_cameras, self.camera_metrics, self.stop_event
- )
- self.audio_process.start()
- self.processes["audio_detector"] = self.audio_process.pid or 0
+ self.audio_process = AudioProcessor(
+ self.config, self.camera_metrics, self.stop_event
+ )
+ self.audio_process.start()
+ self.processes["audio_detector"] = self.audio_process.pid or 0
def start_timeline_processor(self) -> None:
self.timeline_processor = TimelineProcessor(
@@ -635,6 +624,10 @@ class FrigateApp:
self.start_record_cleanup()
self.start_watchdog()
+ # restore persisted runtime overrides on top of config
+ self.restore_active_profile()
+ self.dispatcher.restore_runtime_state()
+
self.init_auth()
try:
diff --git a/frigate/camera/activity_manager.py b/frigate/camera/activity_manager.py
index 38425add9a..bd3474b1ab 100644
--- a/frigate/camera/activity_manager.py
+++ b/frigate/camera/activity_manager.py
@@ -6,13 +6,18 @@ import logging
import random
import string
from collections import Counter
-from typing import Any, Callable
+from collections.abc import Callable
+from typing import Any
from frigate.comms.event_metadata_updater import (
EventMetadataPublisher,
EventMetadataTypeEnum,
)
from frigate.config import CameraConfig, FrigateConfig
+from frigate.config.camera.updater import (
+ CameraConfigUpdateEnum,
+ CameraConfigUpdateSubscriber,
+)
logger = logging.getLogger(__name__)
@@ -29,6 +34,11 @@ class CameraActivityManager:
self.zone_all_object_counts: dict[str, Counter] = {}
self.zone_active_object_counts: dict[str, Counter] = {}
self.all_zone_labels: dict[str, set[str]] = {}
+ self.config_subscriber = CameraConfigUpdateSubscriber(
+ config,
+ config.cameras,
+ [CameraConfigUpdateEnum.zones, CameraConfigUpdateEnum.objects],
+ )
for camera_config in config.cameras.values():
if not camera_config.enabled_in_config:
@@ -56,7 +66,40 @@ class CameraActivityManager:
else camera_config.objects.track
)
+ def __rebuild_zone_labels(self) -> None:
+ """Rebuild zone label tracking after a runtime zones/objects change."""
+ new_zone_labels: dict[str, set[str]] = {}
+
+ for camera_config in self.config.cameras.values():
+ if not camera_config.enabled_in_config or camera_config.name is None:
+ continue
+
+ for zone, zone_config in camera_config.zones.items():
+ new_zone_labels.setdefault(zone, set()).update(
+ zone_config.objects
+ if zone_config.objects
+ else camera_config.objects.track
+ )
+
+ # drop counters for zones that no longer exist
+ for zone in list(self.zone_all_object_counts.keys()):
+ if zone not in new_zone_labels:
+ self.zone_all_object_counts.pop(zone, None)
+ self.zone_active_object_counts.pop(zone, None)
+
+ # ensure counters exist for new zones so the first count is published
+ for zone in new_zone_labels:
+ self.zone_all_object_counts.setdefault(zone, Counter())
+ self.zone_active_object_counts.setdefault(zone, Counter())
+
+ self.all_zone_labels = new_zone_labels
+
def update_activity(self, new_activity: dict[str, dict[str, Any]]) -> None:
+ updated_topics = self.config_subscriber.check_for_updates()
+
+ if "zones" in updated_topics or "objects" in updated_topics:
+ self.__rebuild_zone_labels()
+
all_objects: list[dict[str, Any]] = []
for camera in new_activity.keys():
@@ -161,6 +204,9 @@ class CameraActivityManager:
self.publish(f"{camera}/all", sum(list(all_objects.values())))
self.publish(f"{camera}/all/active", sum(list(active_objects.values())))
+ def stop(self) -> None:
+ self.config_subscriber.stop()
+
class AudioActivityManager:
def __init__(
diff --git a/frigate/camera/maintainer.py b/frigate/camera/maintainer.py
index c4ddc51e89..ea8df7bff0 100644
--- a/frigate/camera/maintainer.py
+++ b/frigate/camera/maintainer.py
@@ -14,6 +14,7 @@ from frigate.config.camera.updater import (
CameraConfigUpdateEnum,
CameraConfigUpdateSubscriber,
)
+from frigate.const import REPLAY_CAMERA_PREFIX
from frigate.models import Regions
from frigate.util.builtin import empty_and_close_queue
from frigate.util.image import SharedMemoryFrameManager, UntrackedSharedMemory
@@ -50,6 +51,7 @@ class CameraMaintainer(threading.Thread):
[
CameraConfigUpdateEnum.add,
CameraConfigUpdateEnum.remove,
+ CameraConfigUpdateEnum.refresh,
],
)
self.shm_count = self.__calculate_shm_frame_count()
@@ -202,6 +204,25 @@ class CameraMaintainer(threading.Thread):
capture_process.terminate()
capture_process.join()
+ def __unlink_camera_frame_slots(self, camera: str) -> None:
+ """Drop the camera's per-frame YUV SHM segments from this
+ process's frame_manager and unlink them at the OS level.
+
+ Safe to call after the camera's capture/processor subprocesses
+ have been joined — they no longer hold mappings, so unlink frees
+ the segments immediately. Other long-lived processes that opened
+ these slots will continue using their existing mappings until
+ they call frame_manager.get with a shape that no longer fits
+ (the get path drops and reopens stale refs).
+ """
+ prefix = f"{camera}_frame"
+ names = [n for n in list(self.frame_manager.shm_store) if n.startswith(prefix)]
+ for name in names:
+ try:
+ self.frame_manager.delete(name)
+ except Exception as exc:
+ logger.debug("Could not unlink SHM %s: %s", name, exc)
+
def __stop_camera_process(self, camera: str) -> None:
camera_process = self.camera_processes.get(camera)
if camera_process is not None:
@@ -253,12 +274,45 @@ class CameraMaintainer(threading.Thread):
for camera in updated_cameras:
self.__stop_camera_capture_process(camera)
self.__stop_camera_process(camera)
+ self.__unlink_camera_frame_slots(camera)
self.capture_processes.pop(camera, None)
self.camera_processes.pop(camera, None)
self.camera_stop_events.pop(camera, None)
self.region_grids.pop(camera, None)
self.camera_metrics.pop(camera, None)
self.ptz_metrics.pop(camera, None)
+ elif update_type == CameraConfigUpdateEnum.refresh.name:
+ # Recycle replay cameras so detect width/height/fps
+ # propagate through ffmpeg args, SHM sizing, and the
+ # region grid. Regular cameras detect change still
+ # requires a full restart.
+ for camera in updated_cameras:
+ if not camera.startswith(REPLAY_CAMERA_PREFIX):
+ continue
+
+ new_config = self.update_subscriber.camera_configs.get(camera)
+ if new_config is None:
+ # remove arrived in the same batch
+ continue
+
+ if (
+ camera not in self.camera_processes
+ and camera not in self.capture_processes
+ ):
+ continue
+
+ # rebuild ffmpeg cmds on the shared config so the
+ # new subprocesses spawn with current args
+ new_config.recreate_ffmpeg_cmds()
+
+ self.__stop_camera_capture_process(camera)
+ self.__stop_camera_process(camera)
+ self.__unlink_camera_frame_slots(camera)
+ self.capture_processes.pop(camera, None)
+ self.camera_processes.pop(camera, None)
+
+ self.__start_camera_processor(camera, new_config, runtime=True)
+ self.__start_camera_capture(camera, new_config, runtime=True)
# ensure the capture processes are done
for camera in self.capture_processes.keys():
diff --git a/frigate/camera/state.py b/frigate/camera/state.py
index 8d0b586022..c0d0f8b7ff 100644
--- a/frigate/camera/state.py
+++ b/frigate/camera/state.py
@@ -5,7 +5,8 @@ import logging
import os
import threading
from collections import defaultdict
-from typing import Any, Callable
+from collections.abc import Callable
+from typing import Any
import cv2
import numpy as np
@@ -45,6 +46,7 @@ class CameraState:
self.frame_cache: dict[float, dict[str, Any]] = {}
self.zone_objects: defaultdict[str, list[Any]] = defaultdict(list)
self._current_frame = np.zeros(self.camera_config.frame_shape_yuv, np.uint8)
+ self._last_frame_shape: tuple[int, int] = self.camera_config.frame_shape_yuv
self.current_frame_lock = threading.Lock()
self.current_frame_time = 0.0
self.motion_boxes: list[tuple[int, int, int, int]] = []
@@ -303,6 +305,42 @@ class CameraState:
def on(self, event_type: str, callback: Callable[..., Any]) -> None:
self.callbacks[event_type].append(callback)
+ def _discard_stale_resolution_state(
+ self, current_detections: dict[str, dict[str, Any]]
+ ) -> bool:
+ """Drop tracked state when the camera's detect resolution has
+ changed, and signal the caller to skip this batch if it contains
+ out-of-bounds boxes from the pre-recycle detect process.
+
+ Returns True when the batch should be skipped entirely.
+ """
+ # detect resolution changed — drop tracked state so old-grid
+ # boxes don't leak through end-callbacks
+ current_shape = self.camera_config.frame_shape_yuv
+ if current_shape != self._last_frame_shape:
+ logger.debug(
+ f"{self.name}: detect resolution changed {self._last_frame_shape} -> {current_shape}, dropping tracked state"
+ )
+ with self.current_frame_lock:
+ self.tracked_objects.clear()
+ self.motion_boxes = []
+ self.regions = []
+ self._last_frame_shape = current_shape
+
+ # drop in-flight batches from the pre-recycle detect process
+ # whose boxes exceed the current detect resolution
+ detect = self.camera_config.detect
+ if detect.width is not None and detect.height is not None:
+ for obj in current_detections.values():
+ box = obj.get("box")
+ if box and (box[2] > detect.width or box[3] > detect.height):
+ logger.debug(
+ f"{self.name}: dropping stale-resolution detection batch (box {box} exceeds {detect.width}x{detect.height})"
+ )
+ return True
+
+ return False
+
def update(
self,
frame_name: str,
@@ -311,6 +349,9 @@ class CameraState:
motion_boxes: list[tuple[int, int, int, int]],
regions: list[tuple[int, int, int, int]],
) -> None:
+ if self._discard_stale_resolution_state(current_detections):
+ return
+
current_frame = self.frame_manager.get(
frame_name, self.camera_config.frame_shape_yuv
)
@@ -332,14 +373,18 @@ class CameraState:
current_detections[id],
)
- # add initial frame to frame cache
- logger.debug(
- f"{self.name}: New object, adding {frame_time} to frame cache for {id}"
- )
- self.frame_cache[frame_time] = {
- "frame": np.copy(current_frame), # type: ignore[arg-type]
- "object_id": id,
- }
+ # Skip caching when the frame buffer isn't readable — e.g.
+ # frame_manager.get returned None because the SHM segment was
+ # unlinked or hasn't been recreated yet during a camera
+ # add/remove cycle.
+ if current_frame is not None:
+ logger.debug(
+ f"{self.name}: New object, adding {frame_time} to frame cache for {id}"
+ )
+ self.frame_cache[frame_time] = {
+ "frame": np.copy(current_frame),
+ "object_id": id,
+ }
# save initial thumbnail data and best object
thumbnail_data = {
diff --git a/frigate/comms/base_communicator.py b/frigate/comms/base_communicator.py
index 5dfbf1115e..090552a5bd 100644
--- a/frigate/comms/base_communicator.py
+++ b/frigate/comms/base_communicator.py
@@ -1,5 +1,6 @@
from abc import ABC, abstractmethod
-from typing import Any, Callable
+from collections.abc import Callable
+from typing import Any
class Communicator(ABC):
diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py
index 27d5ef1255..f83a117510 100644
--- a/frigate/comms/dispatcher.py
+++ b/frigate/comms/dispatcher.py
@@ -3,11 +3,13 @@
import datetime
import json
import logging
-from typing import Any, Callable, Optional, cast
+from collections.abc import Callable, Iterable
+from typing import Any, cast
from frigate.camera import PTZMetrics
from frigate.camera.activity_manager import AudioActivityManager, CameraActivityManager
from frigate.comms.base_communicator import Communicator
+from frigate.comms.runtime_state import RuntimeStatePersistence
from frigate.comms.webpush import WebPushClient
from frigate.config import BirdseyeModeEnum, FrigateConfig
from frigate.config.camera.updater import (
@@ -67,6 +69,7 @@ class Dispatcher:
self.embeddings_reindex: dict[str, Any] = {}
self.birdseye_layout: dict[str, Any] = {}
self.audio_transcription_state: str = "idle"
+ self._runtime_state = RuntimeStatePersistence()
self._camera_settings_handlers: dict[str, Callable] = {
"audio": self._on_audio_command,
"audio_transcription": self._on_audio_transcription_command,
@@ -94,7 +97,7 @@ class Dispatcher:
"notifications": self._on_global_notification_command,
"profile": self._on_profile_command,
}
- self.profile_manager: Optional[ProfileManager] = None
+ self.profile_manager: ProfileManager | None = None
for comm in self.comms:
comm.subscribe(self._receive)
@@ -102,8 +105,10 @@ class Dispatcher:
self.web_push_client = next(
(comm for comm in communicators if isinstance(comm, WebPushClient)), None
)
+ if self.web_push_client is not None:
+ self.web_push_client.set_suspension_broadcaster(self.publish)
- def _receive(self, topic: str, payload: Any) -> Optional[Any]:
+ def _receive(self, topic: str, payload: Any) -> Any | None:
"""Handle receiving of payload from communicators."""
def handle_camera_command(
@@ -394,9 +399,65 @@ class Dispatcher:
comm.publish(topic, payload, retain)
def stop(self) -> None:
+ self.camera_activity.stop()
+
for comm in self.comms:
comm.stop()
+ def restore_runtime_state(self) -> None:
+ """Replay persisted runtime overrides through the camera settings handlers.
+
+ Called once after Frigate startup completes so processing threads can
+ receive the resulting ``config_updater`` broadcasts. Unknown cameras
+ and topics are skipped; handler exceptions are logged and replay
+ continues for remaining entries.
+ """
+ state = self._runtime_state.load()
+ for camera_name, features in state.items():
+ if camera_name not in self.config.cameras:
+ continue
+ for topic, value in features.items():
+ handler = self._camera_settings_handlers.get(topic)
+ if handler is None:
+ continue
+ payload = "ON" if value else "OFF"
+ try:
+ handler(camera_name, payload)
+ except Exception:
+ logger.exception(
+ "Failed to restore runtime state %s.%s=%s",
+ camera_name,
+ topic,
+ payload,
+ )
+ continue
+ logger.info(
+ "Restored runtime state: %s.%s=%s",
+ camera_name,
+ topic,
+ payload,
+ )
+
+ def clear_runtime_state_for_yaml_keys(self, dotted_keys: Iterable[str]) -> None:
+ """Clear stored runtime overrides for YAML keys that were just rewritten.
+
+ Called by ``/api/config/set`` after a successful YAML save so an
+ explicit settings-UI save isn't silently overridden by an older
+ runtime toggle on the next restart.
+ """
+ self._runtime_state.clear_for_yaml_keys(dotted_keys)
+
+ def clear_runtime_state(self) -> None:
+ """Wipe every stored runtime override.
+
+ Called when a profile is activated or deactivated. A profile switch
+ changes the layer below the runtime overrides, so the stored
+ "steady state" is no longer valid and must be reset; otherwise a
+ subsequent restart would replay stale overrides on top of the new
+ profile-derived in-memory state.
+ """
+ self._runtime_state.clear_all()
+
def _on_detect_command(self, camera_name: str, payload: str) -> None:
"""Callback for detect topic."""
detect_settings = self.config.cameras[camera_name].detect
@@ -428,6 +489,7 @@ class Dispatcher:
CameraConfigUpdateTopic(CameraConfigUpdateEnum.detect, camera_name),
detect_settings,
)
+ self._runtime_state.set(camera_name, "detect", detect_settings.enabled)
self.publish(f"{camera_name}/detect/state", payload, retain=True)
def _on_enabled_command(self, camera_name: str, payload: str) -> None:
@@ -452,6 +514,7 @@ class Dispatcher:
CameraConfigUpdateTopic(CameraConfigUpdateEnum.enabled, camera_name),
camera_settings.enabled,
)
+ self._runtime_state.set(camera_name, "enabled", camera_settings.enabled)
self.publish(f"{camera_name}/enabled/state", payload, retain=True)
def _on_motion_command(self, camera_name: str, payload: str) -> None:
@@ -614,6 +677,7 @@ class Dispatcher:
CameraConfigUpdateTopic(CameraConfigUpdateEnum.audio, camera_name),
audio_settings,
)
+ self._runtime_state.set(camera_name, "audio", audio_settings.enabled)
self.publish(f"{camera_name}/audio/state", payload, retain=True)
def _on_audio_transcription_command(self, camera_name: str, payload: str) -> None:
@@ -670,6 +734,7 @@ class Dispatcher:
CameraConfigUpdateTopic(CameraConfigUpdateEnum.record, camera_name),
record_settings,
)
+ self._runtime_state.set(camera_name, "recordings", record_settings.enabled)
self.publish(f"{camera_name}/recordings/state", payload, retain=True)
def _on_snapshots_command(self, camera_name: str, payload: str) -> None:
@@ -689,6 +754,7 @@ class Dispatcher:
CameraConfigUpdateTopic(CameraConfigUpdateEnum.snapshots, camera_name),
snapshots_settings,
)
+ self._runtime_state.set(camera_name, "snapshots", snapshots_settings.enabled)
self.publish(f"{camera_name}/snapshots/state", payload, retain=True)
def _on_ptz_command(self, camera_name: str, payload: str | bytes) -> None:
diff --git a/frigate/comms/embeddings_updater.py b/frigate/comms/embeddings_updater.py
index f7fd9c2bf3..cd83709f0b 100644
--- a/frigate/comms/embeddings_updater.py
+++ b/frigate/comms/embeddings_updater.py
@@ -1,8 +1,9 @@
"""Facilitates communication between processes."""
import logging
+from collections.abc import Callable
from enum import Enum
-from typing import Any, Callable
+from typing import Any
import zmq
diff --git a/frigate/comms/inter_process.py b/frigate/comms/inter_process.py
index 5e76da5ebd..6897b12ebf 100644
--- a/frigate/comms/inter_process.py
+++ b/frigate/comms/inter_process.py
@@ -3,8 +3,9 @@
import logging
import multiprocessing as mp
import threading
+from collections.abc import Callable
from multiprocessing.synchronize import Event as MpEvent
-from typing import Any, Callable
+from typing import Any
import zmq
diff --git a/frigate/comms/mqtt.py b/frigate/comms/mqtt.py
index 89a986e08c..979fc6d6c0 100644
--- a/frigate/comms/mqtt.py
+++ b/frigate/comms/mqtt.py
@@ -1,6 +1,7 @@
import logging
import threading
-from typing import Any, Callable
+from collections.abc import Callable
+from typing import Any
import paho.mqtt.client as mqtt
from paho.mqtt.enums import CallbackAPIVersion
@@ -41,6 +42,18 @@ class MqttClient(Communicator):
self.publish("available", "stopped", retain=True)
self.client.disconnect()
+ def _notifications_enabled_in_config(self) -> bool:
+ """Whether notifications are configured globally or on any camera.
+
+ Notifications can be enabled per camera with the global config left
+ disabled, so the global topics must consider both (matching how
+ app.py decides to create the WebPushClient).
+ """
+ return self.config.notifications.enabled_in_config or any(
+ cam.enabled and cam.notifications.enabled_in_config
+ for cam in self.config.cameras.values()
+ )
+
def _set_initial_topics(self) -> None:
"""Set initial state topics."""
for camera_name, camera in self.config.cameras.items():
@@ -157,7 +170,7 @@ class MqttClient(Communicator):
retain=True,
)
- if self.config.notifications.enabled_in_config:
+ if self._notifications_enabled_in_config():
self.publish(
"notifications/state",
"ON" if self.config.notifications.enabled else "OFF",
@@ -202,8 +215,8 @@ class MqttClient(Communicator):
logger.error("Unable to connect to MQTT server: MQTT Not authorized")
else:
logger.error(
- "Unable to connect to MQTT server: Connection refused. Error code: "
- + reason_code.getName()
+ "Unable to connect to MQTT server: Connection refused. Error code: %s",
+ reason_code.getName(),
)
self.connected = True
@@ -256,6 +269,7 @@ class MqttClient(Communicator):
"review_detections",
"object_descriptions",
"review_descriptions",
+ "notifications",
]
for name in self.config.cameras.keys():
@@ -265,6 +279,12 @@ class MqttClient(Communicator):
self.on_mqtt_command,
)
+ # notifications suspend doesn't follow the /set topic pattern
+ self.client.message_callback_add(
+ f"{self.mqtt_config.topic_prefix}/{name}/notifications/suspend",
+ self.on_mqtt_command,
+ )
+
if self.config.cameras[name].onvif.host:
self.client.message_callback_add(
f"{self.mqtt_config.topic_prefix}/{name}/ptz",
@@ -289,7 +309,7 @@ class MqttClient(Communicator):
self.on_mqtt_command,
)
- if self.config.notifications.enabled_in_config:
+ if self._notifications_enabled_in_config():
self.client.message_callback_add(
f"{self.mqtt_config.topic_prefix}/notifications/set",
self.on_mqtt_command,
diff --git a/frigate/comms/runtime_state.py b/frigate/comms/runtime_state.py
new file mode 100644
index 0000000000..222d488ec9
--- /dev/null
+++ b/frigate/comms/runtime_state.py
@@ -0,0 +1,163 @@
+"""Persistence layer for dispatcher runtime state overrides."""
+
+import json
+import logging
+import os
+from collections.abc import Iterable
+from typing import Any
+
+from filelock import FileLock, Timeout
+
+from frigate.util.config import find_config_file
+
+logger = logging.getLogger(__name__)
+
+
+class RuntimeStatePersistence:
+ """Persist last-known runtime states for dispatcher toggles.
+
+ Stores boolean overrides applied to camera-level toggles by the dispatcher.
+ Overrides are replayed at startup on top of the YAML-derived in-memory
+ config, so changes made via MQTT or the live-view UI survive a restart.
+ """
+
+ # Maps dispatcher topic name -> YAML key suffix under cameras.
+ TRACKED_TOPICS: dict[str, str] = {
+ "enabled": "enabled",
+ "detect": "detect.enabled",
+ "snapshots": "snapshots.enabled",
+ "recordings": "record.enabled",
+ "audio": "audio.enabled",
+ }
+
+ _SUFFIX_TO_TOPIC: dict[str, str] = {v: k for k, v in TRACKED_TOPICS.items()}
+
+ def __init__(self) -> None:
+ self._path = os.path.join(
+ os.path.dirname(find_config_file()), ".runtime_state.json"
+ )
+ self._lock_path = f"{self._path}.lock"
+ self._lock_timeout = 5
+
+ def load(self) -> dict[str, dict[str, bool]]:
+ """Return {camera: {topic: bool}} or {} if missing/corrupt."""
+ try:
+ with FileLock(self._lock_path, timeout=self._lock_timeout):
+ data = self._read_locked()
+ except Timeout:
+ logger.error("Timed out acquiring runtime state lock for load")
+ return {}
+ cameras = data.get("cameras", {})
+ if not isinstance(cameras, dict):
+ return {}
+ # Filter out malformed camera entries so callers can trust the shape.
+ return {
+ name: features
+ for name, features in cameras.items()
+ if isinstance(features, dict)
+ }
+
+ def set(self, camera: str, topic: str, value: bool) -> None:
+ """Persist a single (camera, topic, value). No-op if topic untracked."""
+ if topic not in self.TRACKED_TOPICS:
+ return
+ try:
+ with FileLock(self._lock_path, timeout=self._lock_timeout):
+ data = self._read_locked()
+ cameras = data.setdefault("cameras", {})
+ if not isinstance(cameras, dict):
+ cameras = {}
+ data["cameras"] = cameras
+ cam = cameras.setdefault(camera, {})
+ if not isinstance(cam, dict):
+ cam = {}
+ cameras[camera] = cam
+ cam[topic] = bool(value)
+ self._write_locked(data)
+ except Timeout:
+ logger.error("Timed out persisting runtime state for %s/%s", camera, topic)
+ except OSError:
+ logger.exception("Failed to persist runtime state for %s/%s", camera, topic)
+
+ def clear_all(self) -> None:
+ """Wipe every stored runtime override.
+
+ Called when the "layer below" changes in a way that invalidates all
+ runtime overrides for the current session (currently: profile
+ activation or deactivation).
+ """
+ try:
+ with FileLock(self._lock_path, timeout=self._lock_timeout):
+ if not os.path.exists(self._path):
+ return
+ self._write_locked({"cameras": {}})
+ except Timeout:
+ logger.error("Timed out clearing runtime state")
+ except OSError:
+ logger.exception("Failed to clear runtime state")
+
+ def clear_for_yaml_keys(self, dotted_keys: Iterable[str]) -> None:
+ """Remove stored entries whose YAML key was just rewritten.
+
+ Each dotted key must be of the form ``cameras..``.
+ Keys that don't match a tracked topic are ignored.
+ """
+ to_remove: list[tuple[str, str]] = []
+ for key in dotted_keys:
+ parts = key.split(".")
+ if len(parts) < 3 or parts[0] != "cameras":
+ continue
+ camera = parts[1]
+ suffix = ".".join(parts[2:])
+ topic = self._SUFFIX_TO_TOPIC.get(suffix)
+ if topic is not None:
+ to_remove.append((camera, topic))
+
+ if not to_remove:
+ return
+
+ try:
+ with FileLock(self._lock_path, timeout=self._lock_timeout):
+ data = self._read_locked()
+ cameras = data.get("cameras")
+ if not isinstance(cameras, dict):
+ return
+ changed = False
+ for camera, topic in to_remove:
+ cam = cameras.get(camera)
+ if isinstance(cam, dict) and topic in cam:
+ del cam[topic]
+ changed = True
+ if not cam:
+ del cameras[camera]
+ if changed:
+ self._write_locked(data)
+ except Timeout:
+ logger.error("Timed out clearing runtime state for YAML keys")
+ except OSError:
+ logger.exception("Failed to clear runtime state for YAML keys")
+
+ def _read_locked(self) -> dict[str, Any]:
+ """Read the JSON file while the FileLock is held.
+
+ Returns ``{}`` on a missing or corrupt file so the caller can write a
+ fresh structure on the next mutation.
+ """
+ if not os.path.exists(self._path):
+ return {}
+ try:
+ with open(self._path) as f:
+ data = json.load(f)
+ except (OSError, json.JSONDecodeError):
+ logger.exception(
+ "Failed to read runtime state file %s; starting fresh", self._path
+ )
+ return {}
+ return data if isinstance(data, dict) else {}
+
+ def _write_locked(self, data: dict[str, Any]) -> None:
+ """Atomically write the JSON file while the FileLock is held."""
+ tmp_path = f"{self._path}.tmp"
+ with open(tmp_path, "w") as f:
+ json.dump(data, f, indent=2, sort_keys=True)
+ os.replace(tmp_path, self._path)
diff --git a/frigate/comms/webpush.py b/frigate/comms/webpush.py
index e4ed832682..6f2d9ab8da 100644
--- a/frigate/comms/webpush.py
+++ b/frigate/comms/webpush.py
@@ -6,9 +6,10 @@ import logging
import os
import queue
import threading
+from collections.abc import Callable
from dataclasses import dataclass
from multiprocessing.synchronize import Event as MpEvent
-from typing import Any, Callable
+from typing import Any
from py_vapid import Vapid01
from pywebpush import WebPusher
@@ -54,6 +55,7 @@ class WebPushClient(Communicator):
c.name: 0 # type: ignore[misc]
for c in self.config.cameras.values()
}
+ self.suspension_broadcaster: Callable[[str, Any, bool], None] | None = None
self.last_camera_notification_time: dict[str, float] = {
c.name: 0 # type: ignore[misc]
for c in self.config.cameras.values()
@@ -65,6 +67,10 @@ class WebPushClient(Communicator):
target=self._process_notifications, daemon=True
)
self.notification_thread.start()
+ self.suspension_thread = threading.Thread(
+ target=self._process_suspensions, daemon=True
+ )
+ self.suspension_thread.start()
if not self.config.notifications.email:
logger.warning("Email must be provided for push notifications to be sent.")
@@ -163,6 +169,27 @@ class WebPushClient(Communicator):
def is_camera_suspended(self, camera: str) -> bool:
return datetime.datetime.now().timestamp() <= self.suspended_cameras[camera]
+ def set_suspension_broadcaster(
+ self, broadcaster: Callable[[str, Any, bool], None]
+ ) -> None:
+ """Register the callback used to broadcast suspension state changes."""
+ self.suspension_broadcaster = broadcaster
+
+ def _process_suspensions(self) -> None:
+ while not self.stop_event.wait(1):
+ self._clear_expired_suspensions()
+
+ def _clear_expired_suspensions(self) -> None:
+ """Reset and broadcast cameras whose suspension window has elapsed."""
+ now = datetime.datetime.now().timestamp()
+ for camera, suspended_until in list(self.suspended_cameras.items()):
+ if suspended_until and now > suspended_until:
+ self.unsuspend_notifications(camera)
+ if self.suspension_broadcaster is not None:
+ self.suspension_broadcaster(
+ f"{camera}/notifications/suspended", "0", True
+ )
+
def publish(self, topic: str, payload: Any, retain: bool = False) -> None:
"""Wrapper for publishing when client is in valid state."""
# check for updated global config (notifications, auth)
@@ -429,7 +456,10 @@ class WebPushClient(Communicator):
else:
title = base_title
- message = payload["after"]["data"]["metadata"]["shortSummary"]
+ if payload["after"]["data"]["metadata"].get("shortSummary"):
+ message = payload["after"]["data"]["metadata"]["shortSummary"]
+ else:
+ message = f"Detected on {camera_name}"
else:
zone_names = payload["after"]["data"]["zones"]
formatted_zone_names = []
@@ -549,6 +579,14 @@ class WebPushClient(Communicator):
logger.debug(f"Sending camera monitoring push notification for {camera_name}")
for user in self.web_pushers:
+ if not self._user_has_camera_access(user, camera):
+ logger.debug(
+ "Skipping notification for user %s - no access to camera %s",
+ user,
+ camera,
+ )
+ continue
+
self.send_push_notification(
user=user,
payload=payload,
diff --git a/frigate/comms/ws.py b/frigate/comms/ws.py
index 6cfe4ecc0b..ac047b053f 100644
--- a/frigate/comms/ws.py
+++ b/frigate/comms/ws.py
@@ -4,7 +4,8 @@ import errno
import json
import logging
import threading
-from typing import Any, Callable
+from collections.abc import Callable
+from typing import Any
from wsgiref.simple_server import make_server
from ws4py.server.wsgirefserver import (
@@ -17,9 +18,434 @@ from ws4py.websocket import WebSocket as WebSocket_
from frigate.comms.base_communicator import Communicator
from frigate.config import FrigateConfig
+from frigate.const import (
+ CLEAR_ONGOING_REVIEW_SEGMENTS,
+ EXPIRE_AUDIO_ACTIVITY,
+ INSERT_MANY_RECORDINGS,
+ INSERT_PREVIEW,
+ NOTIFICATION_TEST,
+ REQUEST_REGION_GRID,
+ UPDATE_AUDIO_ACTIVITY,
+ UPDATE_AUDIO_TRANSCRIPTION_STATE,
+ UPDATE_BIRDSEYE_LAYOUT,
+ UPDATE_CAMERA_ACTIVITY,
+ UPDATE_EMBEDDINGS_REINDEX_PROGRESS,
+ UPDATE_EVENT_DESCRIPTION,
+ UPDATE_MODEL_STATE,
+ UPDATE_REVIEW_DESCRIPTION,
+ UPSERT_REVIEW_SEGMENT,
+)
+from frigate.models import User
+from frigate.output.ws_auth import ws_has_camera_access
logger = logging.getLogger(__name__)
+# Internal IPC topics — NEVER allowed from WebSocket, regardless of role
+_WS_BLOCKED_TOPICS = frozenset(
+ {
+ INSERT_MANY_RECORDINGS,
+ INSERT_PREVIEW,
+ REQUEST_REGION_GRID,
+ UPSERT_REVIEW_SEGMENT,
+ CLEAR_ONGOING_REVIEW_SEGMENTS,
+ UPDATE_CAMERA_ACTIVITY,
+ UPDATE_AUDIO_ACTIVITY,
+ EXPIRE_AUDIO_ACTIVITY,
+ UPDATE_EVENT_DESCRIPTION,
+ UPDATE_REVIEW_DESCRIPTION,
+ UPDATE_MODEL_STATE,
+ UPDATE_EMBEDDINGS_REINDEX_PROGRESS,
+ UPDATE_BIRDSEYE_LAYOUT,
+ UPDATE_AUDIO_TRANSCRIPTION_STATE,
+ NOTIFICATION_TEST,
+ }
+)
+
+# Read-only topics any authenticated user (including viewer) can send
+_WS_VIEWER_TOPICS = frozenset(
+ {
+ "onConnect",
+ "modelState",
+ "audioTranscriptionState",
+ "birdseyeLayout",
+ "embeddingsReindexProgress",
+ "jobState",
+ }
+)
+
+# Camera-scoped command topics a camera-authorized (non-admin) user may send.
+_WS_CAMERA_COMMAND_TOPICS = frozenset({"ptz"})
+
+
+def _check_ws_authorization(
+ topic: str,
+ role_header: str | None,
+ separator: str,
+ roles_config: dict[str, list[str]] | None = None,
+ camera_names: set[str] | None = None,
+) -> bool:
+ """Check if a WebSocket message is authorized.
+
+ Args:
+ topic: The message topic.
+ role_header: The HTTP_REMOTE_ROLE header value, or None.
+ separator: The role separator character from proxy config.
+ roles_config: The auth.roles mapping (role -> allowed cameras), used to
+ authorize camera-scoped commands for non-admin users.
+ camera_names: All configured camera names, used to resolve a role's
+ allowed cameras.
+
+ Returns:
+ True if authorized, False if blocked.
+ """
+ # Block IPC-only topics unconditionally
+ if topic in _WS_BLOCKED_TOPICS:
+ return False
+
+ # No role header: default to viewer (fail-closed)
+ roles = [r.strip() for r in role_header.split(separator)] if role_header else []
+
+ # Admin can send anything
+ if "admin" in roles:
+ return True
+
+ # Read-only topics any authenticated user can send
+ if topic in _WS_VIEWER_TOPICS:
+ return True
+
+ # Camera-scoped command like "/ptz": allow when the user's role(s)
+ # grant access to that camera.
+ parts = topic.split("/")
+ if (
+ roles_config is not None
+ and len(parts) == 2
+ and parts[1] in _WS_CAMERA_COMMAND_TOPICS
+ ):
+ allowed: set[str] = set()
+ # No role header maps to the default viewer role (e.g. proxy-only setups)
+ for role in roles or ["viewer"]:
+ allowed.update(
+ User.get_allowed_cameras(role, roles_config, camera_names or set())
+ )
+ return parts[0] in allowed
+
+ return False
+
+
+# ---- Outbound filtering ---------------------------------------------------
+#
+# Every WebSocket broadcast is classified into one of a small set of scopes,
+# then materialized per recipient. Connections with restricted roles only see
+# data for cameras they are authorized to access; admin and full-access roles
+# behave as today.
+
+# Topics that are safe to broadcast to every authenticated client.
+_WS_GLOBAL_OUTBOUND_TOPICS = frozenset(
+ {
+ "model_state",
+ "embeddings_reindex_progress",
+ "audio_transcription_state",
+ "profile/state",
+ "notifications/state",
+ "notification_test",
+ }
+)
+
+# Topics that restricted roles must never receive. Birdseye composites span
+# all cameras, so the existing JSMPEG policy already restricts birdseye access
+# to unrestricted roles; the layout broadcast follows the same rule.
+_WS_UNRESTRICTED_ONLY_TOPICS = frozenset(
+ {
+ "birdseye_layout",
+ }
+)
+
+# Topics whose payload (parsed as JSON) names a single owning camera at the
+# given key path. Used to scope events, reviews, triggers, etc.
+_WS_PAYLOAD_CAMERA_TOPICS: dict[str, tuple[str, ...]] = {
+ "events": ("after", "camera"),
+ "reviews": ("after", "camera"),
+ "tracked_object_update": ("camera",),
+ "triggers": ("camera",),
+ "camera_monitoring": ("camera",),
+}
+
+# Topics whose payload is a dict keyed by camera name; filter keys per
+# recipient.
+_WS_RESHAPE_BY_CAMERA_KEY_TOPICS = frozenset(
+ {
+ "camera_activity",
+ "audio_detections",
+ }
+)
+
+# Topics whose payload is a dict keyed by job_type, where each entry may
+# contain a "camera" or "source_camera" field, or a nested ``results.jobs``
+# list of per-camera sub-jobs (export broadcasts).
+_WS_RESHAPE_JOB_STATE_TOPICS = frozenset(
+ {
+ "job_state",
+ }
+)
+
+# Topics whose payload mixes global aggregates with a ``cameras`` sub-dict
+# keyed by camera name. Aggregates and detector data stay; per-camera entries
+# are filtered.
+_WS_RESHAPE_STATS_TOPICS = frozenset(
+ {
+ "stats",
+ }
+)
+
+
+def _collect_zone_names(config: FrigateConfig) -> set[str]:
+ """Return the set of all zone names defined across cameras."""
+ names: set[str] = set()
+ for camera in config.cameras.values():
+ zones = getattr(camera, "zones", None) or {}
+ names.update(zones.keys())
+ return names
+
+
+def _parse_json_payload(payload: Any) -> Any:
+ """Return payload parsed as JSON if it is a string, else as-is."""
+ if isinstance(payload, str):
+ try:
+ return json.loads(payload)
+ except (ValueError, TypeError):
+ return None
+ return payload
+
+
+def _scope_job_entry_to_allowed(entry: Any, allowed: set[str]) -> dict[str, Any] | None:
+ """Filter a single job_state entry to the recipient's allowed cameras.
+
+ Returns the (possibly reshaped) entry, or None to drop it. Four shapes
+ are handled:
+
+ * Top-level ``camera`` or ``source_camera`` (motion_search, vlm_watch,
+ export sub-job dicts): drop the entry if not allowed.
+ * Nested ``results.jobs`` list of per-camera sub-jobs (the aggregated
+ export broadcast): filter the list; drop the entry if nothing remains.
+ * Nested ``results.camera`` or ``results.source_camera`` (debug_replay,
+ which puts replay-specific fields inside ``results``): drop the entry
+ if not allowed.
+ * No camera anywhere (e.g. ``media_sync``): treat as global and keep.
+ """
+ if not isinstance(entry, dict):
+ return None
+
+ cam = entry.get("camera") or entry.get("source_camera")
+
+ if cam is None:
+ results = entry.get("results")
+ if isinstance(results, dict):
+ sub_jobs = results.get("jobs")
+ if isinstance(sub_jobs, list):
+ filtered_jobs = [
+ j
+ for j in sub_jobs
+ if isinstance(j, dict)
+ and (j.get("camera") or j.get("source_camera")) in allowed
+ ]
+ if not filtered_jobs:
+ return None
+ reshaped = dict(entry)
+ reshaped["results"] = dict(results)
+ reshaped["results"]["jobs"] = filtered_jobs
+ return reshaped
+
+ cam = results.get("camera") or results.get("source_camera")
+
+ if cam is not None:
+ return entry if cam in allowed else None
+
+ return entry
+
+
+def _extract_payload_camera(payload: Any, path: tuple[str, ...]) -> str | None:
+ """Walk the dotted path through a (possibly JSON-encoded) payload."""
+ cur = _parse_json_payload(payload)
+ for key in path:
+ if not isinstance(cur, dict):
+ return None
+ cur = cur.get(key)
+ return cur if isinstance(cur, str) else None
+
+
+def _classify_outbound(
+ topic: str, all_cameras: set[str], all_zones: set[str]
+) -> tuple[str, Any]:
+ """Classify an outbound topic into (kind, extra).
+
+ kind values:
+ - "global" : send to every authenticated client
+ - "drop" : send to nobody (fail-closed for unknowns)
+ - "unrestricted_only" : send only to admin/full-access roles
+ - "camera" : extra is the owning camera name
+ - "payload_camera" : extra is the JSON key path to the camera name
+ - "reshape_by_camera_key"
+ - "reshape_job_state"
+ - "reshape_stats"
+ """
+ if topic in _WS_GLOBAL_OUTBOUND_TOPICS:
+ return ("global", None)
+ if topic in _WS_UNRESTRICTED_ONLY_TOPICS:
+ return ("unrestricted_only", None)
+ if topic in _WS_RESHAPE_BY_CAMERA_KEY_TOPICS:
+ return ("reshape_by_camera_key", None)
+ if topic in _WS_RESHAPE_JOB_STATE_TOPICS:
+ return ("reshape_job_state", None)
+ if topic in _WS_RESHAPE_STATS_TOPICS:
+ return ("reshape_stats", None)
+ if topic in _WS_PAYLOAD_CAMERA_TOPICS:
+ return ("payload_camera", _WS_PAYLOAD_CAMERA_TOPICS[topic])
+
+ # Topic-prefix based: first segment names the owning camera or zone.
+ first = topic.split("/", 1)[0]
+ if first in all_cameras:
+ return ("camera", first)
+ if first in all_zones:
+ # Zone aggregates span cameras; restricted users see nothing here.
+ return ("unrestricted_only", None)
+
+ return ("drop", None)
+
+
+def _ws_role_header(ws: Any) -> str | None:
+ """Return the HTTP_REMOTE_ROLE header value, if any."""
+ environ = getattr(ws, "environ", None)
+ if not environ:
+ return None
+ value = environ.get("HTTP_REMOTE_ROLE")
+ return value if isinstance(value, str) else None
+
+
+def _ws_valid_roles(ws: Any, config: FrigateConfig) -> list[str]:
+ """Return the list of recognized roles for this connection."""
+ header = _ws_role_header(ws)
+ if not header:
+ return []
+ roles = [r.strip() for r in header.split(config.proxy.separator) if r.strip()]
+ return [r for r in roles if r in config.auth.roles]
+
+
+def _ws_is_unrestricted(ws: Any, config: FrigateConfig) -> bool:
+ """True when the connection has unrestricted camera access.
+
+ Mirrors the policy in ``frigate.output.ws_auth``: admin or any role with
+ an empty allow-list grants full access.
+ """
+ roles = _ws_valid_roles(ws, config)
+ if not roles:
+ return False
+ roles_dict = config.auth.roles
+ return any(r == "admin" or not roles_dict.get(r) for r in roles)
+
+
+def _ws_allowed_cameras(ws: Any, config: FrigateConfig) -> set[str]:
+ """Return the union of cameras this connection may access across its roles."""
+ roles = _ws_valid_roles(ws, config)
+ if not roles:
+ return set()
+ all_cameras = set(config.cameras.keys())
+ allowed: set[str] = set()
+ for role in roles:
+ if role == "admin" or not config.auth.roles.get(role):
+ return all_cameras
+ allowed.update(User.get_allowed_cameras(role, config.auth.roles, all_cameras))
+ return allowed
+
+
+def _wrap_envelope(topic: str, inner_payload: Any) -> str:
+ """Re-serialize a (topic, payload) message after payload reshaping.
+
+ Frigate's wire format keeps payloads as JSON-encoded strings inside the
+ outer envelope, mirroring what producers send today.
+ """
+ return json.dumps({"topic": topic, "payload": json.dumps(inner_payload)})
+
+
+def _materialize_for_ws(
+ ws: Any,
+ topic: str,
+ full_message: str,
+ scope: tuple[str, Any],
+ parsed_payload: Any,
+ config: FrigateConfig,
+) -> str | None:
+ """Return the JSON string to deliver to ``ws``, or None to skip it."""
+ kind, extra = scope
+ has_role = _ws_role_header(ws) is not None
+
+ if kind == "drop":
+ return None
+
+ if kind == "global":
+ # Globals still require an authenticated connection. Missing role
+ # falls back to viewer semantics (matching the inbound rule).
+ return full_message
+
+ # Beyond globals, an authenticated role header is required (fail-closed).
+ if not has_role:
+ return None
+
+ if kind == "unrestricted_only":
+ return full_message if _ws_is_unrestricted(ws, config) else None
+
+ if kind == "camera":
+ return full_message if ws_has_camera_access(ws, extra, config) else None
+
+ if kind == "payload_camera":
+ camera = _extract_payload_camera(parsed_payload, extra)
+ if camera is None:
+ return None
+ return full_message if ws_has_camera_access(ws, camera, config) else None
+
+ if kind == "reshape_by_camera_key":
+ if _ws_is_unrestricted(ws, config):
+ return full_message
+ if not isinstance(parsed_payload, dict):
+ return None
+ allowed = _ws_allowed_cameras(ws, config)
+ filtered = {cam: data for cam, data in parsed_payload.items() if cam in allowed}
+ if not filtered:
+ return None
+ return _wrap_envelope(topic, filtered)
+
+ if kind == "reshape_job_state":
+ if _ws_is_unrestricted(ws, config):
+ return full_message
+ if not isinstance(parsed_payload, dict):
+ return None
+ allowed = _ws_allowed_cameras(ws, config)
+ filtered_jobs: dict[str, Any] = {}
+ for job_type, job_payload in parsed_payload.items():
+ scoped = _scope_job_entry_to_allowed(job_payload, allowed)
+ if scoped is not None:
+ filtered_jobs[job_type] = scoped
+ if not filtered_jobs:
+ return None
+ return _wrap_envelope(topic, filtered_jobs)
+
+ if kind == "reshape_stats":
+ if _ws_is_unrestricted(ws, config):
+ return full_message
+ if not isinstance(parsed_payload, dict):
+ return None
+ allowed = _ws_allowed_cameras(ws, config)
+ cameras_block = parsed_payload.get("cameras")
+ if isinstance(cameras_block, dict):
+ filtered_cameras = {
+ name: data for name, data in cameras_block.items() if name in allowed
+ }
+ reshaped = dict(parsed_payload)
+ reshaped["cameras"] = filtered_cameras
+ return _wrap_envelope(topic, reshaped)
+ return full_message
+
+ return None
+
class WebSocket(WebSocket_): # type: ignore[misc]
def unhandled_error(self, error: Any) -> None:
@@ -49,6 +475,9 @@ class WebSocketClient(Communicator):
class _WebSocketHandler(WebSocket):
receiver = self._dispatcher
+ role_separator = self.config.proxy.separator or ","
+ roles_config = self.config.auth.roles
+ camera_names = set(self.config.cameras.keys())
def received_message(self, message: WebSocket.received_message) -> None: # type: ignore[name-defined]
try:
@@ -63,11 +492,29 @@ class WebSocketClient(Communicator):
)
return
- logger.debug(
- f"Publishing mqtt message from websockets at {json_message['topic']}."
+ topic = json_message["topic"]
+
+ # Authorization check (skip when environ is None — direct internal connection)
+ role_header = (
+ self.environ.get("HTTP_REMOTE_ROLE") if self.environ else None
)
+ if self.environ is not None and not _check_ws_authorization(
+ topic,
+ role_header,
+ self.role_separator,
+ self.roles_config,
+ self.camera_names,
+ ):
+ logger.warning(
+ "Blocked unauthorized WebSocket message: topic=%s, role=%s",
+ topic,
+ role_header,
+ )
+ return
+
+ logger.debug(f"Publishing mqtt message from websockets at {topic}.")
self.receiver(
- json_message["topic"],
+ topic,
json_message["payload"],
)
@@ -87,6 +534,10 @@ class WebSocketClient(Communicator):
self.websocket_thread.start()
def publish(self, topic: str, payload: Any, _: bool = False) -> None:
+ if self.websocket_server is None:
+ logger.debug("Skipping message, websocket not connected yet")
+ return
+
try:
ws_message = json.dumps(
{
@@ -99,14 +550,42 @@ class WebSocketClient(Communicator):
logger.debug(f"payload for {topic} wasn't text. Skipping...")
return
- if self.websocket_server is None:
- logger.debug("Skipping message, websocket not connected yet")
+ all_cameras = set(self.config.cameras.keys())
+ all_zones = _collect_zone_names(self.config)
+ scope = _classify_outbound(topic, all_cameras, all_zones)
+
+ if scope[0] == "drop":
return
- try:
- self.websocket_server.manager.broadcast(ws_message)
- except ConnectionResetError:
- pass
+ # Pre-parse payload once for topics that need to read its contents.
+ parsed_payload: Any = None
+ if scope[0] in (
+ "payload_camera",
+ "reshape_by_camera_key",
+ "reshape_job_state",
+ "reshape_stats",
+ ):
+ parsed_payload = _parse_json_payload(payload)
+ if parsed_payload is None:
+ # malformed payload — fail closed
+ return
+
+ manager = self.websocket_server.manager
+ with manager.lock:
+ websockets = list(manager.websockets.values())
+
+ for ws in websockets:
+ if getattr(ws, "terminated", False):
+ continue
+ message = _materialize_for_ws(
+ ws, topic, ws_message, scope, parsed_payload, self.config
+ )
+ if message is None:
+ continue
+ try:
+ ws.send(message)
+ except (ConnectionResetError, BrokenPipeError, ValueError):
+ pass
def stop(self) -> None:
if self.websocket_server is not None:
diff --git a/frigate/config/auth.py b/frigate/config/auth.py
index fccbfbaf2c..04beeb7757 100644
--- a/frigate/config/auth.py
+++ b/frigate/config/auth.py
@@ -1,5 +1,3 @@
-from typing import Dict, List, Optional
-
from pydantic import Field, field_validator, model_validator
from .base import FrigateBaseModel
@@ -41,7 +39,7 @@ class AuthConfig(FrigateBaseModel):
description="When a session is within this many seconds of expiring, refresh it back to full length.",
ge=30,
)
- failed_login_rate_limit: Optional[str] = Field(
+ failed_login_rate_limit: str | None = Field(
default=None,
title="Failed login limits",
description="Rate limiting rules for failed login attempts to reduce brute-force attacks.",
@@ -57,12 +55,12 @@ class AuthConfig(FrigateBaseModel):
title="Hash iterations",
description="Number of PBKDF2-SHA256 iterations to use when hashing user passwords.",
)
- roles: Dict[str, List[str]] = Field(
+ roles: dict[str, list[str]] = Field(
default_factory=dict,
title="Role mappings",
description="Map roles to camera lists. An empty list grants access to all cameras for the role.",
)
- admin_first_time_login: Optional[bool] = Field(
+ admin_first_time_login: bool | None = Field(
default=False,
title="First-time admin flag",
description=(
@@ -72,7 +70,7 @@ class AuthConfig(FrigateBaseModel):
@field_validator("roles")
@classmethod
- def validate_roles(cls, v: Dict[str, List[str]]) -> Dict[str, List[str]]:
+ def validate_roles(cls, v: dict[str, list[str]]) -> dict[str, list[str]]:
# Ensure role names are valid (alphanumeric with underscores)
for role in v.keys():
if not role.replace("_", "").isalnum():
diff --git a/frigate/config/camera/audio.py b/frigate/config/camera/audio.py
index 6028802df9..813c2988db 100644
--- a/frigate/config/camera/audio.py
+++ b/frigate/config/camera/audio.py
@@ -1,5 +1,3 @@
-from typing import Optional
-
from pydantic import Field
from frigate.const import AUDIO_MIN_CONFIDENCE
@@ -9,7 +7,7 @@ from ..base import FrigateBaseModel
__all__ = ["AudioConfig", "AudioFilterConfig"]
-DEFAULT_LISTEN_AUDIO = ["bark", "fire_alarm", "scream", "speech", "yell"]
+DEFAULT_LISTEN_AUDIO = ["bark", "fire_alarm", "speech", "yell"]
class AudioFilterConfig(FrigateBaseModel):
@@ -41,14 +39,14 @@ class AudioConfig(FrigateBaseModel):
listen: list[str] = Field(
default=DEFAULT_LISTEN_AUDIO,
title="Listen types",
- description="List of audio event types to detect (for example: bark, fire_alarm, scream, speech, yell).",
+ description="List of audio event types to detect (for example: bark, fire_alarm, speech, yell).",
)
- filters: Optional[dict[str, AudioFilterConfig]] = Field(
+ filters: dict[str, AudioFilterConfig] | None = Field(
None,
title="Audio filters",
description="Per-audio-type filter settings such as confidence thresholds used to reduce false positives.",
)
- enabled_in_config: Optional[bool] = Field(
+ enabled_in_config: bool | None = Field(
None,
title="Original audio state",
description="Indicates whether audio detection was originally enabled in the static config file.",
diff --git a/frigate/config/camera/birdseye.py b/frigate/config/camera/birdseye.py
index 32aa66a985..b51c73d3c6 100644
--- a/frigate/config/camera/birdseye.py
+++ b/frigate/config/camera/birdseye.py
@@ -1,5 +1,4 @@
from enum import Enum
-from typing import Optional
from pydantic import BaseModel, Field
@@ -35,7 +34,7 @@ class BirdseyeLayoutConfig(FrigateBaseModel):
ge=1.0,
le=5.0,
)
- max_cameras: Optional[int] = Field(
+ max_cameras: int | None = Field(
default=None,
title="Max cameras",
description="Maximum number of cameras to display at once in Birdseye; shows the most recent cameras.",
diff --git a/frigate/config/camera/camera.py b/frigate/config/camera/camera.py
index 529b8e45cf..b9d2cef727 100644
--- a/frigate/config/camera/camera.py
+++ b/frigate/config/camera/camera.py
@@ -1,6 +1,5 @@
import os
from enum import Enum
-from typing import Optional
from pydantic import Field, PrivateAttr, model_validator
@@ -51,14 +50,14 @@ class CameraTypeEnum(str, Enum):
class CameraConfig(FrigateBaseModel):
- name: Optional[str] = Field(
+ name: str | None = Field(
None,
title="Camera name",
description="Camera name is required",
pattern=REGEX_CAMERA_NAME,
)
- friendly_name: Optional[str] = Field(
+ friendly_name: str | None = Field(
None,
title="Friendly name",
description="Camera friendly name used in the Frigate UI",
@@ -76,7 +75,7 @@ class CameraConfig(FrigateBaseModel):
# Options with global fallback
audio: AudioConfig = Field(
default_factory=AudioConfig,
- title="Audio events",
+ title="Audio detection",
description="Settings for audio-based event detection for this camera.",
)
audio_transcription: CameraAudioTranscriptionConfig = Field(
@@ -100,8 +99,8 @@ class CameraConfig(FrigateBaseModel):
description="Settings for face detection and recognition for this camera.",
)
ffmpeg: CameraFfmpegConfig = Field(
- title="FFmpeg",
- description="FFmpeg settings including binary path, args, hwaccel options, and per-role output args.",
+ title="Streams (FFmpeg)",
+ description="Camera stream inputs and FFmpeg options, including binary path, args, hwaccel, and per-role output args.",
)
live: CameraLiveConfig = Field(
default_factory=CameraLiveConfig,
@@ -146,7 +145,7 @@ class CameraConfig(FrigateBaseModel):
timestamp_style: TimestampStyleConfig = Field(
default_factory=TimestampStyleConfig,
title="Timestamp style",
- description="Styling options for in-feed timestamps applied to recordings and snapshots.",
+ description="Styling options for timestamps applied to snapshots and Debug view.",
)
# Options without global fallback
@@ -180,7 +179,7 @@ class CameraConfig(FrigateBaseModel):
title="Camera UI",
description="Display ordering and visibility for this camera in the UI. Ordering affects the default dashboard. For more granular control, use camera groups.",
)
- webui_url: Optional[str] = Field(
+ webui_url: str | None = Field(
None,
title="Camera URL",
description="URL to visit the camera directly from system page",
@@ -196,7 +195,7 @@ class CameraConfig(FrigateBaseModel):
title="Zones",
description="Zones allow you to define a specific area of the frame so you can determine whether or not an object is within a particular area.",
)
- enabled_in_config: Optional[bool] = Field(
+ enabled_in_config: bool | None = Field(
default=None,
title="Original camera state",
description="Keep track of original state of camera.",
diff --git a/frigate/config/camera/detect.py b/frigate/config/camera/detect.py
index 71dbc32928..d093ed986e 100644
--- a/frigate/config/camera/detect.py
+++ b/frigate/config/camera/detect.py
@@ -1,5 +1,3 @@
-from typing import Optional
-
from pydantic import Field, model_validator
from ..base import FrigateBaseModel
@@ -8,7 +6,7 @@ __all__ = ["DetectConfig", "StationaryConfig", "StationaryMaxFramesConfig"]
class StationaryMaxFramesConfig(FrigateBaseModel):
- default: Optional[int] = Field(
+ default: int | None = Field(
default=None,
title="Default max frames",
description="Default maximum frames to track a stationary object before stopping.",
@@ -22,13 +20,13 @@ class StationaryMaxFramesConfig(FrigateBaseModel):
class StationaryConfig(FrigateBaseModel):
- interval: Optional[int] = Field(
+ interval: int | None = Field(
default=None,
title="Stationary interval",
description="How often (in frames) to run a detection check to confirm a stationary object.",
gt=0,
)
- threshold: Optional[int] = Field(
+ threshold: int | None = Field(
default=None,
title="Stationary threshold",
description="Number of frames with no position change required to mark an object as stationary.",
@@ -52,12 +50,12 @@ class DetectConfig(FrigateBaseModel):
title="Enable object detection",
description="Enable or disable object detection for all cameras; can be overridden per-camera.",
)
- height: Optional[int] = Field(
+ height: int | None = Field(
default=None,
title="Detect height",
description="Height (pixels) of frames used for the detect stream; leave empty to use the native stream resolution.",
)
- width: Optional[int] = Field(
+ width: int | None = Field(
default=None,
title="Detect width",
description="Width (pixels) of frames used for the detect stream; leave empty to use the native stream resolution.",
@@ -67,13 +65,13 @@ class DetectConfig(FrigateBaseModel):
title="Detect FPS",
description="Desired frames per second to run detection on; lower values reduce CPU usage (recommended value is 5, only set higher - at most 10 - if tracking extremely fast moving objects).",
)
- min_initialized: Optional[int] = Field(
+ min_initialized: int | None = Field(
default=None,
title="Minimum initialization frames",
description="Number of consecutive detection hits required before creating a tracked object. Increase to reduce false initializations. Default value is fps divided by 2.",
ge=2,
)
- max_disappeared: Optional[int] = Field(
+ max_disappeared: int | None = Field(
default=None,
title="Maximum disappeared frames",
description="Number of frames without a detection before a tracked object is considered gone.",
diff --git a/frigate/config/camera/ffmpeg.py b/frigate/config/camera/ffmpeg.py
index 05769dc66d..ad7cbc8aa1 100644
--- a/frigate/config/camera/ffmpeg.py
+++ b/frigate/config/camera/ffmpeg.py
@@ -1,9 +1,8 @@
from enum import Enum
-from typing import Union
from pydantic import Field, field_validator
-from frigate.const import DEFAULT_FFMPEG_VERSION, INCLUDED_FFMPEG_VERSIONS
+from frigate.util.config import resolve_ffmpeg_path
from ..base import FrigateBaseModel
from ..env import EnvString
@@ -33,12 +32,12 @@ DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT = [
class FfmpegOutputArgsConfig(FrigateBaseModel):
- detect: Union[str, list[str]] = Field(
+ detect: str | list[str] = Field(
default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT,
title="Detect output arguments",
description="Default output arguments for detect role streams.",
)
- record: Union[str, list[str]] = Field(
+ record: str | list[str] = Field(
default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT,
title="Record output arguments",
description="Default output arguments for record role streams.",
@@ -49,19 +48,19 @@ class FfmpegConfig(FrigateBaseModel):
path: str = Field(
default="default",
title="FFmpeg path",
- description='Path to the FFmpeg binary to use or a version alias ("5.0" or "7.0").',
+ description='Path to the FFmpeg binary to use or a version alias ("7.0" or "8.0").',
)
- global_args: Union[str, list[str]] = Field(
+ global_args: str | list[str] = Field(
default=FFMPEG_GLOBAL_ARGS_DEFAULT,
title="FFmpeg global arguments",
description="Global arguments passed to FFmpeg processes.",
)
- hwaccel_args: Union[str, list[str]] = Field(
+ hwaccel_args: str | list[str] = Field(
default="auto",
title="Hardware acceleration arguments",
description="Hardware acceleration arguments for FFmpeg. Provider-specific presets are recommended.",
)
- input_args: Union[str, list[str]] = Field(
+ input_args: str | list[str] = Field(
default=FFMPEG_INPUT_ARGS_DEFAULT,
title="Input arguments",
description="Input arguments applied to FFmpeg input streams.",
@@ -90,21 +89,11 @@ class FfmpegConfig(FrigateBaseModel):
@property
def ffmpeg_path(self) -> str:
- if self.path == "default":
- return f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffmpeg"
- elif self.path in INCLUDED_FFMPEG_VERSIONS:
- return f"/usr/lib/ffmpeg/{self.path}/bin/ffmpeg"
- else:
- return f"{self.path}/bin/ffmpeg"
+ return resolve_ffmpeg_path(self.path, "ffmpeg")
@property
def ffprobe_path(self) -> str:
- if self.path == "default":
- return f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffprobe"
- elif self.path in INCLUDED_FFMPEG_VERSIONS:
- return f"/usr/lib/ffmpeg/{self.path}/bin/ffprobe"
- else:
- return f"{self.path}/bin/ffprobe"
+ return resolve_ffmpeg_path(self.path, "ffprobe")
class CameraRoleEnum(str, Enum):
@@ -122,17 +111,17 @@ class CameraInput(FrigateBaseModel):
title="Input roles",
description="Roles for this input stream.",
)
- global_args: Union[str, list[str]] = Field(
+ global_args: str | list[str] = Field(
default_factory=list,
title="FFmpeg global arguments",
description="FFmpeg global arguments for this input stream.",
)
- hwaccel_args: Union[str, list[str]] = Field(
+ hwaccel_args: str | list[str] = Field(
default_factory=list,
title="Hardware acceleration arguments",
description="Hardware acceleration arguments for this input stream.",
)
- input_args: Union[str, list[str]] = Field(
+ input_args: str | list[str] = Field(
default_factory=list,
title="Input arguments",
description="Input arguments specific to this stream.",
diff --git a/frigate/config/camera/genai.py b/frigate/config/camera/genai.py
index 721eeb60d8..82358252f8 100644
--- a/frigate/config/camera/genai.py
+++ b/frigate/config/camera/genai.py
@@ -1,5 +1,5 @@
from enum import Enum
-from typing import Any, Optional
+from typing import Any
from pydantic import Field
@@ -26,23 +26,22 @@ class GenAIRoleEnum(str, Enum):
class GenAIConfig(FrigateBaseModel):
"""Primary GenAI Config to define GenAI Provider."""
- api_key: Optional[EnvString] = Field(
+ api_key: EnvString | None = Field(
default=None,
title="API key",
description="API key required by some providers (can also be set via environment variables).",
)
- base_url: Optional[str] = Field(
+ base_url: str | None = Field(
default=None,
title="Base URL",
description="Base URL for self-hosted or compatible providers (for example an Ollama instance).",
)
model: str = Field(
- default="gpt-4o",
+ default="",
title="Model",
description="The model to use from the provider for generating descriptions or summaries.",
)
- provider: GenAIProviderEnum | None = Field(
- default=None,
+ provider: GenAIProviderEnum = Field(
title="Provider",
description="The GenAI provider to use (for example: ollama, gemini, openai).",
)
diff --git a/frigate/config/camera/live.py b/frigate/config/camera/live.py
index 54b5a2bfd5..90a7c8e4c1 100644
--- a/frigate/config/camera/live.py
+++ b/frigate/config/camera/live.py
@@ -1,5 +1,3 @@
-from typing import Dict
-
from pydantic import Field
from ..base import FrigateBaseModel
@@ -8,7 +6,7 @@ __all__ = ["CameraLiveConfig"]
class CameraLiveConfig(FrigateBaseModel):
- streams: Dict[str, str] = Field(
+ streams: dict[str, str] = Field(
default_factory=list,
title="Live stream names",
description="Mapping of configured stream names to restream/go2rtc names used for live playback.",
diff --git a/frigate/config/camera/mask.py b/frigate/config/camera/mask.py
index dbe0f063ce..ecfdb418b4 100644
--- a/frigate/config/camera/mask.py
+++ b/frigate/config/camera/mask.py
@@ -1,6 +1,6 @@
"""Mask configuration for motion and object masks."""
-from typing import Any, Optional, Union
+from typing import Any
from pydantic import Field, field_serializer
@@ -12,7 +12,7 @@ __all__ = ["MotionMaskConfig", "ObjectMaskConfig"]
class MotionMaskConfig(FrigateBaseModel):
"""Configuration for a single motion mask."""
- friendly_name: Optional[str] = Field(
+ friendly_name: str | None = Field(
default=None,
title="Friendly name",
description="A friendly name for this motion mask used in the Frigate UI",
@@ -22,13 +22,13 @@ class MotionMaskConfig(FrigateBaseModel):
title="Enabled",
description="Enable or disable this motion mask",
)
- coordinates: Union[str, list[str]] = Field(
+ coordinates: str | list[str] = Field(
default="",
title="Coordinates",
description="Ordered x,y coordinates defining the motion mask polygon used to include/exclude areas.",
)
- raw_coordinates: Union[str, list[str]] = ""
- enabled_in_config: Optional[bool] = Field(
+ raw_coordinates: str | list[str] = ""
+ enabled_in_config: bool | None = Field(
default=None, title="Keep track of original state of motion mask."
)
@@ -50,7 +50,7 @@ class MotionMaskConfig(FrigateBaseModel):
class ObjectMaskConfig(FrigateBaseModel):
"""Configuration for a single object mask."""
- friendly_name: Optional[str] = Field(
+ friendly_name: str | None = Field(
default=None,
title="Friendly name",
description="A friendly name for this object mask used in the Frigate UI",
@@ -60,13 +60,13 @@ class ObjectMaskConfig(FrigateBaseModel):
title="Enabled",
description="Enable or disable this object mask",
)
- coordinates: Union[str, list[str]] = Field(
+ coordinates: str | list[str] = Field(
default="",
title="Coordinates",
description="Ordered x,y coordinates defining the object mask polygon used to include/exclude areas.",
)
- raw_coordinates: Union[str, list[str]] = ""
- enabled_in_config: Optional[bool] = Field(
+ raw_coordinates: str | list[str] = ""
+ enabled_in_config: bool | None = Field(
default=None, title="Keep track of original state of object mask."
)
diff --git a/frigate/config/camera/motion.py b/frigate/config/camera/motion.py
index ebba8613cb..c44c1453f6 100644
--- a/frigate/config/camera/motion.py
+++ b/frigate/config/camera/motion.py
@@ -1,4 +1,4 @@
-from typing import Any, Optional
+from typing import Any
from pydantic import Field, field_serializer
@@ -28,7 +28,7 @@ class MotionConfig(FrigateBaseModel):
ge=0.3,
le=1.0,
)
- skip_motion_threshold: Optional[float] = Field(
+ skip_motion_threshold: float | None = Field(
default=None,
title="Skip motion threshold",
description="If set to a value between 0.0 and 1.0, and more than this fraction of the image changes in a single frame, the detector will return no motion boxes and immediately recalibrate. This can save CPU and reduce false positives during lightning, storms, etc., but may miss real events such as a PTZ camera auto‑tracking an object. The trade‑off is between dropping a few megabytes of recordings versus reviewing a couple short clips. Leave unset (None) to disable this feature.",
@@ -40,7 +40,7 @@ class MotionConfig(FrigateBaseModel):
title="Improve contrast",
description="Apply contrast improvement to frames before motion analysis to help detection.",
)
- contour_area: Optional[int] = Field(
+ contour_area: int | None = Field(
default=10,
title="Contour area",
description="Minimum contour area in pixels required for a motion contour to be counted.",
@@ -55,12 +55,12 @@ class MotionConfig(FrigateBaseModel):
title="Frame alpha",
description="Alpha value used when blending frames for motion preprocessing.",
)
- frame_height: Optional[int] = Field(
+ frame_height: int | None = Field(
default=100,
title="Frame height",
description="Height in pixels to scale frames to when computing motion.",
)
- mask: dict[str, Optional[MotionMaskConfig]] = Field(
+ mask: dict[str, MotionMaskConfig | None] = Field(
default_factory=dict,
title="Mask coordinates",
description="Ordered x,y coordinates defining the motion mask polygon used to include/exclude areas.",
@@ -70,12 +70,12 @@ class MotionConfig(FrigateBaseModel):
title="MQTT off delay",
description="Seconds to wait after last motion before publishing an MQTT 'off' state.",
)
- enabled_in_config: Optional[bool] = Field(
+ enabled_in_config: bool | None = Field(
default=None,
title="Original motion state",
description="Indicates whether motion detection was enabled in the original static configuration.",
)
- raw_mask: dict[str, Optional[MotionMaskConfig]] = Field(
+ raw_mask: dict[str, MotionMaskConfig | None] = Field(
default_factory=dict, exclude=True
)
diff --git a/frigate/config/camera/notification.py b/frigate/config/camera/notification.py
index dabf94675c..7f5968193f 100644
--- a/frigate/config/camera/notification.py
+++ b/frigate/config/camera/notification.py
@@ -1,5 +1,3 @@
-from typing import Optional
-
from pydantic import Field
from ..base import FrigateBaseModel
@@ -13,7 +11,7 @@ class NotificationConfig(FrigateBaseModel):
title="Enable notifications",
description="Enable or disable notifications for all cameras; can be overridden per-camera.",
)
- email: Optional[str] = Field(
+ email: str | None = Field(
default=None,
title="Notification email",
description="Email address used for push notifications or required by certain notification providers.",
@@ -24,7 +22,7 @@ class NotificationConfig(FrigateBaseModel):
title="Cooldown period",
description="Cooldown (seconds) between notifications to avoid spamming recipients.",
)
- enabled_in_config: Optional[bool] = Field(
+ enabled_in_config: bool | None = Field(
default=None,
title="Original notifications state",
description="Indicates whether notifications were enabled in the original static configuration.",
diff --git a/frigate/config/camera/objects.py b/frigate/config/camera/objects.py
index e93778f236..6b6759edf1 100644
--- a/frigate/config/camera/objects.py
+++ b/frigate/config/camera/objects.py
@@ -1,4 +1,4 @@
-from typing import Any, Optional, Union
+from typing import Any
from pydantic import Field, PrivateAttr, field_serializer, field_validator
@@ -12,12 +12,12 @@ DEFAULT_TRACKED_OBJECTS = ["person"]
class FilterConfig(FrigateBaseModel):
- min_area: Union[int, float] = Field(
+ min_area: int | float = Field(
default=0,
title="Minimum object area",
description="Minimum bounding box area (pixels or percentage) required for this object type. Can be pixels (int) or percentage (float between 0.000001 and 0.99).",
)
- max_area: Union[int, float] = Field(
+ max_area: int | float = Field(
default=24000000,
title="Maximum object area",
description="Maximum bounding box area (pixels or percentage) allowed for this object type. Can be pixels (int) or percentage (float between 0.000001 and 0.99).",
@@ -42,12 +42,12 @@ class FilterConfig(FrigateBaseModel):
title="Minimum confidence",
description="Minimum single-frame detection confidence required for the object to be counted.",
)
- mask: dict[str, Optional[ObjectMaskConfig]] = Field(
+ mask: dict[str, ObjectMaskConfig | None] = Field(
default_factory=dict,
title="Filter mask",
description="Polygon coordinates defining where this filter applies within the frame.",
)
- raw_mask: dict[str, Optional[ObjectMaskConfig]] = Field(
+ raw_mask: dict[str, ObjectMaskConfig | None] = Field(
default_factory=dict, exclude=True
)
@@ -68,7 +68,7 @@ class GenAIObjectTriggerConfig(FrigateBaseModel):
title="Send on end",
description="Send a request to GenAI when the tracked object ends.",
)
- after_significant_updates: Optional[int] = Field(
+ after_significant_updates: int | None = Field(
default=None,
title="Early GenAI trigger",
description="Send a request to GenAI after a specified number of significant updates for the tracked object.",
@@ -98,12 +98,12 @@ class GenAIObjectConfig(FrigateBaseModel):
description="Per-object prompts to customize GenAI outputs for specific labels.",
)
- objects: Union[str, list[str]] = Field(
+ objects: str | list[str] = Field(
default_factory=list,
title="GenAI objects",
description="List of object labels to send to GenAI by default.",
)
- required_zones: Union[str, list[str]] = Field(
+ required_zones: str | list[str] = Field(
default_factory=list,
title="Required zones",
description="Zones that must be entered for objects to qualify for GenAI description generation.",
@@ -118,7 +118,7 @@ class GenAIObjectConfig(FrigateBaseModel):
title="GenAI triggers",
description="Defines when frames should be sent to GenAI (on end, after updates, etc.).",
)
- enabled_in_config: Optional[bool] = Field(
+ enabled_in_config: bool | None = Field(
default=None,
title="Original GenAI state",
description="Indicates whether GenAI was enabled in the original static config.",
@@ -144,12 +144,12 @@ class ObjectConfig(FrigateBaseModel):
title="Object filters",
description="Filters applied to detected objects to reduce false positives (area, ratio, confidence).",
)
- mask: dict[str, Optional[ObjectMaskConfig]] = Field(
+ mask: dict[str, ObjectMaskConfig | None] = Field(
default_factory=dict,
title="Object mask",
description="Mask polygon used to prevent object detection in specified areas.",
)
- raw_mask: dict[str, Optional[ObjectMaskConfig]] = Field(
+ raw_mask: dict[str, ObjectMaskConfig | None] = Field(
default_factory=dict, exclude=True
)
genai: GenAIObjectConfig = Field(
diff --git a/frigate/config/camera/onvif.py b/frigate/config/camera/onvif.py
index 836dec6aae..01c9432819 100644
--- a/frigate/config/camera/onvif.py
+++ b/frigate/config/camera/onvif.py
@@ -1,5 +1,4 @@
from enum import Enum
-from typing import Optional, Union
from pydantic import Field, field_validator
@@ -59,12 +58,12 @@ class PtzAutotrackConfig(FrigateBaseModel):
title="Return timeout",
description="Wait this many seconds after losing tracking before returning camera to preset position.",
)
- movement_weights: Optional[Union[str, list[str]]] = Field(
+ movement_weights: str | list[str] | None = Field(
default_factory=list,
title="Movement weights",
description="Calibration values automatically generated by camera calibration. Do not modify manually.",
)
- enabled_in_config: Optional[bool] = Field(
+ enabled_in_config: bool | None = Field(
default=None,
title="Original autotrack state",
description="Internal field to track whether autotracking was enabled in configuration.",
@@ -102,12 +101,12 @@ class OnvifConfig(FrigateBaseModel):
title="ONVIF port",
description="Port number for the ONVIF service.",
)
- user: Optional[EnvString] = Field(
+ user: EnvString | None = Field(
default=None,
title="ONVIF username",
description="Username for ONVIF authentication; some devices require admin user for ONVIF.",
)
- password: Optional[EnvString] = Field(
+ password: EnvString | None = Field(
default=None,
title="ONVIF password",
description="Password for ONVIF authentication.",
@@ -117,7 +116,7 @@ class OnvifConfig(FrigateBaseModel):
title="Disable TLS verify",
description="Skip TLS verification and disable digest auth for ONVIF (unsafe; use in safe networks only).",
)
- profile: Optional[str] = Field(
+ profile: str | None = Field(
default=None,
title="ONVIF profile",
description="Specific ONVIF media profile to use for PTZ control, matched by token or name. If not set, the first profile with valid PTZ configuration is selected automatically.",
diff --git a/frigate/config/camera/profile.py b/frigate/config/camera/profile.py
index 6a52a9ad6a..e3014aa4e6 100644
--- a/frigate/config/camera/profile.py
+++ b/frigate/config/camera/profile.py
@@ -1,7 +1,5 @@
"""Camera profile configuration for named config overrides."""
-from typing import Optional
-
from ..base import FrigateBaseModel
from ..classification import (
CameraFaceRecognitionConfig,
@@ -29,16 +27,16 @@ class CameraProfileConfig(FrigateBaseModel):
explicitly-set fields are used as overrides via exclude_unset.
"""
- enabled: Optional[bool] = None
- audio: Optional[AudioConfig] = None
- birdseye: Optional[BirdseyeCameraConfig] = None
- detect: Optional[DetectConfig] = None
- face_recognition: Optional[CameraFaceRecognitionConfig] = None
- lpr: Optional[CameraLicensePlateRecognitionConfig] = None
- motion: Optional[MotionConfig] = None
- notifications: Optional[NotificationConfig] = None
- objects: Optional[ObjectConfig] = None
- record: Optional[RecordConfig] = None
- review: Optional[ReviewConfig] = None
- snapshots: Optional[SnapshotsConfig] = None
- zones: Optional[dict[str, ZoneConfig]] = None
+ enabled: bool | None = None
+ audio: AudioConfig | None = None
+ birdseye: BirdseyeCameraConfig | None = None
+ detect: DetectConfig | None = None
+ face_recognition: CameraFaceRecognitionConfig | None = None
+ lpr: CameraLicensePlateRecognitionConfig | None = None
+ motion: MotionConfig | None = None
+ notifications: NotificationConfig | None = None
+ objects: ObjectConfig | None = None
+ record: RecordConfig | None = None
+ review: ReviewConfig | None = None
+ snapshots: SnapshotsConfig | None = None
+ zones: dict[str, ZoneConfig] | None = None
diff --git a/frigate/config/camera/record.py b/frigate/config/camera/record.py
index 1f7afc6ceb..44a71c9cb9 100644
--- a/frigate/config/camera/record.py
+++ b/frigate/config/camera/record.py
@@ -1,5 +1,4 @@
from enum import Enum
-from typing import Optional, Union
from pydantic import Field
@@ -9,6 +8,7 @@ from frigate.review.types import SeverityEnum
from ..base import FrigateBaseModel
__all__ = [
+ "ChaptersEnum",
"RecordConfig",
"RecordExportConfig",
"RecordPreviewConfig",
@@ -86,8 +86,14 @@ class RecordPreviewConfig(FrigateBaseModel):
)
+class ChaptersEnum(str, Enum):
+ none = "none"
+ recording_segments = "recording_segments"
+ review_items = "review_items"
+
+
class RecordExportConfig(FrigateBaseModel):
- hwaccel_args: Union[str, list[str]] = Field(
+ hwaccel_args: str | list[str] = Field(
default="auto",
title="Export hwaccel args",
description="Hardware acceleration args to use for export/transcode operations.",
@@ -98,6 +104,10 @@ class RecordExportConfig(FrigateBaseModel):
title="Maximum concurrent exports",
description="Maximum number of export jobs to process at the same time.",
)
+ chapters: ChaptersEnum = Field(
+ default=ChaptersEnum.review_items,
+ title="Chapter metadata to embed in exported recordings",
+ )
class RecordConfig(FrigateBaseModel):
@@ -141,7 +151,7 @@ class RecordConfig(FrigateBaseModel):
title="Preview config",
description="Settings controlling the quality of recording previews shown in the UI.",
)
- enabled_in_config: Optional[bool] = Field(
+ enabled_in_config: bool | None = Field(
default=None,
title="Original recording state",
description="Indicates whether recording was enabled in the original static configuration.",
diff --git a/frigate/config/camera/review.py b/frigate/config/camera/review.py
index fbe24c98c4..f267f46ea2 100644
--- a/frigate/config/camera/review.py
+++ b/frigate/config/camera/review.py
@@ -1,5 +1,4 @@
from enum import Enum
-from typing import Optional, Union
from pydantic import Field, field_validator
@@ -32,13 +31,13 @@ class AlertsConfig(FrigateBaseModel):
title="Alert labels",
description="List of object labels that qualify as alerts (for example: car, person).",
)
- required_zones: Union[str, list[str]] = Field(
+ required_zones: str | list[str] = Field(
default_factory=list,
title="Required zones",
description="Zones that an object must enter to be considered an alert; leave empty to allow any zone.",
)
- enabled_in_config: Optional[bool] = Field(
+ enabled_in_config: bool | None = Field(
default=None,
title="Original alerts state",
description="Tracks whether alerts were originally enabled in the static configuration.",
@@ -67,12 +66,12 @@ class DetectionsConfig(FrigateBaseModel):
description="Enable or disable detection events for all cameras; can be overridden per-camera.",
)
- labels: Optional[list[str]] = Field(
+ labels: list[str] | None = Field(
default=None,
title="Detection labels",
description="List of object labels that qualify as detection events.",
)
- required_zones: Union[str, list[str]] = Field(
+ required_zones: str | list[str] = Field(
default_factory=list,
title="Required zones",
description="Zones that an object must enter to be considered a detection; leave empty to allow any zone.",
@@ -83,7 +82,7 @@ class DetectionsConfig(FrigateBaseModel):
description="Seconds to wait after no detection-causing activity before cutting off a detection.",
)
- enabled_in_config: Optional[bool] = Field(
+ enabled_in_config: bool | None = Field(
default=None,
title="Original detections state",
description="Tracks whether detections were originally enabled in the static configuration.",
@@ -129,7 +128,7 @@ class GenAIReviewConfig(FrigateBaseModel):
title="Save thumbnails",
description="Save thumbnails that are sent to the GenAI provider for debugging and review.",
)
- enabled_in_config: Optional[bool] = Field(
+ enabled_in_config: bool | None = Field(
default=None,
title="Original GenAI state",
description="Tracks whether GenAI review was originally enabled in the static configuration.",
diff --git a/frigate/config/camera/snapshots.py b/frigate/config/camera/snapshots.py
index 63bcba2267..7f5590a06c 100644
--- a/frigate/config/camera/snapshots.py
+++ b/frigate/config/camera/snapshots.py
@@ -1,9 +1,6 @@
-from typing import Optional
-
from pydantic import Field
from ..base import FrigateBaseModel
-from .record import RetainModeEnum
__all__ = ["SnapshotsConfig", "RetainConfig"]
@@ -14,11 +11,6 @@ class RetainConfig(FrigateBaseModel):
title="Default retention",
description="Default number of days to retain snapshots.",
)
- mode: RetainModeEnum = Field(
- default=RetainModeEnum.motion,
- title="Retention mode",
- description="Mode for retention: all (save all segments), motion (save segments with motion), or active_objects (save segments with active objects).",
- )
objects: dict[str, float] = Field(
default_factory=dict,
title="Object retention",
@@ -52,7 +44,7 @@ class SnapshotsConfig(FrigateBaseModel):
title="Required zones",
description="Zones an object must enter for a snapshot to be saved.",
)
- height: Optional[int] = Field(
+ height: int | None = Field(
default=None,
title="Snapshot height",
description="Height (pixels) to resize snapshots from API to; leave empty to preserve original size.",
diff --git a/frigate/config/camera/timestamp.py b/frigate/config/camera/timestamp.py
index 48ec8240bd..1245c311bf 100644
--- a/frigate/config/camera/timestamp.py
+++ b/frigate/config/camera/timestamp.py
@@ -1,5 +1,4 @@
from enum import Enum
-from typing import Optional
from pydantic import Field
@@ -76,7 +75,7 @@ class TimestampStyleConfig(FrigateBaseModel):
title="Timestamp thickness",
description="Line thickness of the timestamp text.",
)
- effect: Optional[TimestampEffectEnum] = Field(
+ effect: TimestampEffectEnum | None = Field(
default=None,
title="Timestamp effect",
description="Visual effect for the timestamp text (none, solid, shadow).",
diff --git a/frigate/config/camera/ui.py b/frigate/config/camera/ui.py
index 5e903b2543..7db1c537fa 100644
--- a/frigate/config/camera/ui.py
+++ b/frigate/config/camera/ui.py
@@ -16,3 +16,8 @@ class CameraUiConfig(FrigateBaseModel):
title="Show in UI",
description="Toggle whether this camera is visible everywhere in the Frigate UI. Disabling this will require manually editing the config to view this camera in the UI again.",
)
+ review: bool = Field(
+ default=True,
+ title="Show in review",
+ description="Toggle whether this camera is visible in review (the review page and its camera filter, motion review, and the history view).",
+ )
diff --git a/frigate/config/camera/updater.py b/frigate/config/camera/updater.py
index 1965f38137..afa501904a 100644
--- a/frigate/config/camera/updater.py
+++ b/frigate/config/camera/updater.py
@@ -20,11 +20,13 @@ class CameraConfigUpdateEnum(str, Enum):
ffmpeg = "ffmpeg"
live = "live"
motion = "motion" # includes motion and motion masks
+ mqtt = "mqtt"
notifications = "notifications"
objects = "objects"
object_genai = "object_genai"
onvif = "onvif"
record = "record"
+ refresh = "refresh" # signals the camera maintainer to recycle the camera process
remove = "remove" # for removing a camera
review = "review"
review_genai = "review_genai"
@@ -33,6 +35,7 @@ class CameraConfigUpdateEnum(str, Enum):
lpr = "lpr"
snapshots = "snapshots"
timestamp_style = "timestamp_style"
+ ui = "ui"
zones = "zones"
@@ -70,7 +73,12 @@ class CameraConfigUpdateSubscriber:
base_topic = "config/cameras"
- if len(self.camera_configs) == 1:
+ # global subscribers must hear every camera; only narrow per-camera workers
+ is_global_subscriber = (
+ CameraConfigUpdateEnum.add in self.topics
+ or CameraConfigUpdateEnum.remove in self.topics
+ )
+ if not is_global_subscriber and len(self.camera_configs) == 1:
base_topic += f"/{list(self.camera_configs.keys())[0]}"
self.subscriber = ConfigSubscriber(
@@ -82,8 +90,8 @@ class CameraConfigUpdateSubscriber:
self, camera: str, update_type: CameraConfigUpdateEnum, updated_config: Any
) -> None:
if update_type == CameraConfigUpdateEnum.add:
- self.config.cameras[camera] = updated_config
- self.camera_configs[camera] = updated_config
+ shared = self.config.cameras.setdefault(camera, updated_config)
+ self.camera_configs[camera] = shared
return
elif update_type == CameraConfigUpdateEnum.remove:
self.config.cameras.pop(camera, None)
@@ -119,7 +127,10 @@ class CameraConfigUpdateSubscriber:
elif update_type == CameraConfigUpdateEnum.objects:
config.objects = updated_config
elif update_type == CameraConfigUpdateEnum.record:
+ old_enabled_in_config = config.record.enabled_in_config
config.record = updated_config
+ if old_enabled_in_config != updated_config.enabled_in_config:
+ config.recreate_ffmpeg_cmds()
elif update_type == CameraConfigUpdateEnum.review:
config.review = updated_config
elif update_type == CameraConfigUpdateEnum.review_genai:
diff --git a/frigate/config/camera/zone.py b/frigate/config/camera/zone.py
index e4737f8dca..24c62e0ff6 100644
--- a/frigate/config/camera/zone.py
+++ b/frigate/config/camera/zone.py
@@ -1,6 +1,5 @@
# this uses the base model because the color is an extra attribute
import logging
-from typing import Optional, Union
import numpy as np
from pydantic import BaseModel, Field, PrivateAttr, field_validator, model_validator
@@ -13,7 +12,7 @@ logger = logging.getLogger(__name__)
class ZoneConfig(BaseModel):
- friendly_name: Optional[str] = Field(
+ friendly_name: str | None = Field(
None,
title="Zone name",
description="A user-friendly name for the zone, displayed in the Frigate UI. If not set, a formatted version of the zone name will be used.",
@@ -23,7 +22,7 @@ class ZoneConfig(BaseModel):
title="Enabled",
description="Enable or disable this zone. Disabled zones are ignored at runtime.",
)
- enabled_in_config: Optional[bool] = Field(
+ enabled_in_config: bool | None = Field(
default=None, title="Keep track of original state of zone."
)
filters: dict[str, FilterConfig] = Field(
@@ -31,11 +30,11 @@ class ZoneConfig(BaseModel):
title="Zone filters",
description="Filters to apply to objects within this zone. Used to reduce false positives or restrict which objects are considered present in the zone.",
)
- coordinates: Union[str, list[str]] = Field(
+ coordinates: str | list[str] = Field(
title="Coordinates",
description="Polygon coordinates that define the zone area. Can be a comma-separated string or a list of coordinate strings. Coordinates should be relative (0-1) or absolute (legacy).",
)
- distances: Optional[Union[str, list[str]]] = Field(
+ distances: str | list[str] | None = Field(
default_factory=list,
title="Real-world distances",
description="Optional real-world distances for each side of the zone quadrilateral, used for speed or distance calculations. Must have exactly 4 values if set.",
@@ -52,18 +51,18 @@ class ZoneConfig(BaseModel):
title="Loitering seconds",
description="Number of seconds an object must remain in the zone to be considered as loitering. Set to 0 to disable loitering detection.",
)
- speed_threshold: Optional[float] = Field(
+ speed_threshold: float | None = Field(
default=None,
ge=0.1,
title="Minimum speed",
description="Minimum speed (in real-world units if distances are set) required for an object to be considered present in the zone. Used for speed-based zone triggers.",
)
- objects: Union[str, list[str]] = Field(
+ objects: str | list[str] = Field(
default_factory=list,
title="Trigger objects",
description="List of object types (from labelmap) that can trigger this zone. Can be a string or a list of strings. If empty, all objects are considered.",
)
- _color: Optional[tuple[int, int, int]] = PrivateAttr()
+ _color: tuple[int, int, int] | None = PrivateAttr()
_contour: np.ndarray = PrivateAttr()
@property
@@ -147,7 +146,7 @@ class ZoneConfig(BaseModel):
except ValueError:
raise ValueError(
f"Invalid coordinates found in configuration file. Coordinates must be relative (between 0-1): {coordinates}"
- )
+ ) from None
if explicit:
self.coordinates = ",".join(
@@ -176,7 +175,7 @@ class ZoneConfig(BaseModel):
except ValueError:
raise ValueError(
f"Invalid coordinates found in configuration file. Coordinates must be relative (between 0-1): {coordinates}"
- )
+ ) from None
if explicit:
self.coordinates = ",".join(
diff --git a/frigate/config/camera_group.py b/frigate/config/camera_group.py
index 65319001ac..8e08f560fd 100644
--- a/frigate/config/camera_group.py
+++ b/frigate/config/camera_group.py
@@ -1,5 +1,3 @@
-from typing import Union
-
from pydantic import Field, field_validator
from .base import FrigateBaseModel
@@ -8,7 +6,7 @@ __all__ = ["CameraGroupConfig"]
class CameraGroupConfig(FrigateBaseModel):
- cameras: Union[str, list[str]] = Field(
+ cameras: str | list[str] = Field(
default_factory=list,
title="Camera list",
description="Array of camera names included in this group.",
diff --git a/frigate/config/classification.py b/frigate/config/classification.py
index 05d6edc762..f4dc5f6001 100644
--- a/frigate/config/classification.py
+++ b/frigate/config/classification.py
@@ -1,5 +1,4 @@
from enum import Enum
-from typing import Dict, List, Optional, Union
from pydantic import ConfigDict, Field, field_validator
@@ -26,6 +25,11 @@ class EnrichmentsDeviceEnum(str, Enum):
CPU = "CPU"
+class ModelSizeEnum(str, Enum):
+ small = "small"
+ large = "large"
+
+
class TriggerType(str, Enum):
THUMBNAIL = "thumbnail"
DESCRIPTION = "description"
@@ -53,17 +57,17 @@ class AudioTranscriptionConfig(FrigateBaseModel):
title="Transcription language",
description="Language code used for transcription/translation (for example 'en' for English). See https://whisper-api.com/docs/languages/ for supported language codes.",
)
- device: Optional[EnrichmentsDeviceEnum] = Field(
+ device: EnrichmentsDeviceEnum = Field(
default=EnrichmentsDeviceEnum.CPU,
title="Transcription device",
description="Device key (CPU/GPU) to run the transcription model on. Only NVIDIA CUDA GPUs are currently supported for transcription.",
)
- model_size: str = Field(
- default="small",
+ model_size: ModelSizeEnum = Field(
+ default=ModelSizeEnum.small,
title="Model size",
description="Model size to use for offline audio event transcription.",
)
- live_enabled: Optional[bool] = Field(
+ live_enabled: bool | None = Field(
default=False,
title="Live transcription",
description="Enable streaming live transcription for audio as it is received.",
@@ -93,7 +97,7 @@ class CustomClassificationStateCameraConfig(FrigateBaseModel):
class CustomClassificationStateConfig(FrigateBaseModel):
- cameras: Dict[str, CustomClassificationStateCameraConfig] = Field(
+ cameras: dict[str, CustomClassificationStateCameraConfig] = Field(
title="Classification cameras",
description="Per-camera crop and settings for running state classification.",
)
@@ -155,7 +159,7 @@ class ClassificationConfig(FrigateBaseModel):
title="Bird classification config",
description="Settings specific to bird classification models.",
)
- custom: Dict[str, CustomClassificationConfig] = Field(
+ custom: dict[str, CustomClassificationConfig] = Field(
default={},
title="Custom Classification Models",
description="Configuration for custom classification models used for objects or state detection.",
@@ -168,12 +172,12 @@ class SemanticSearchConfig(FrigateBaseModel):
title="Enable semantic search",
description="Enable or disable the semantic search feature.",
)
- reindex: Optional[bool] = Field(
+ reindex: bool | None = Field(
default=False,
title="Reindex on startup",
description="Trigger a full reindex of historical tracked objects into the embeddings database.",
)
- model: Optional[Union[SemanticSearchModelEnum, str]] = Field(
+ model: SemanticSearchModelEnum | str | None = Field(
default=SemanticSearchModelEnum.jinav1,
title="Semantic search model or GenAI provider name",
description="The embeddings model to use for semantic search (for example 'jinav1'), or the name of a GenAI provider with the embeddings role.",
@@ -189,12 +193,12 @@ class SemanticSearchConfig(FrigateBaseModel):
return v
return v
- model_size: str = Field(
- default="small",
+ model_size: ModelSizeEnum = Field(
+ default=ModelSizeEnum.small,
title="Model size",
description="Select model size; 'small' runs on CPU and 'large' typically requires GPU.",
)
- device: Optional[str] = Field(
+ device: str | None = Field(
default=None,
title="Device",
description="This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information",
@@ -202,7 +206,7 @@ class SemanticSearchConfig(FrigateBaseModel):
class TriggerConfig(FrigateBaseModel):
- friendly_name: Optional[str] = Field(
+ friendly_name: str | None = Field(
None,
title="Friendly name",
description="Optional friendly name displayed in the UI for this trigger.",
@@ -228,7 +232,7 @@ class TriggerConfig(FrigateBaseModel):
gt=0.0,
le=1.0,
)
- actions: List[TriggerAction] = Field(
+ actions: list[TriggerAction] = Field(
default=[],
title="Trigger actions",
description="List of actions to execute when trigger matches (notification, sub_label, attribute).",
@@ -238,7 +242,7 @@ class TriggerConfig(FrigateBaseModel):
class CameraSemanticSearchConfig(FrigateBaseModel):
- triggers: Dict[str, TriggerConfig] = Field(
+ triggers: dict[str, TriggerConfig] = Field(
default={},
title="Triggers",
description="Actions and matching criteria for camera-specific semantic search triggers.",
@@ -253,8 +257,8 @@ class FaceRecognitionConfig(FrigateBaseModel):
title="Enable face recognition",
description="Enable or disable face recognition for all cameras; can be overridden per-camera.",
)
- model_size: str = Field(
- default="small",
+ model_size: ModelSizeEnum = Field(
+ default=ModelSizeEnum.small,
title="Model size",
description="Model size to use for face embeddings (small/large); larger may require GPU.",
)
@@ -302,7 +306,7 @@ class FaceRecognitionConfig(FrigateBaseModel):
title="Blur confidence filter",
description="Adjust confidence scores based on image blur to reduce false positives for poor quality faces.",
)
- device: Optional[str] = Field(
+ device: str | None = Field(
default=None,
title="Device",
description="This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information",
@@ -335,8 +339,8 @@ class LicensePlateRecognitionConfig(FrigateBaseModel):
title="Enable LPR",
description="Enable or disable license plate recognition for all cameras; can be overridden per-camera.",
)
- model_size: str = Field(
- default="small",
+ model_size: ModelSizeEnum = Field(
+ default=ModelSizeEnum.small,
title="Model size",
description="Model size used for text detection/recognition. Most users should use 'small'.",
)
@@ -364,7 +368,7 @@ class LicensePlateRecognitionConfig(FrigateBaseModel):
title="Min plate length",
description="Minimum number of characters a recognized plate must contain to be considered valid.",
)
- format: Optional[str] = Field(
+ format: str | None = Field(
default=None,
title="Plate format regex",
description="Optional regex to validate recognized plate strings against an expected format.",
@@ -375,7 +379,7 @@ class LicensePlateRecognitionConfig(FrigateBaseModel):
description="Number of character mismatches allowed when comparing detected plates to known plates.",
ge=0,
)
- known_plates: Optional[Dict[str, List[str]]] = Field(
+ known_plates: dict[str, list[str]] | None = Field(
default={},
title="Known plates",
description="List of plates or regexes to specially track or alert on.",
@@ -392,12 +396,12 @@ class LicensePlateRecognitionConfig(FrigateBaseModel):
title="Save debug plates",
description="Save plate crop images for debugging LPR performance.",
)
- device: Optional[str] = Field(
+ device: str | None = Field(
default=None,
title="Device",
description="This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information",
)
- replace_rules: List[ReplaceRule] = Field(
+ replace_rules: list[ReplaceRule] = Field(
default_factory=list,
title="Replacement rules",
description="Regex replacement rules used to normalize detected plate strings before matching.",
@@ -438,10 +442,10 @@ class CameraAudioTranscriptionConfig(FrigateBaseModel):
title="Enable transcription",
description="Enable or disable manually triggered audio event transcription.",
)
- enabled_in_config: Optional[bool] = Field(
+ enabled_in_config: bool | None = Field(
default=None, title="Original transcription state"
)
- live_enabled: Optional[bool] = Field(
+ live_enabled: bool | None = Field(
default=False,
title="Live transcription",
description="Enable streaming live transcription for audio as it is received.",
diff --git a/frigate/config/config.py b/frigate/config/config.py
index 1d09016f64..b450d26e91 100644
--- a/frigate/config/config.py
+++ b/frigate/config/config.py
@@ -1,9 +1,10 @@
from __future__ import annotations
+import io
import json
import logging
import os
-from typing import Any, Dict, Optional
+from typing import Any, Self
import numpy as np
from pydantic import (
@@ -16,7 +17,6 @@ from pydantic import (
model_validator,
)
from ruamel.yaml import YAML
-from typing_extensions import Self
from frigate.const import REGEX_JSON
from frigate.detectors import DetectorConfig, ModelConfig
@@ -25,7 +25,6 @@ from frigate.plus import PlusApi
from frigate.util.builtin import (
deep_merge,
get_ffmpeg_arg_list,
- load_labels,
)
from frigate.util.config import (
CURRENT_CONFIG_VERSION,
@@ -80,17 +79,41 @@ logger = logging.getLogger(__name__)
yaml = YAML()
+# Pydantic field default applied when an existing config omits `detectors:`.
+# Kept as cpu tflite for backwards compatibility with 0.17 configs.
+DEFAULT_DETECTORS = {"cpu": {"type": "cpu"}}
+
+# Used by the openvino branch below and rendered into the new-config YAML
+# template so first-time setups default to openvino on CPU.
+DEFAULT_MODEL = {
+ "width": 300,
+ "height": 300,
+ "input_tensor": "nhwc",
+ "input_pixel_format": "bgr",
+ "path": "/openvino-model/ssdlite_mobilenet_v2.xml",
+ "labelmap_path": "/openvino-model/coco_91cl_bkgr.txt",
+}
+NEW_CONFIG_DETECTORS = {"ov": {"type": "openvino", "device": "CPU"}}
+DEFAULT_DETECT_DIMENSIONS = {"width": 1280, "height": 720}
+
+
+def _render_default_yaml(data: dict) -> str:
+ buf = io.StringIO()
+ _yaml_writer = YAML()
+ _yaml_writer.indent(mapping=2, sequence=4, offset=2)
+ _yaml_writer.dump(data, buf)
+ return buf.getvalue()
+
+
DEFAULT_CONFIG = f"""
mqtt:
enabled: False
+{_render_default_yaml({"detectors": NEW_CONFIG_DETECTORS, "model": DEFAULT_MODEL})}
cameras: {{}} # No cameras defined, UI wizard should be used
version: {CURRENT_CONFIG_VERSION}
"""
-DEFAULT_DETECTORS = {"cpu": {"type": "cpu"}}
-DEFAULT_DETECT_DIMENSIONS = {"width": 1280, "height": 720}
-
# stream info handler
stream_info_retriever = StreamInfoRetriever()
@@ -150,7 +173,7 @@ class RuntimeMotionConfig(MotionConfig):
class RuntimeFilterConfig(FilterConfig):
"""Runtime version of FilterConfig with rasterized masks."""
- rasterized_mask: Optional[np.ndarray] = Field(default=None, exclude=True)
+ rasterized_mask: np.ndarray | None = Field(default=None, exclude=True)
def __init__(self, **config):
frame_shape = config.get("frame_shape", (1, 1))
@@ -269,7 +292,7 @@ def verify_recording_segments_setup_with_reasonable_time(
raise ValueError(
f"Camera {camera_config.name} has no segment_time in \
recording output args, segment args are required for record."
- )
+ ) from None
if int(record_args[seg_arg_index + 1]) > 60:
raise ValueError(
@@ -302,6 +325,47 @@ def verify_required_zones_exist(camera_config: CameraConfig) -> None:
)
+def verify_profile_overrides_match_base(camera_config: CameraConfig) -> None:
+ """Verify that profile zone and mask IDs reference entries defined on the base camera."""
+ for profile_name, profile in camera_config.profiles.items():
+ if profile.zones:
+ for zone_name in profile.zones:
+ if zone_name not in camera_config.zones:
+ raise ValueError(
+ f"Camera '{camera_config.name}' profile '{profile_name}' defines "
+ f"zone '{zone_name}' that does not exist on the base config"
+ )
+
+ if profile.motion and profile.motion.mask:
+ for mask_name in profile.motion.mask:
+ if mask_name not in camera_config.motion.mask:
+ raise ValueError(
+ f"Camera '{camera_config.name}' profile '{profile_name}' defines "
+ f"motion mask '{mask_name}' that does not exist on the base config"
+ )
+
+ if profile.objects:
+ for mask_name in profile.objects.mask or {}:
+ if mask_name not in (camera_config.objects.mask or {}):
+ raise ValueError(
+ f"Camera '{camera_config.name}' profile '{profile_name}' defines "
+ f"object mask '{mask_name}' that does not exist on the base config"
+ )
+ for label, filter_config in (profile.objects.filters or {}).items():
+ base_filter = (camera_config.objects.filters or {}).get(label)
+ profile_filter_masks = (
+ filter_config.mask if filter_config else None
+ ) or {}
+ base_filter_masks = (base_filter.mask if base_filter else None) or {}
+ for mask_name in profile_filter_masks:
+ if mask_name not in base_filter_masks:
+ raise ValueError(
+ f"Camera '{camera_config.name}' profile '{profile_name}' defines "
+ f"object mask '{mask_name}' for '{label}' that does not exist "
+ f"on the base config"
+ )
+
+
def verify_autotrack_zones(camera_config: CameraConfig) -> ValueError | None:
"""Verify that required_zones are specified when autotracking is enabled."""
if (
@@ -336,6 +400,9 @@ def verify_objects_track(
)
camera_config.objects.track = valid_objects
+ for label in invalid_objects:
+ camera_config.objects.filters.pop(label, None)
+
def verify_lpr_and_face(
frigate_config: FrigateConfig, camera_config: CameraConfig
@@ -355,7 +422,7 @@ def verify_lpr_and_face(
class FrigateConfig(FrigateBaseModel):
- version: Optional[str] = Field(
+ version: str | None = Field(
default=None,
title="Current config version",
description="Numeric or string version of the active configuration to help detect migrations or format changes.",
@@ -431,7 +498,7 @@ class FrigateConfig(FrigateBaseModel):
)
# Detector config
- detectors: Dict[str, BaseDetectorConfig] = Field(
+ detectors: dict[str, BaseDetectorConfig] = Field(
default=DEFAULT_DETECTORS,
title="Detector hardware",
description="Configuration for object detectors (CPU, GPU, ONNX backends) and any detector-specific model settings.",
@@ -443,17 +510,17 @@ class FrigateConfig(FrigateBaseModel):
)
# GenAI config (named provider configs: name -> GenAIConfig)
- genai: Dict[str, GenAIConfig] = Field(
+ genai: dict[str, GenAIConfig] = Field(
default_factory=dict,
title="Generative AI configuration",
description="Settings for integrated generative AI providers used to generate object descriptions and review summaries.",
)
# Camera config
- cameras: Dict[str, CameraConfig] = Field(title="Cameras", description="Cameras")
+ cameras: dict[str, CameraConfig] = Field(title="Cameras", description="Cameras")
audio: AudioConfig = Field(
default_factory=AudioConfig,
- title="Audio events",
+ title="Audio detection",
description="Settings for audio-based event detection for all cameras; can be overridden per-camera.",
)
birdseye: BirdseyeConfig = Field(
@@ -476,7 +543,7 @@ class FrigateConfig(FrigateBaseModel):
title="Live playback",
description="Settings to control the jsmpeg live stream resolution and quality. This does not affect restreamed cameras that use go2rtc for live view.",
)
- motion: Optional[MotionConfig] = Field(
+ motion: MotionConfig | None = Field(
default=None,
title="Motion detection",
description="Default motion detection settings applied to cameras unless overridden per-camera.",
@@ -534,19 +601,19 @@ class FrigateConfig(FrigateBaseModel):
description="License plate recognition settings including detection thresholds, formatting, and known plates.",
)
- camera_groups: Dict[str, CameraGroupConfig] = Field(
+ camera_groups: dict[str, CameraGroupConfig] = Field(
default_factory=dict,
title="Camera groups",
description="Configuration for named camera groups used to organize cameras in the UI.",
)
- profiles: Dict[str, ProfileDefinitionConfig] = Field(
+ profiles: dict[str, ProfileDefinitionConfig] = Field(
default_factory=dict,
title="Profiles",
description="Named profile definitions with friendly names. Camera profiles must reference names defined here.",
)
- active_profile: Optional[str] = Field(
+ active_profile: str | None = Field(
default=None,
title="Active profile",
description="Currently active profile name. Runtime-only, not persisted in YAML.",
@@ -605,26 +672,29 @@ class FrigateConfig(FrigateBaseModel):
# set default min_score for object attributes
for attribute in self.model.all_attributes:
- if not self.objects.filters.get(attribute):
+ existing = self.objects.filters.get(attribute)
+ if existing is None:
self.objects.filters[attribute] = FilterConfig(min_score=0.7)
- elif self.objects.filters[attribute].min_score == 0.5:
- self.objects.filters[attribute].min_score = 0.7
+ elif "min_score" not in existing.model_fields_set:
+ existing.min_score = 0.7
# auto detect hwaccel args
if self.ffmpeg.hwaccel_args == "auto":
self.ffmpeg.hwaccel_args = auto_detect_hwaccel()
- # Populate global audio filters for all audio labels
- all_audio_labels = {
- label
- for label in load_labels("/audio-labelmap.txt", prefill=521).values()
- if label
- }
+ # Resolve global export hwaccel_args so it matches the per-camera
+ # resolution below. Without this, every camera reads as overriding
+ # record.export.hwaccel_args because the global stays "auto" while
+ # the camera value gets resolved to the actual args list.
+ if self.record.export.hwaccel_args == "auto":
+ self.record.export.hwaccel_args = self.ffmpeg.hwaccel_args
+ # Populate global audio filters from listen. Existing user-defined
+ # entries for labels not in listen are preserved but unused at runtime.
if self.audio.filters is None:
self.audio.filters = {}
- for key in sorted(all_audio_labels - self.audio.filters.keys()):
+ for key in sorted(set(self.audio.listen) - self.audio.filters.keys()):
self.audio.filters[key] = AudioFilterConfig()
self.audio.filters = dict(sorted(self.audio.filters.items()))
@@ -679,6 +749,9 @@ class FrigateConfig(FrigateBaseModel):
model_config["path"] = "/cpu_model.tflite"
elif detector_config.type == "edgetpu":
model_config["path"] = "/edgetpu_model.tflite"
+ elif detector_config.type == "openvino":
+ for default_key, default_value in DEFAULT_MODEL.items():
+ model_config.setdefault(default_key, default_value)
model = ModelConfig.model_validate(model_config)
model.check_and_load_plus_model(self.plus_api, detector_config.type)
@@ -730,6 +803,9 @@ class FrigateConfig(FrigateBaseModel):
)
if need_detect_dimensions:
+ logger.info(
+ f"detect.width and detect.height not set for {camera_config.name}, probing detect stream to determine resolution."
+ )
stream_info = {"width": 0, "height": 0, "fourcc": None}
try:
stream_info = stream_info_retriever.get_stream_info(
@@ -810,7 +886,9 @@ class FrigateConfig(FrigateBaseModel):
if camera_config.audio.filters is None:
camera_config.audio.filters = {}
- for key in sorted(all_audio_labels - camera_config.audio.filters.keys()):
+ for key in sorted(
+ set(camera_config.audio.listen) - camera_config.audio.filters.keys()
+ ):
camera_config.audio.filters[key] = AudioFilterConfig()
camera_config.audio.filters = dict(
@@ -832,7 +910,9 @@ class FrigateConfig(FrigateBaseModel):
if mask_config:
coords = mask_config.coordinates
relative_coords = get_relative_coordinates(
- coords, camera_config.frame_shape
+ coords,
+ camera_config.frame_shape,
+ camera_name=camera_config.name,
)
# Create a new ObjectMaskConfig with raw_coordinates set
processed_global_masks[mask_id] = ObjectMaskConfig(
@@ -922,6 +1002,7 @@ class FrigateConfig(FrigateBaseModel):
verify_recording_segments_setup_with_reasonable_time(camera_config)
verify_zone_objects_are_tracked(camera_config)
verify_required_zones_exist(camera_config)
+ verify_profile_overrides_match_base(camera_config)
verify_autotrack_zones(camera_config)
verify_motion_and_detect(camera_config)
verify_objects_track(camera_config, labelmap_objects)
@@ -975,7 +1056,7 @@ class FrigateConfig(FrigateBaseModel):
@field_validator("cameras")
@classmethod
- def ensure_zones_and_cameras_have_different_names(cls, v: Dict[str, CameraConfig]):
+ def ensure_zones_and_cameras_have_different_names(cls, v: dict[str, CameraConfig]):
zones = [zone for camera in v.values() for zone in camera.zones.keys()]
for zone in zones:
if zone in v.keys():
@@ -1057,7 +1138,7 @@ class FrigateConfig(FrigateBaseModel):
@classmethod
def parse_object(
- cls, obj: Any, *, plus_api: Optional[PlusApi] = None, install: bool = False
+ cls, obj: Any, *, plus_api: PlusApi | None = None, install: bool = False
):
return cls.model_validate(
obj, context={"plus_api": plus_api, "install": install}
diff --git a/frigate/config/logger.py b/frigate/config/logger.py
index c8920a198a..906f75e4a3 100644
--- a/frigate/config/logger.py
+++ b/frigate/config/logger.py
@@ -1,5 +1,6 @@
+from typing import Self
+
from pydantic import Field, ValidationInfo, model_validator
-from typing_extensions import Self
from frigate.log import LogLevel, apply_log_levels
diff --git a/frigate/config/mqtt.py b/frigate/config/mqtt.py
index becbe7e69a..28c002a0de 100644
--- a/frigate/config/mqtt.py
+++ b/frigate/config/mqtt.py
@@ -1,7 +1,6 @@
-from typing import Optional
+from typing import Self
from pydantic import Field, ValidationInfo, model_validator
-from typing_extensions import Self
from frigate.const import FREQUENCY_STATS_POINTS
@@ -43,33 +42,33 @@ class MqttConfig(FrigateBaseModel):
title="Stats interval",
description="Interval in seconds for publishing system and camera stats to MQTT.",
)
- user: Optional[EnvString] = Field(
+ user: EnvString | None = Field(
default=None,
title="MQTT username",
description="Optional MQTT username; can be provided via environment variables or secrets.",
)
- password: Optional[EnvString] = Field(
+ password: EnvString | None = Field(
default=None,
title="MQTT password",
description="Optional MQTT password; can be provided via environment variables or secrets.",
validate_default=True,
)
- tls_ca_certs: Optional[str] = Field(
+ tls_ca_certs: str | None = Field(
default=None,
title="TLS CA certs",
description="Path to CA certificate for TLS connections to the broker (for self-signed certs).",
)
- tls_client_cert: Optional[str] = Field(
+ tls_client_cert: str | None = Field(
default=None,
title="Client cert",
description="Client certificate path for TLS mutual authentication; do not set user/password when using client certs.",
)
- tls_client_key: Optional[str] = Field(
+ tls_client_key: str | None = Field(
default=None,
title="Client key",
description="Private key path for the client certificate.",
)
- tls_insecure: Optional[bool] = Field(
+ tls_insecure: bool | None = Field(
default=None,
title="TLS insecure",
description="Allow insecure TLS connections by skipping hostname verification (not recommended).",
diff --git a/frigate/config/network.py b/frigate/config/network.py
index f537c73b9d..7feaab0127 100644
--- a/frigate/config/network.py
+++ b/frigate/config/network.py
@@ -1,5 +1,3 @@
-from typing import Union
-
from pydantic import Field
from .base import FrigateBaseModel
@@ -16,12 +14,12 @@ class IPv6Config(FrigateBaseModel):
class ListenConfig(FrigateBaseModel):
- internal: Union[int, str] = Field(
+ internal: int | str = Field(
default=5000,
title="Internal port",
description="Internal listening port for Frigate (default 5000).",
)
- external: Union[int, str] = Field(
+ external: int | str = Field(
default=8971,
title="External port",
description="External listening port for Frigate (default 8971).",
diff --git a/frigate/config/profile_manager.py b/frigate/config/profile_manager.py
index d109bdecbc..cfd0dc9df5 100644
--- a/frigate/config/profile_manager.py
+++ b/frigate/config/profile_manager.py
@@ -3,9 +3,10 @@
import copy
import json
import logging
-from datetime import datetime, timezone
+from collections.abc import Callable
+from datetime import UTC, datetime
from pathlib import Path
-from typing import Optional
+from typing import Any
from frigate.config.camera.updater import (
CameraConfigUpdateEnum,
@@ -34,6 +35,45 @@ PROFILE_SECTION_UPDATES: dict[str, CameraConfigUpdateEnum] = {
"zones": CameraConfigUpdateEnum.zones,
}
+# Retained MQTT switch topics per profile section, with a payload getter.
+# Republished on profile change so MQTT/HA don't show a stale toggle.
+SECTION_STATE_TOPICS: dict[str, list[tuple[str, Callable[[Any], Any]]]] = {
+ "audio": [("audio", lambda c: "ON" if c.audio.enabled else "OFF")],
+ "birdseye": [
+ ("birdseye", lambda c: "ON" if c.birdseye.enabled else "OFF"),
+ (
+ "birdseye_mode",
+ lambda c: c.birdseye.mode.value.upper() if c.birdseye.enabled else "OFF",
+ ),
+ ],
+ "detect": [("detect", lambda c: "ON" if c.detect.enabled else "OFF")],
+ "motion": [
+ ("motion", lambda c: "ON" if c.motion.enabled else "OFF"),
+ ("improve_contrast", lambda c: "ON" if c.motion.improve_contrast else "OFF"),
+ ("motion_threshold", lambda c: c.motion.threshold),
+ ("motion_contour_area", lambda c: c.motion.contour_area),
+ ],
+ "notifications": [
+ ("notifications", lambda c: "ON" if c.notifications.enabled else "OFF"),
+ ],
+ "objects": [
+ ("object_descriptions", lambda c: "ON" if c.objects.genai.enabled else "OFF"),
+ ],
+ "record": [("recordings", lambda c: "ON" if c.record.enabled else "OFF")],
+ "review": [
+ ("review_alerts", lambda c: "ON" if c.review.alerts.enabled else "OFF"),
+ (
+ "review_detections",
+ lambda c: "ON" if c.review.detections.enabled else "OFF",
+ ),
+ (
+ "review_descriptions",
+ lambda c: "ON" if c.review.genai.enabled else "OFF",
+ ),
+ ],
+ "snapshots": [("snapshots", lambda c: "ON" if c.snapshots.enabled else "OFF")],
+}
+
PERSISTENCE_FILE = Path(CONFIG_DIR) / ".profiles"
@@ -124,11 +164,24 @@ class ProfileManager:
self.config.active_profile = None
self._persist_active_profile(None)
- def activate_profile(self, profile_name: Optional[str]) -> Optional[str]:
+ # drop all runtime overrides so they don't replay stale values on restart
+ if self.dispatcher is not None:
+ self.dispatcher.clear_runtime_state()
+
+ def activate_profile(
+ self,
+ profile_name: str | None,
+ clear_runtime_overrides: bool = True,
+ ) -> str | None:
"""Activate a profile by name, or deactivate if None.
Args:
profile_name: Profile name to activate, or None to deactivate.
+ clear_runtime_overrides: When True (the default, for user-initiated
+ activations) drop the dispatcher's runtime override file because
+ the layer below changed. Startup callers that are replaying a
+ persisted profile pass False so the runtime state stays
+ available for the subsequent replay step.
Returns:
None on success, or an error message string on failure.
@@ -156,6 +209,11 @@ class ProfileManager:
self.config.active_profile = profile_name
self._persist_active_profile(profile_name)
+
+ # a profile switch invalidates the steady-state runtime overrides
+ if clear_runtime_overrides and self.dispatcher is not None:
+ self.dispatcher.clear_runtime_state()
+
logger.info(
"Profile %s",
f"'{profile_name}' activated" if profile_name else "deactivated",
@@ -199,7 +257,7 @@ class ProfileManager:
def _apply_profile_overrides(
self, profile_name: str, changed: dict[str, set[str]]
- ) -> Optional[str]:
+ ) -> str | None:
"""Apply profile overrides for all cameras that have the named profile."""
for cam_name, cam_config in self.config.cameras.items():
profile = cam_config.profiles.get(profile_name)
@@ -292,14 +350,23 @@ class ProfileManager:
settings,
)
- def _persist_active_profile(self, profile_name: Optional[str]) -> None:
+ # republish MQTT switch states
+ if self.dispatcher is not None:
+ for suffix, get_payload in SECTION_STATE_TOPICS.get(section, ()):
+ self.dispatcher.publish(
+ f"{cam_name}/{suffix}/state",
+ get_payload(cam_config),
+ retain=True,
+ )
+
+ def _persist_active_profile(self, profile_name: str | None) -> None:
"""Persist the active profile state to disk as JSON."""
try:
data = self._load_persisted_data()
data["active"] = profile_name
if profile_name is not None:
data.setdefault("last_activated", {})[profile_name] = datetime.now(
- timezone.utc
+ UTC
).timestamp()
PERSISTENCE_FILE.write_text(json.dumps(data))
except OSError:
@@ -318,7 +385,7 @@ class ProfileManager:
return {"active": None, "last_activated": {}}
@staticmethod
- def load_persisted_profile() -> Optional[str]:
+ def load_persisted_profile() -> str | None:
"""Load the persisted active profile name from disk."""
data = ProfileManager._load_persisted_data()
name = data.get("active")
diff --git a/frigate/config/proxy.py b/frigate/config/proxy.py
index 2426fcf104..196110520b 100644
--- a/frigate/config/proxy.py
+++ b/frigate/config/proxy.py
@@ -1,5 +1,3 @@
-from typing import Optional
-
from pydantic import Field, field_validator
from .base import FrigateBaseModel
@@ -19,7 +17,7 @@ class HeaderMappingConfig(FrigateBaseModel):
title="Role header",
description="Header containing the authenticated user's role or groups from the upstream proxy.",
)
- role_map: Optional[dict[str, list[str]]] = Field(
+ role_map: dict[str, list[str]] | None = Field(
default_factory=dict,
title=("Role mapping"),
description="Map upstream group values to Frigate roles (for example map admin groups to the admin role).",
@@ -32,22 +30,22 @@ class ProxyConfig(FrigateBaseModel):
title="Header mapping",
description="Map incoming proxy headers to Frigate user and role fields for proxy-based auth.",
)
- logout_url: Optional[str] = Field(
+ logout_url: str | None = Field(
default=None,
title="Logout URL",
description="URL to redirect users to when logging out via the proxy.",
)
- auth_secret: Optional[EnvString] = Field(
+ auth_secret: EnvString | None = Field(
default=None,
title="Proxy secret",
description="Optional secret checked against the X-Proxy-Secret header to verify trusted proxies.",
)
- default_role: Optional[str] = Field(
+ default_role: str | None = Field(
default="viewer",
title="Default role",
- description="Default role assigned to proxy-authenticated users when no role mapping applies (admin or viewer).",
+ description="Default role assigned to proxy-authenticated users when no role mapping applies.",
)
- separator: Optional[str] = Field(
+ separator: str | None = Field(
default=",",
title="Separator character",
description="Character used to split multiple values provided in proxy headers.",
diff --git a/frigate/config/telemetry.py b/frigate/config/telemetry.py
index 41c3f7bbc2..3c219d7460 100644
--- a/frigate/config/telemetry.py
+++ b/frigate/config/telemetry.py
@@ -1,5 +1,3 @@
-from typing import Optional
-
from pydantic import Field
from .base import FrigateBaseModel
@@ -23,10 +21,10 @@ class StatsConfig(FrigateBaseModel):
title="Network bandwidth",
description="Enable per-process network bandwidth monitoring for camera ffmpeg processes and detectors (requires capabilities).",
)
- intel_gpu_device: Optional[str] = Field(
+ intel_gpu_device: str | None = Field(
default=None,
- title="SR-IOV device",
- description="Device identifier used when treating Intel GPUs as SR-IOV to fix GPU stats.",
+ title="Intel GPU device",
+ description="PCI bus address or DRM device path (e.g. /dev/dri/card1) used to pin Intel GPU stats to a specific device when multiple are present.",
)
diff --git a/frigate/config/ui.py b/frigate/config/ui.py
index 2c3104bbc9..5958c0e2c5 100644
--- a/frigate/config/ui.py
+++ b/frigate/config/ui.py
@@ -1,11 +1,10 @@
from enum import Enum
-from typing import Optional
from pydantic import Field
from .base import FrigateBaseModel
-__all__ = ["TimeFormatEnum", "DateTimeStyleEnum", "UnitSystemEnum", "UIConfig"]
+__all__ = ["TimeFormatEnum", "UnitSystemEnum", "UIConfig"]
class TimeFormatEnum(str, Enum):
@@ -14,20 +13,13 @@ class TimeFormatEnum(str, Enum):
hours24 = "24hour"
-class DateTimeStyleEnum(str, Enum):
- full = "full"
- long = "long"
- medium = "medium"
- short = "short"
-
-
class UnitSystemEnum(str, Enum):
imperial = "imperial"
metric = "metric"
class UIConfig(FrigateBaseModel):
- timezone: Optional[str] = Field(
+ timezone: str | None = Field(
default=None,
title="Timezone",
description="Optional timezone to display across the UI (defaults to browser local time if unset).",
@@ -37,16 +29,6 @@ class UIConfig(FrigateBaseModel):
title="Time format",
description="Time format to use in the UI (browser, 12hour, or 24hour).",
)
- date_style: DateTimeStyleEnum = Field(
- default=DateTimeStyleEnum.short,
- title="Date style",
- description="Date style to use in the UI (full, long, medium, short).",
- )
- time_style: DateTimeStyleEnum = Field(
- default=DateTimeStyleEnum.medium,
- title="Time style",
- description="Time style to use in the UI (full, long, medium, short).",
- )
unit_system: UnitSystemEnum = Field(
default=UnitSystemEnum.metric,
title="Unit system",
diff --git a/frigate/const.py b/frigate/const.py
index 51e06e4ad8..dac04c4f1a 100644
--- a/frigate/const.py
+++ b/frigate/const.py
@@ -15,12 +15,14 @@ TRIGGER_DIR = f"{CLIPS_DIR}/triggers"
BIRDSEYE_PIPE = "/tmp/cache/birdseye"
CACHE_DIR = "/tmp/cache"
REPLAY_CAMERA_PREFIX = "_replay_"
-REPLAY_DIR = os.path.join(CACHE_DIR, "replay")
+REPLAY_DIR = os.path.join(CLIPS_DIR, "replay")
PLUS_ENV_VAR = "PLUS_API_KEY"
PLUS_API_HOST = "https://api.frigate.video"
SHM_FRAMES_VAR = "SHM_MAX_FRAMES"
+REDACTED_CREDENTIAL_SENTINEL = "__FRIGATE_SAVED_CREDENTIAL__"
+
# Attribute & Object constants
DEFAULT_ATTRIBUTE_LABEL_MAP = {
diff --git a/frigate/data_processing/common/face/model.py b/frigate/data_processing/common/face/model.py
index 45e8b8939e..87293f7f02 100644
--- a/frigate/data_processing/common/face/model.py
+++ b/frigate/data_processing/common/face/model.py
@@ -133,6 +133,61 @@ class FaceRecognizer(ABC):
return 0.0
+def build_class_mean(
+ embs: list[np.ndarray],
+ trim: float = 0.15,
+ outlier_threshold: float = 0.30,
+ min_keep_frac: float = 0.7,
+ max_iters: int = 3,
+) -> np.ndarray:
+ """Build a class-mean embedding with two-layer outlier protection.
+
+ Layer 1 (iterative, vector-wise): drop whole embeddings whose cosine
+ similarity to the current class mean is below ``outlier_threshold``.
+ Catches mislabeled or corrupted training samples (wrong face in the
+ folder, full-frame screenshots, extreme crops) that per-dimension
+ trimming cannot detect.
+
+ Layer 2 (per-dimension): ``scipy.stats.trim_mean`` on the retained set
+ to smooth per-component noise (lighting, expression, alignment jitter).
+
+ Collections with fewer than 5 images bypass outlier rejection — too few
+ samples to establish a reliable class center.
+ """
+ arr = np.stack(embs, axis=0)
+
+ if len(arr) < 5:
+ return np.asarray(stats.trim_mean(arr, trim, axis=0))
+
+ keep = np.ones(len(arr), dtype=bool)
+ floor = max(5, int(np.ceil(min_keep_frac * len(arr))))
+
+ for _ in range(max_iters):
+ mean = stats.trim_mean(arr[keep], trim, axis=0)
+ m_norm = mean / (np.linalg.norm(mean) + 1e-9)
+ e_norms = arr / (np.linalg.norm(arr, axis=1, keepdims=True) + 1e-9)
+ cos = e_norms @ m_norm
+ new_keep = cos >= outlier_threshold
+
+ if new_keep.sum() < floor:
+ top = np.argsort(-cos)[:floor]
+ new_keep = np.zeros(len(arr), dtype=bool)
+ new_keep[top] = True
+
+ if np.array_equal(new_keep, keep):
+ break
+ keep = new_keep
+
+ dropped = int((~keep).sum())
+
+ if dropped:
+ logger.debug(
+ f"Vector-wise outlier filter dropped {dropped}/{len(arr)} embeddings"
+ )
+
+ return np.asarray(stats.trim_mean(arr[keep], trim, axis=0))
+
+
def similarity_to_confidence(
cosine_similarity: float,
median: float = 0.3,
@@ -229,7 +284,7 @@ class FaceNetRecognizer(FaceRecognizer):
for name, embs in face_embeddings_map.items():
if embs:
- self.mean_embs[name] = stats.trim_mean(embs, 0.15)
+ self.mean_embs[name] = build_class_mean(embs)
logger.debug("Finished building ArcFace model")
@@ -340,7 +395,7 @@ class ArcFaceRecognizer(FaceRecognizer):
for name, embs in face_embeddings_map.items():
if embs:
- self.mean_embs[name] = stats.trim_mean(embs, 0.15)
+ self.mean_embs[name] = build_class_mean(embs)
logger.debug("Finished building ArcFace model")
diff --git a/frigate/data_processing/common/license_plate/mixin.py b/frigate/data_processing/common/license_plate/mixin.py
index f767a5c2f4..432787e40f 100644
--- a/frigate/data_processing/common/license_plate/mixin.py
+++ b/frigate/data_processing/common/license_plate/mixin.py
@@ -10,7 +10,7 @@ import random
import re
import string
from pathlib import Path
-from typing import Any, List, Tuple
+from typing import Any
import cv2
import numpy as np
@@ -86,13 +86,15 @@ class LicensePlateProcessingMixin:
self.similarity_threshold = 0.8
self.cluster_threshold = 0.85
- def _detect(self, image: np.ndarray) -> List[np.ndarray]:
+ def _detect(self, image: np.ndarray, debug_frame_id: int) -> list[np.ndarray]:
"""
Detect possible areas of text in the input image by first resizing and normalizing it,
running a detection model, and filtering out low-probability regions.
Args:
image (np.ndarray): The input image in which license plates will be detected.
+ debug_frame_id (int): Shared id used to name debug images so all artifacts
+ from a single LPR pass share the same filename suffix.
Returns:
List[np.ndarray]: A list of bounding box coordinates representing detected license plates.
@@ -106,9 +108,8 @@ class LicensePlateProcessingMixin:
normalized_image = self._normalize_image(resized_image)
if WRITE_DEBUG_IMAGES:
- current_time = int(datetime.datetime.now().timestamp())
cv2.imwrite(
- f"debug/frames/license_plate_resized_{current_time}.jpg",
+ f"debug/frames/license_plate_resized_{debug_frame_id}.jpg",
resized_image,
)
@@ -131,8 +132,8 @@ class LicensePlateProcessingMixin:
return self._filter_polygon(boxes, (h, w)) # type: ignore[return-value,arg-type]
def _classify(
- self, images: List[np.ndarray]
- ) -> Tuple[List[np.ndarray], List[Tuple[str, float]]] | None:
+ self, images: list[np.ndarray]
+ ) -> tuple[list[np.ndarray], list[tuple[str, float]]] | None:
"""
Classify the orientation or category of each detected license plate.
@@ -162,8 +163,8 @@ class LicensePlateProcessingMixin:
return self._process_classification_output(images, outputs)
def _recognize(
- self, camera: str, images: List[np.ndarray]
- ) -> Tuple[List[str], List[List[float]]]:
+ self, camera: str, images: list[np.ndarray]
+ ) -> tuple[list[str], list[list[float]]]:
"""
Recognize the characters on the detected license plates using the recognition model.
@@ -203,8 +204,8 @@ class LicensePlateProcessingMixin:
return self.ctc_decoder(outputs)
def _process_license_plate(
- self, camera: str, id: str, image: np.ndarray
- ) -> Tuple[List[str], List[List[float]], List[int]]:
+ self, camera: str, id: str, image: np.ndarray, debug_frame_id: int
+ ) -> tuple[list[str], list[list[float]], list[int]]:
"""
Complete pipeline for detecting, classifying, and recognizing license plates in the input image.
Combines multi-line plates into a single plate string, grouping boxes by vertical alignment and ordering top to bottom,
@@ -214,6 +215,8 @@ class LicensePlateProcessingMixin:
camera (str): Camera identifier.
id (str): Event identifier.
image (np.ndarray): The input image in which to detect, classify, and recognize license plates.
+ debug_frame_id (int): Shared id used to name debug images so all artifacts
+ from a single LPR pass share the same filename suffix.
Returns:
Tuple[List[str], List[List[float]], List[int]]: Detected license plate texts, character-level confidence scores for each plate (flattened into a single list per plate), and areas of the plates.
@@ -227,7 +230,7 @@ class LicensePlateProcessingMixin:
logger.debug("Model runners not loaded")
return [], [], []
- boxes = self._detect(image)
+ boxes = self._detect(image, debug_frame_id)
if len(boxes) == 0:
logger.debug(f"{camera}: No boxes found by OCR detector model")
return [], [], []
@@ -243,7 +246,6 @@ class LicensePlateProcessingMixin:
boxes, plate_width=plate_width, gap_fraction=0.1
)
- current_time = int(datetime.datetime.now().timestamp())
if WRITE_DEBUG_IMAGES:
debug_image = image.copy()
for box in boxes:
@@ -259,7 +261,7 @@ class LicensePlateProcessingMixin:
)
cv2.imwrite(
- f"debug/frames/license_plate_boxes_{current_time}.jpg", debug_image
+ f"debug/frames/license_plate_boxes_{debug_frame_id}.jpg", debug_image
)
boxes = self._sort_boxes(list(boxes))
@@ -322,7 +324,7 @@ class LicensePlateProcessingMixin:
if WRITE_DEBUG_IMAGES:
for i, img in enumerate(group_plate_images):
cv2.imwrite(
- f"debug/frames/license_plate_cropped_{current_time}_{group_indices[i] + 1}.jpg",
+ f"debug/frames/license_plate_cropped_{debug_frame_id}_{group_indices[i] + 1}.jpg",
img,
)
@@ -335,7 +337,7 @@ class LicensePlateProcessingMixin:
cv2.imwrite(
os.path.join(
CLIPS_DIR,
- f"lpr/{camera}/{id}/{current_time}_{group_indices[i] + 1}.jpg",
+ f"lpr/{camera}/{id}/{debug_frame_id}_{group_indices[i] + 1}.jpg",
),
img,
)
@@ -467,11 +469,11 @@ class LicensePlateProcessingMixin:
def _merge_nearby_boxes(
self,
- boxes: List[np.ndarray],
+ boxes: list[np.ndarray],
plate_width: float,
gap_fraction: float = 0.1,
min_overlap_fraction: float = -0.2,
- ) -> List[np.ndarray]:
+ ) -> list[np.ndarray]:
"""
Merge bounding boxes that are likely part of the same license plate based on proximity,
with a dynamic max_gap based on the provided width of the entire license plate.
@@ -553,7 +555,7 @@ class LicensePlateProcessingMixin:
def _boxes_from_bitmap(
self, output: np.ndarray, mask: np.ndarray, dest_width: int, dest_height: int
- ) -> Tuple[np.ndarray, List[float]]:
+ ) -> tuple[np.ndarray, list[float]]:
"""
Process the binary mask to extract bounding boxes and associated confidence scores.
@@ -618,7 +620,7 @@ class LicensePlateProcessingMixin:
return np.array(boxes, dtype="int32"), scores
@staticmethod
- def _get_min_boxes(contour: np.ndarray) -> Tuple[List[Tuple[float, float]], float]:
+ def _get_min_boxes(contour: np.ndarray) -> tuple[list[tuple[float, float]], float]:
"""
Calculate the minimum bounding box (rotated rectangle) for a given contour.
@@ -657,7 +659,7 @@ class LicensePlateProcessingMixin:
return cv2.mean(bitmap[y1 : y2 + 1, x1 : x2 + 1], mask)[0]
@staticmethod
- def _expand_box(points: List[Tuple[float, float]]) -> np.ndarray:
+ def _expand_box(points: list[tuple[float, float]]) -> np.ndarray:
"""
Expand a polygonal shape slightly by a factor determined by the area-to-perimeter ratio.
@@ -675,7 +677,7 @@ class LicensePlateProcessingMixin:
return expanded
def _filter_polygon(
- self, points: List[np.ndarray], shape: Tuple[int, int]
+ self, points: list[np.ndarray], shape: tuple[int, int]
) -> np.ndarray:
"""
Filter a set of polygons to include only valid ones that fit within an image shape
@@ -837,8 +839,8 @@ class LicensePlateProcessingMixin:
return padded_image
def _process_classification_output(
- self, images: List[np.ndarray], outputs: List[np.ndarray]
- ) -> Tuple[List[np.ndarray], List[Tuple[str, float]]]:
+ self, images: list[np.ndarray], outputs: list[np.ndarray]
+ ) -> tuple[list[np.ndarray], list[tuple[str, float]]]:
"""
Process the classification model output by matching labels with confidence scores.
@@ -1073,10 +1075,6 @@ class LicensePlateProcessingMixin:
top_score = score
top_box = bbox
- if score > top_score:
- top_score = score
- top_box = bbox
-
# Return the top scoring bounding box if found
if top_box is not None:
# expand box by 5% to help with OCR
@@ -1092,16 +1090,13 @@ class LicensePlateProcessingMixin:
]
).clip(0, [input.shape[1], input.shape[0]] * 2)
- logger.debug(
- f"{camera}: Found license plate. Bounding box: {expanded_box.astype(int)}"
- )
return tuple(int(x) for x in expanded_box) # type: ignore[return-value]
else:
return None # No detection above the threshold
def _get_cluster_rep(
- self, plates: List[dict]
- ) -> Tuple[str, float, List[float], int]:
+ self, plates: list[dict]
+ ) -> tuple[str, float, list[float], int]:
"""
Cluster plate variants and select the representative from the best cluster.
"""
@@ -1206,6 +1201,7 @@ class LicensePlateProcessingMixin:
self.metrics.yolov9_lpr_pps.value = self.plates_det_second.eps()
camera = obj_data if dedicated_lpr else obj_data["camera"]
current_time = int(datetime.datetime.now().timestamp())
+ debug_frame_id = int(datetime.datetime.now().timestamp() * 1000)
if not self.config.cameras[camera].lpr.enabled:
return
@@ -1221,7 +1217,7 @@ class LicensePlateProcessingMixin:
if WRITE_DEBUG_IMAGES:
cv2.imwrite(
- f"debug/frames/dedicated_lpr_masked_{current_time}.jpg",
+ f"debug/frames/dedicated_lpr_masked_{debug_frame_id}.jpg",
rgb,
)
@@ -1333,7 +1329,7 @@ class LicensePlateProcessingMixin:
if WRITE_DEBUG_IMAGES:
cv2.imwrite(
- f"debug/frames/car_frame_{current_time}.jpg",
+ f"debug/frames/car_frame_{debug_frame_id}.jpg",
car,
)
@@ -1360,8 +1356,8 @@ class LicensePlateProcessingMixin:
)
# check that license plate is valid
- # double the value because we've doubled the size of the car
- if license_plate_area < self.config.cameras[camera].lpr.min_area * 2:
+ # quadruple the value because we've doubled both dimensions of the car
+ if license_plate_area < self.config.cameras[camera].lpr.min_area * 4:
logger.debug(f"{camera}: License plate is less than min_area")
return
@@ -1461,16 +1457,17 @@ class LicensePlateProcessingMixin:
if WRITE_DEBUG_IMAGES:
cv2.imwrite(
- f"debug/frames/license_plate_frame_{current_time}.jpg",
+ f"debug/frames/license_plate_frame_{debug_frame_id}.jpg",
license_plate_frame,
)
+ logger.debug(f"{camera}: Found license plate. Bounding box: {list(plate_box)}")
logger.debug(f"{camera}: Running plate recognition for id: {id}.")
# run detection, returns results sorted by confidence, best first
start = datetime.datetime.now().timestamp()
license_plates, confidences, areas = self._process_license_plate(
- camera, id, license_plate_frame
+ camera, id, license_plate_frame, debug_frame_id
)
self.plates_rec_second.update()
self.plate_rec_speed.update(datetime.datetime.now().timestamp() - start)
@@ -1707,7 +1704,7 @@ class CTCDecoder:
"""
self.characters = []
if character_dict_path and os.path.exists(character_dict_path):
- with open(character_dict_path, "r", encoding="utf-8") as f:
+ with open(character_dict_path, encoding="utf-8") as f:
self.characters = (
["blank"] + [line.strip() for line in f if line.strip()] + [" "]
)
@@ -1815,8 +1812,8 @@ class CTCDecoder:
self.char_map = {i: char for i, char in enumerate(self.characters)}
def __call__(
- self, outputs: List[np.ndarray]
- ) -> Tuple[List[str], List[List[float]]]:
+ self, outputs: list[np.ndarray]
+ ) -> tuple[list[str], list[list[float]]]:
"""
Decode a batch of model outputs into character sequences and their confidence scores.
diff --git a/frigate/data_processing/post/audio_transcription.py b/frigate/data_processing/post/audio_transcription.py
index dbeb210287..16895dc873 100644
--- a/frigate/data_processing/post/audio_transcription.py
+++ b/frigate/data_processing/post/audio_transcription.py
@@ -4,7 +4,7 @@ import logging
import os
import threading
import time
-from typing import Any, Optional
+from typing import Any
from peewee import DoesNotExist
@@ -142,7 +142,7 @@ class AudioTranscriptionPostProcessor(PostProcessorApi):
except Exception as e:
logger.error(f"Error in audio transcription post-processing: {e}")
- def __transcribe_audio(self, audio_data: bytes) -> Optional[str]:
+ def __transcribe_audio(self, audio_data: bytes) -> str | None:
"""Transcribe WAV audio data using faster-whisper."""
if not self.recognizer:
logger.debug("Recognizer not initialized")
@@ -168,8 +168,9 @@ class AudioTranscriptionPostProcessor(PostProcessorApi):
return None
logger.debug(
- "Detected language '%s' with probability %f"
- % (info.language, info.language_probability)
+ "Detected language '%s' with probability %f",
+ info.language,
+ info.language_probability,
)
return text
diff --git a/frigate/data_processing/post/license_plate.py b/frigate/data_processing/post/license_plate.py
index aa89aeb12b..55f863cf0b 100644
--- a/frigate/data_processing/post/license_plate.py
+++ b/frigate/data_processing/post/license_plate.py
@@ -102,10 +102,8 @@ class LicensePlatePostProcessor(LicensePlateProcessingMixin, PostProcessorApi):
Recordings.start_time,
)
.where(
- (
- (frame_time >= Recordings.start_time)
- & (frame_time <= Recordings.end_time)
- )
+ (frame_time >= Recordings.start_time)
+ & (frame_time <= Recordings.end_time)
)
.where(Recordings.camera == camera_name)
.order_by(Recordings.start_time.desc())
diff --git a/frigate/data_processing/post/object_descriptions.py b/frigate/data_processing/post/object_descriptions.py
index babdb72521..6404b38516 100644
--- a/frigate/data_processing/post/object_descriptions.py
+++ b/frigate/data_processing/post/object_descriptions.py
@@ -269,7 +269,9 @@ class ObjectDescriptionProcessor(PostProcessorApi):
if event.has_snapshot and camera_config.objects.genai.use_snapshot:
snapshot_image = self._read_and_crop_snapshot(event)
+
if not snapshot_image:
+ self.cleanup_event(event_id)
return
num_thumbnails = len(self.tracked_events.get(event_id, []))
diff --git a/frigate/data_processing/post/review_descriptions.py b/frigate/data_processing/post/review_descriptions.py
index 536b57f3c5..3740ac25f7 100644
--- a/frigate/data_processing/post/review_descriptions.py
+++ b/frigate/data_processing/post/review_descriptions.py
@@ -39,6 +39,8 @@ logger = logging.getLogger(__name__)
RECORDING_BUFFER_EXTENSION_PERCENT = 0.10
MIN_RECORDING_DURATION = 10
+MAX_IMAGE_TOKENS = 24000
+MAX_FRAMES_PER_SECOND = 1
class ReviewDescriptionProcessor(PostProcessorApi):
@@ -60,14 +62,22 @@ class ReviewDescriptionProcessor(PostProcessorApi):
def calculate_frame_count(
self,
camera: str,
+ duration: float,
image_source: ImageSourceEnum = ImageSourceEnum.preview,
height: int = 480,
) -> int:
- """Calculate optimal number of frames based on context size, image source, and resolution.
+ """Calculate optimal number of frames based on event duration, context size,
+ image source, and resolution.
- Token usage varies by resolution: larger images (ultra-wide aspect ratios) use more tokens.
- Estimates ~1 token per 1250 pixels. Targets 98% context utilization with safety margin.
- Capped at 20 frames.
+ Per-image token cost is asked of the GenAI provider so providers that know
+ their model's true cost (e.g. llama.cpp can probe the loaded mmproj) can
+ diverge from the default ~1-token-per-1250-pixels heuristic. The frame
+ budget is bounded by:
+ - remaining context window after prompt + response reservations
+ - a fixed MAX_IMAGE_TOKENS ceiling
+ - MAX_FRAMES_PER_SECOND x duration, to avoid drowning short events in
+ near-duplicate frames where the model latches onto the redundant middle
+ and skips the start/end action
"""
client = self.genai_manager.description_client
@@ -105,14 +115,15 @@ class ReviewDescriptionProcessor(PostProcessorApi):
width = target_width
height = int(target_width / aspect_ratio)
- pixels_per_image = width * height
- tokens_per_image = pixels_per_image / 1250
+ tokens_per_image = client.estimate_image_tokens(width, height)
prompt_tokens = 3800
response_tokens = 300
- available_tokens = context_size - prompt_tokens - response_tokens
- max_frames = int(available_tokens / tokens_per_image)
-
- return min(max(max_frames, 3), 20)
+ context_budget = context_size - prompt_tokens - response_tokens
+ image_token_budget = min(context_budget, MAX_IMAGE_TOKENS)
+ max_frames_by_tokens = int(image_token_budget / tokens_per_image)
+ max_frames_by_duration = int(duration * MAX_FRAMES_PER_SECOND)
+ max_frames = min(max_frames_by_tokens, max_frames_by_duration)
+ return max(max_frames, 3)
def process_data(
self, data: dict[str, Any], data_type: PostProcessDataEnum
@@ -355,12 +366,17 @@ class ReviewDescriptionProcessor(PostProcessorApi):
file_start = f"preview_{camera}-"
start_file = f"{file_start}{start_time}.webp"
end_file = f"{file_start}{end_time}.webp"
+
+ camera_files = [
+ entry.name
+ for entry in os.scandir(preview_dir)
+ if entry.name.startswith(file_start)
+ ]
+ camera_files.sort()
+
all_frames: list[str] = []
- for file in sorted(os.listdir(preview_dir)):
- if not file.startswith(file_start):
- continue
-
+ for file in camera_files:
if file < start_file:
if len(all_frames):
all_frames[0] = os.path.join(preview_dir, file)
@@ -376,7 +392,9 @@ class ReviewDescriptionProcessor(PostProcessorApi):
all_frames.append(os.path.join(preview_dir, file))
frame_count = len(all_frames)
- desired_frame_count = self.calculate_frame_count(camera)
+ desired_frame_count = self.calculate_frame_count(
+ camera, duration=end_time - start_time
+ )
if frame_count <= desired_frame_count:
return all_frames
@@ -400,7 +418,7 @@ class ReviewDescriptionProcessor(PostProcessorApi):
"""Get frames from recordings at specified timestamps."""
duration = end_time - start_time
desired_frame_count = self.calculate_frame_count(
- camera, ImageSourceEnum.recordings, height
+ camera, duration, ImageSourceEnum.recordings, height
)
# Calculate evenly spaced timestamps throughout the duration
diff --git a/frigate/data_processing/post/semantic_trigger.py b/frigate/data_processing/post/semantic_trigger.py
index 08f8a2e762..e2b305ea2d 100644
--- a/frigate/data_processing/post/semantic_trigger.py
+++ b/frigate/data_processing/post/semantic_trigger.py
@@ -55,7 +55,7 @@ class SemanticTriggerProcessor(PostProcessorApi):
# load stats from disk
try:
- with open(os.path.join(CONFIG_DIR, ".search_stats.json"), "r") as f:
+ with open(os.path.join(CONFIG_DIR, ".search_stats.json")) as f:
data = json.loads(f.read())
self.thumb_stats.from_dict(data["thumb_stats"])
self.desc_stats.from_dict(data["desc_stats"])
diff --git a/frigate/data_processing/post/types.py b/frigate/data_processing/post/types.py
index 6906f4a4ee..3698e99a85 100644
--- a/frigate/data_processing/post/types.py
+++ b/frigate/data_processing/post/types.py
@@ -1,21 +1,37 @@
-from pydantic import BaseModel, ConfigDict, Field
+from typing import Annotated
+
+from pydantic import BaseModel, ConfigDict, Field, StringConstraints
+
+ObservationItem = Annotated[str, StringConstraints(min_length=20, max_length=200)]
class ReviewMetadata(BaseModel):
model_config = ConfigDict(extra="ignore", protected_namespaces=())
- title: str = Field(
- description="A short title characterizing what took place and where, under 10 words."
+ observations: list[ObservationItem] = Field(
+ ...,
+ min_length=3,
+ max_length=8,
+ description="Enumerate the significant observations across all frames, in chronological order.",
)
scene: str = Field(
- description="A chronological narrative of what happens from start to finish."
+ min_length=150,
+ max_length=600,
+ description="A chronological narrative of what happens from start to finish, drawing directly from the items in observations.",
+ )
+ title: str = Field(
+ max_length=80,
+ description="Title for the activity.",
)
shortSummary: str = Field(
- description="A brief 2-sentence summary of the scene, suitable for notifications."
+ min_length=70,
+ max_length=140,
+ description="A brief summary for the activity.",
)
confidence: float = Field(
ge=0.0,
- description="Confidence in the analysis, from 0 to 1.",
+ le=1.0,
+ description="Confidence in the analysis as a decimal between 0.0 and 1.0, where 0.0 means no confidence and 1.0 means complete confidence. Express ONLY as a decimal.",
)
potential_threat_level: int = Field(
ge=0,
diff --git a/frigate/data_processing/real_time/api.py b/frigate/data_processing/real_time/api.py
index b9b7ba26ee..98efb532b4 100644
--- a/frigate/data_processing/real_time/api.py
+++ b/frigate/data_processing/real_time/api.py
@@ -4,9 +4,10 @@ import logging
import threading
from abc import ABC, abstractmethod
from collections import deque
+from collections.abc import Callable
from concurrent.futures import Future
from queue import Empty, Full, Queue
-from typing import Any, Callable
+from typing import Any
import numpy as np
diff --git a/frigate/data_processing/real_time/audio_transcription.py b/frigate/data_processing/real_time/audio_transcription.py
index 3d1536f730..0e1842b775 100644
--- a/frigate/data_processing/real_time/audio_transcription.py
+++ b/frigate/data_processing/real_time/audio_transcription.py
@@ -4,7 +4,7 @@ import logging
import os
import queue
import threading
-from typing import Any, Optional
+from typing import Any
import numpy as np
@@ -75,9 +75,7 @@ class AudioTranscriptionRealTimeProcessor(RealTimeProcessorApi):
f"Failed to initialize live streaming audio transcription: {e}"
)
- def __process_audio_stream(
- self, audio_data: np.ndarray
- ) -> Optional[tuple[str, bool]]:
+ def __process_audio_stream(self, audio_data: np.ndarray) -> tuple[str, bool] | None:
if (
self.model_runner.model is None
and self.config.audio_transcription.model_size == "small"
diff --git a/frigate/data_processing/real_time/face.py b/frigate/data_processing/real_time/face.py
index c6b6346b54..83c8a2e55a 100644
--- a/frigate/data_processing/real_time/face.py
+++ b/frigate/data_processing/real_time/face.py
@@ -7,7 +7,7 @@ import logging
import os
import shutil
from pathlib import Path
-from typing import Any, Optional
+from typing import Any
import cv2
import numpy as np
@@ -219,7 +219,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
logger.debug("Not processing due to hitting max rec attempts.")
return
- face: Optional[dict[str, Any]] = None
+ face: dict[str, Any] | None = None
if self.requires_face_detection:
logger.debug("Running manual face detection.")
@@ -229,9 +229,10 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
logger.debug(f"No person box available for {id}")
return
- rgb = cv2.cvtColor(frame, cv2.COLOR_YUV2RGB_I420)
+ # YuNet (cv2.FaceDetectorYN) is trained on BGR
+ bgr = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420)
left, top, right, bottom = person_box
- person = rgb[top:bottom, left:right]
+ person = bgr[top:bottom, left:right]
face_box = self.__detect_face(person, self.face_config.detection_threshold)
if not face_box:
@@ -250,11 +251,6 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
)
return
- try:
- face_frame = cv2.cvtColor(face_frame, cv2.COLOR_RGB2BGR)
- except Exception as e:
- logger.debug(f"Failed to convert face frame color for {id}: {e}")
- return
else:
# don't run for object without attributes
if not obj_data.get("current_attributes"):
diff --git a/frigate/data_processing/real_time/whisper_online.py b/frigate/data_processing/real_time/whisper_online.py
index 024b19fba3..066303c93e 100644
--- a/frigate/data_processing/real_time/whisper_online.py
+++ b/frigate/data_processing/real_time/whisper_online.py
@@ -1053,7 +1053,7 @@ if __name__ == "__main__":
SAMPLING_RATE = 16000
duration = len(load_audio(audio_path)) / SAMPLING_RATE
- logger.info("Audio duration is: %2.2f seconds" % duration)
+ logger.info("Audio duration is: %2.2f seconds", duration)
asr, online = asr_factory(args, logfile=logfile)
if args.vac:
diff --git a/frigate/db/sqlitevecq.py b/frigate/db/sqlitevecq.py
index a72e99b6a2..137fb51451 100644
--- a/frigate/db/sqlitevecq.py
+++ b/frigate/db/sqlitevecq.py
@@ -1,9 +1,11 @@
-import re
import sqlite3
from typing import Any
+import regex
from playhouse.sqliteq import SqliteQueueDatabase
+REGEXP_TIMEOUT_SECONDS = 1.0
+
class SqliteVecQueueDatabase(SqliteQueueDatabase):
def __init__(
@@ -34,8 +36,10 @@ class SqliteVecQueueDatabase(SqliteQueueDatabase):
if item is None:
return False
try:
- return re.search(expr, item) is not None
- except re.error:
+ return (
+ regex.search(expr, item, timeout=REGEXP_TIMEOUT_SECONDS) is not None
+ )
+ except (regex.error, TimeoutError):
return False
conn.create_function("REGEXP", 2, regexp)
diff --git a/frigate/debug_replay.py b/frigate/debug_replay.py
index 15ca3777ac..f8e801c3f3 100644
--- a/frigate/debug_replay.py
+++ b/frigate/debug_replay.py
@@ -1,10 +1,16 @@
-"""Debug replay camera management for replaying recordings with detection overlays."""
+"""Debug replay camera management for replaying recordings with detection overlays.
+The startup work (ffmpeg concat + camera config publish) lives in
+frigate.jobs.debug_replay. This module owns only session presence
+(active), session metadata, and post-session cleanup.
+"""
+
+import asyncio
import logging
import os
import shutil
-import subprocess as sp
import threading
+import time
from ruamel.yaml import YAML
@@ -15,21 +21,37 @@ from frigate.config.camera.updater import (
CameraConfigUpdateTopic,
)
from frigate.const import (
- CLIPS_DIR,
- RECORD_DIR,
REPLAY_CAMERA_PREFIX,
REPLAY_DIR,
THUMB_DIR,
)
-from frigate.models import Recordings
+from frigate.jobs.debug_replay import (
+ JOB_TYPE as DEBUG_REPLAY_JOB_TYPE,
+)
+from frigate.jobs.debug_replay import (
+ cancel_debug_replay_job,
+ wait_for_runner,
+)
+from frigate.jobs.export import JobStatePublisher
+from frigate.types import JobStatusTypesEnum
from frigate.util.camera_cleanup import cleanup_camera_db, cleanup_camera_files
from frigate.util.config import find_config_file
logger = logging.getLogger(__name__)
+MAX_SESSION_DURATION_SECONDS = 12 * 60 * 60
+AUTO_STOP_CHECK_INTERVAL_SECONDS = 60
+
class DebugReplayManager:
- """Manages a single debug replay session."""
+ """Owns the lifecycle pointers for a single debug replay session.
+
+ A session exists from the moment mark_starting is called (synchronously,
+ inside the API handler) until clear_session runs (on success cleanup,
+ failure, or stop). The active property is the source of truth that the
+ status bar consumes — broader than the startup job, which only covers the
+ preparing_clip / starting_camera window.
+ """
def __init__(self) -> None:
self._lock = threading.Lock()
@@ -38,150 +60,76 @@ class DebugReplayManager:
self.clip_path: str | None = None
self.start_ts: float | None = None
self.end_ts: float | None = None
+ self.session_started_at: float | None = None
+ self._job_state_publisher = JobStatePublisher()
@property
def active(self) -> bool:
- """Whether a replay session is currently active."""
+ """True from mark_starting until clear_session."""
return self.replay_camera_name is not None
- def start(
+ def mark_starting(
self,
source_camera: str,
+ replay_camera_name: str,
start_ts: float,
end_ts: float,
- frigate_config: FrigateConfig,
- config_publisher: CameraConfigUpdatePublisher,
- ) -> str:
- """Start a debug replay session.
+ ) -> None:
+ """Synchronously claim the session before the job runner starts.
- Args:
- source_camera: Name of the source camera to replay
- start_ts: Start timestamp
- end_ts: End timestamp
- frigate_config: Current Frigate configuration
- config_publisher: Publisher for camera config updates
-
- Returns:
- The replay camera name
-
- Raises:
- ValueError: If a session is already active or parameters are invalid
- RuntimeError: If clip generation fails
+ Called inside the API handler so the status bar sees active=True
+ immediately, before the worker thread does any ffmpeg work.
"""
with self._lock:
- return self._start_locked(
- source_camera, start_ts, end_ts, frigate_config, config_publisher
- )
+ self.replay_camera_name = replay_camera_name
+ self.source_camera = source_camera
+ self.start_ts = start_ts
+ self.end_ts = end_ts
+ self.clip_path = None
+ self.session_started_at = time.time()
- def _start_locked(
+ def mark_session_ready(self, clip_path: str) -> None:
+ """Record the on-disk clip path after the camera has been published."""
+ with self._lock:
+ self.clip_path = clip_path
+
+ def clear_session(self) -> None:
+ """Reset session pointers without publishing camera removal.
+
+ Used by the job runner on failure paths. stop() does the camera
+ teardown plus this clear in one step.
+ """
+ with self._lock:
+ self._clear_locked()
+
+ def _clear_locked(self) -> None:
+ self.replay_camera_name = None
+ self.source_camera = None
+ self.clip_path = None
+ self.start_ts = None
+ self.end_ts = None
+ self.session_started_at = None
+
+ def publish_camera(
self,
source_camera: str,
- start_ts: float,
- end_ts: float,
+ replay_name: str,
+ clip_path: str,
frigate_config: FrigateConfig,
config_publisher: CameraConfigUpdatePublisher,
- ) -> str:
- if self.active:
- raise ValueError("A replay session is already active")
+ ) -> None:
+ """Build the in-memory replay camera config and publish the add event.
- if source_camera not in frigate_config.cameras:
- raise ValueError(f"Camera '{source_camera}' not found")
-
- if end_ts <= start_ts:
- raise ValueError("End time must be after start time")
-
- # Query recordings for the source camera in the time range
- recordings = (
- Recordings.select(
- Recordings.path,
- Recordings.start_time,
- Recordings.end_time,
- )
- .where(
- Recordings.start_time.between(start_ts, end_ts)
- | Recordings.end_time.between(start_ts, end_ts)
- | ((start_ts > Recordings.start_time) & (end_ts < Recordings.end_time))
- )
- .where(Recordings.camera == source_camera)
- .order_by(Recordings.start_time.asc())
- )
-
- if not recordings.count():
- raise ValueError(
- f"No recordings found for camera '{source_camera}' in the specified time range"
- )
-
- # Create replay directory
- os.makedirs(REPLAY_DIR, exist_ok=True)
-
- # Generate replay camera name
- replay_name = f"{REPLAY_CAMERA_PREFIX}{source_camera}"
-
- # Build concat file for ffmpeg
- concat_file = os.path.join(REPLAY_DIR, f"{replay_name}_concat.txt")
- clip_path = os.path.join(REPLAY_DIR, f"{replay_name}.mp4")
-
- with open(concat_file, "w") as f:
- for recording in recordings:
- f.write(f"file '{recording.path}'\n")
-
- # Concatenate recordings into a single clip with -c copy (fast)
- ffmpeg_cmd = [
- frigate_config.ffmpeg.ffmpeg_path,
- "-hide_banner",
- "-y",
- "-f",
- "concat",
- "-safe",
- "0",
- "-i",
- concat_file,
- "-c",
- "copy",
- "-movflags",
- "+faststart",
- clip_path,
- ]
-
- logger.info(
- "Generating replay clip for %s (%.1f - %.1f)",
- source_camera,
- start_ts,
- end_ts,
- )
-
- try:
- result = sp.run(
- ffmpeg_cmd,
- capture_output=True,
- text=True,
- timeout=120,
- )
- if result.returncode != 0:
- logger.error("FFmpeg error: %s", result.stderr)
- raise RuntimeError(
- f"Failed to generate replay clip: {result.stderr[-500:]}"
- )
- except sp.TimeoutExpired:
- raise RuntimeError("Clip generation timed out")
- finally:
- # Clean up concat file
- if os.path.exists(concat_file):
- os.remove(concat_file)
-
- if not os.path.exists(clip_path):
- raise RuntimeError("Clip file was not created")
-
- # Build camera config dict for the replay camera
+ Called by the job runner during the starting_camera phase.
+ """
source_config = frigate_config.cameras[source_camera]
camera_dict = self._build_camera_config_dict(
source_config, replay_name, clip_path
)
- # Build an in-memory config with the replay camera added
config_file = find_config_file()
yaml_parser = YAML()
- with open(config_file, "r") as f:
+ with open(config_file) as f:
config_data = yaml_parser.load(f)
if "cameras" not in config_data or config_data["cameras"] is None:
@@ -191,75 +139,65 @@ class DebugReplayManager:
try:
new_config = FrigateConfig.parse_object(config_data)
except Exception as e:
- raise RuntimeError(f"Failed to validate replay camera config: {e}")
-
- # Update the running config
+ raise RuntimeError(f"Failed to validate replay camera config: {e}") from e
frigate_config.cameras[replay_name] = new_config.cameras[replay_name]
- # Publish the add event
config_publisher.publish_update(
CameraConfigUpdateTopic(CameraConfigUpdateEnum.add, replay_name),
new_config.cameras[replay_name],
)
- # Store session state
- self.replay_camera_name = replay_name
- self.source_camera = source_camera
- self.clip_path = clip_path
- self.start_ts = start_ts
- self.end_ts = end_ts
-
- logger.info("Debug replay started: %s -> %s", source_camera, replay_name)
- return replay_name
-
def stop(
self,
frigate_config: FrigateConfig,
config_publisher: CameraConfigUpdatePublisher,
) -> None:
- """Stop the active replay session and clean up all artifacts.
+ """Cancel any in-flight startup job and tear down the active session.
- Args:
- frigate_config: Current Frigate configuration
- config_publisher: Publisher for camera config updates
+ Safe to call when no session is active (no-op with a warning).
"""
+ cancel_debug_replay_job()
+ wait_for_runner(timeout=2.0)
+
with self._lock:
- self._stop_locked(frigate_config, config_publisher)
+ if not self.active:
+ logger.warning("No active replay session to stop")
+ return
- def _stop_locked(
- self,
- frigate_config: FrigateConfig,
- config_publisher: CameraConfigUpdatePublisher,
- ) -> None:
- if not self.active:
- logger.warning("No active replay session to stop")
- return
+ replay_name = self.replay_camera_name
+ source_camera = self.source_camera
- replay_name = self.replay_camera_name
+ # Only publish remove if the camera was actually added to the live
+ # config (i.e. the runner reached the starting_camera phase).
+ if replay_name is not None and replay_name in frigate_config.cameras:
+ config_publisher.publish_update(
+ CameraConfigUpdateTopic(CameraConfigUpdateEnum.remove, replay_name),
+ frigate_config.cameras[replay_name],
+ )
+ frigate_config.cameras.pop(replay_name, None)
- # Publish remove event so subscribers stop and remove from their config
- if replay_name in frigate_config.cameras:
- config_publisher.publish_update(
- CameraConfigUpdateTopic(CameraConfigUpdateEnum.remove, replay_name),
- frigate_config.cameras[replay_name],
+ if replay_name is not None:
+ self._cleanup_db(replay_name)
+ self._cleanup_files(replay_name)
+
+ self._job_state_publisher.publish(
+ {
+ "id": "stopped",
+ "job_type": DEBUG_REPLAY_JOB_TYPE,
+ "status": JobStatusTypesEnum.cancelled,
+ "start_time": None,
+ "end_time": time.time(),
+ "error_message": None,
+ "results": {
+ "source_camera": source_camera,
+ "replay_camera_name": replay_name,
+ },
+ }
)
- # Do NOT pop here — let subscribers handle removal from the shared
- # config dict when they process the ZMQ message to avoid race conditions
- # Defensive DB cleanup
- self._cleanup_db(replay_name)
+ self._clear_locked()
- # Remove filesystem artifacts
- self._cleanup_files(replay_name)
-
- # Reset state
- self.replay_camera_name = None
- self.source_camera = None
- self.clip_path = None
- self.start_ts = None
- self.end_ts = None
-
- logger.info("Debug replay stopped and cleaned up: %s", replay_name)
+ logger.info("Debug replay stopped and cleaned up: %s", replay_name)
def _build_camera_config_dict(
self,
@@ -267,16 +205,7 @@ class DebugReplayManager:
replay_name: str,
clip_path: str,
) -> dict:
- """Build a camera config dictionary for the replay camera.
-
- Args:
- source_config: Source camera's CameraConfig
- replay_name: Name for the replay camera
- clip_path: Path to the replay clip file
-
- Returns:
- Camera config as a dictionary
- """
+ """Build a camera config dictionary for the replay camera."""
# Extract detect config (exclude computed fields)
detect_dict = source_config.detect.model_dump(
exclude={"min_initialized", "max_disappeared", "enabled_in_config"}
@@ -311,10 +240,13 @@ class DebugReplayManager:
zone_dump = zone_config.model_dump(
exclude={"contour", "color"}, exclude_defaults=True
)
- # Always include required fields
zone_dump.setdefault("coordinates", zone_config.coordinates)
zones_dict[zone_name] = zone_dump
+ # Extract LPR and face recognition configs
+ lpr_dict = source_config.lpr.model_dump()
+ face_recognition_dict = source_config.face_recognition.model_dump()
+
# Extract motion config (exclude runtime fields)
motion_dict = {}
if source_config.motion is not None:
@@ -323,11 +255,23 @@ class DebugReplayManager:
"frame_shape",
"raw_mask",
"mask",
- "improved_contrast_enabled",
+ "enabled_in_config",
"rasterized_mask",
}
)
+ if source_config.motion.mask:
+ motion_dict["mask"] = {
+ mask_id: (
+ mask_cfg.model_dump(
+ exclude={"raw_coordinates", "enabled_in_config"}
+ )
+ if mask_cfg is not None
+ else None
+ )
+ for mask_id, mask_cfg in source_config.motion.mask.items()
+ }
+
return {
"enabled": True,
"ffmpeg": {
@@ -352,8 +296,8 @@ class DebugReplayManager:
},
"birdseye": {"enabled": False},
"audio": {"enabled": False},
- "lpr": {"enabled": False},
- "face_recognition": {"enabled": False},
+ "lpr": lpr_dict,
+ "face_recognition": face_recognition_dict,
}
def _cleanup_db(self, camera_name: str) -> None:
@@ -385,12 +329,14 @@ def cleanup_replay_cameras() -> None:
"""
stale_cameras: set[str] = set()
- # Scan filesystem for leftover replay artifacts to derive camera names
- for dir_path in [RECORD_DIR, CLIPS_DIR, THUMB_DIR]:
- if os.path.isdir(dir_path):
- for entry in os.listdir(dir_path):
- if entry.startswith(REPLAY_CAMERA_PREFIX):
- stale_cameras.add(entry)
+ # Derive stale camera names from THUMB_DIR (per-camera dirs) and
+ # REPLAY_DIR (the session's source clip); both listings are bounded by
+ # camera count. cleanup_camera_files below removes any remaining
+ # per-camera artifacts (snapshots, thumbnails, LPR images, etc.) by name.
+ if os.path.isdir(THUMB_DIR):
+ for entry in os.listdir(THUMB_DIR):
+ if entry.startswith(REPLAY_CAMERA_PREFIX):
+ stale_cameras.add(entry)
if os.path.isdir(REPLAY_DIR):
for entry in os.listdir(REPLAY_DIR):
@@ -412,3 +358,41 @@ def cleanup_replay_cameras() -> None:
shutil.rmtree(REPLAY_DIR)
except Exception as e:
logger.error("Failed to remove replay cache directory: %s", e)
+
+
+async def debug_replay_auto_stop_watchdog(
+ manager: DebugReplayManager,
+ frigate_config: FrigateConfig,
+ config_publisher: CameraConfigUpdatePublisher,
+) -> None:
+ """Auto-stop debug replay sessions that exceed MAX_SESSION_DURATION_SECONDS.
+
+ Backstop against a session left running for days. The cap is intentionally
+ generous so realistic tuning and overnight soak workflows aren't disrupted.
+ """
+ while True:
+ try:
+ await asyncio.sleep(AUTO_STOP_CHECK_INTERVAL_SECONDS)
+
+ started_at = manager.session_started_at
+ if not manager.active or started_at is None:
+ continue
+
+ if time.time() - started_at < MAX_SESSION_DURATION_SECONDS:
+ continue
+
+ replay_name = manager.replay_camera_name
+ await asyncio.to_thread(
+ manager.stop,
+ frigate_config=frigate_config,
+ config_publisher=config_publisher,
+ )
+ logger.info(
+ "Debug replay auto-stopped after exceeding max session duration of %d hours: %s",
+ MAX_SESSION_DURATION_SECONDS // 3600,
+ replay_name,
+ )
+ except asyncio.CancelledError:
+ raise
+ except Exception:
+ logger.exception("Error in debug replay auto-stop watchdog")
diff --git a/frigate/detectors/detection_api.py b/frigate/detectors/detection_api.py
index 4f03f28aa4..b03d607d5a 100644
--- a/frigate/detectors/detection_api.py
+++ b/frigate/detectors/detection_api.py
@@ -1,6 +1,5 @@
import logging
from abc import ABC, abstractmethod
-from typing import List
import numpy as np
@@ -11,7 +10,7 @@ logger = logging.getLogger(__name__)
class DetectionApi(ABC):
type_key: str
- supported_models: List[ModelTypeEnum]
+ supported_models: list[ModelTypeEnum]
@abstractmethod
def __init__(self, detector_config: BaseDetectorConfig):
diff --git a/frigate/detectors/detection_runners.py b/frigate/detectors/detection_runners.py
index d12c8b733d..ee30f4fead 100644
--- a/frigate/detectors/detection_runners.py
+++ b/frigate/detectors/detection_runners.py
@@ -15,6 +15,9 @@ from frigate.util.rknn_converter import auto_convert_model, is_rknn_compatible
logger = logging.getLogger(__name__)
+# Process-wide lock serializing all OpenVINO compile/inference calls
+_OPENVINO_LOCK = threading.Lock()
+
def is_arm64_platform() -> bool:
"""Check if we're running on an ARM platform."""
@@ -79,7 +82,11 @@ def is_openvino_gpu_npu_available() -> bool:
available_devices = get_openvino_available_devices()
# Check for GPU, NPU, or other acceleration devices (excluding CPU)
acceleration_devices = ["GPU", "MYRIAD", "NPU", "GNA", "HDDL"]
- return any(device in available_devices for device in acceleration_devices)
+ return any(
+ avail_dev == accel_dev or avail_dev.startswith(accel_dev + ".")
+ for avail_dev in available_devices
+ for accel_dev in acceleration_devices
+ )
class BaseModelRunner(ABC):
@@ -132,7 +139,6 @@ class ONNXModelRunner(BaseModelRunner):
return model_type in [
EnrichmentModelTypeEnum.paddleocr.value,
EnrichmentModelTypeEnum.jina_v2.value,
- EnrichmentModelTypeEnum.arcface.value,
ModelTypeEnum.rfdetr.value,
ModelTypeEnum.dfine.value,
]
@@ -279,6 +285,13 @@ class OpenVINOModelRunner(BaseModelRunner):
EnrichmentModelTypeEnum.arcface.value,
]
+ @staticmethod
+ def is_detection_model(model_type: str) -> bool:
+ # Import here to avoid circular imports
+ from frigate.detectors.detector_config import ModelTypeEnum
+
+ return model_type in [m.value for m in ModelTypeEnum]
+
def __init__(self, model_path: str, device: str, model_type: str, **kwargs):
self.model_path = model_path
self.device = device
@@ -307,22 +320,26 @@ class OpenVINOModelRunner(BaseModelRunner):
# Apply performance optimization
self.ov_core.set_property(device, {"PERF_COUNT": "NO"})
- if device in ["GPU", "AUTO"]:
+ if device in ["GPU", "AUTO", "NPU"]:
self.ov_core.set_property(device, {"PERFORMANCE_HINT": "LATENCY"})
- # Compile model
- self.compiled_model = self.ov_core.compile_model(
- model=model_path, device_name=device
- )
+ if device == "NPU" and OpenVINOModelRunner.is_detection_model(model_type):
+ try:
+ self.ov_core.set_property(device, {"NPU_TURBO": "YES"})
+ except Exception as e:
+ logger.debug(f"NPU_TURBO not supported by driver: {e}")
+
+ # Compile model under the shared lock
+ with _OPENVINO_LOCK:
+ self.compiled_model = self.ov_core.compile_model(
+ model=model_path, device_name=device
+ )
+
+ # Create reusable inference request
+ self.infer_request = self.compiled_model.create_infer_request()
- # Create reusable inference request
- self.infer_request = self.compiled_model.create_infer_request()
self.input_tensor: ov.Tensor | None = None
- # Thread lock to prevent concurrent inference (needed for JinaV2 which shares
- # one runner between text and vision embeddings called from different threads)
- self._inference_lock = threading.Lock()
-
if not self.complex_model:
try:
input_shape = self.compiled_model.inputs[0].get_shape()
@@ -366,9 +383,11 @@ class OpenVINOModelRunner(BaseModelRunner):
Returns:
List of output tensors
"""
- # Lock prevents concurrent access to infer_request
- # Needed for JinaV2: genai thread (text) + embeddings thread (vision)
- with self._inference_lock:
+ # Shared lock serializes inference across every OpenVINO runner in this
+ # process — both the shared-runner JinaV2 case (genai text thread +
+ # embeddings vision thread) and distinct runners running on separate
+ # threads (e.g. the ArcFace face-model build vs the LPR detector).
+ with _OPENVINO_LOCK:
from frigate.embeddings.types import EnrichmentModelTypeEnum
if self.model_type in [EnrichmentModelTypeEnum.arcface.value]:
@@ -472,7 +491,7 @@ class RKNNModelRunner(BaseModelRunner):
except ImportError:
logger.error("RKNN Lite not available")
- raise ImportError("RKNN Lite not available")
+ raise ImportError("RKNN Lite not available") from None
except Exception as e:
logger.error(f"Error loading RKNN model: {e}")
raise
diff --git a/frigate/detectors/detector_config.py b/frigate/detectors/detector_config.py
index 5071e3a741..788fb58a79 100644
--- a/frigate/detectors/detector_config.py
+++ b/frigate/detectors/detector_config.py
@@ -3,7 +3,7 @@ import json
import logging
import os
from enum import Enum
-from typing import Any, Dict, Optional, Tuple
+from typing import Any
import requests
from pydantic import BaseModel, ConfigDict, Field
@@ -45,12 +45,12 @@ class ModelTypeEnum(str, Enum):
class ModelConfig(BaseModel):
- path: Optional[str] = Field(
+ path: str | None = Field(
None,
title="Custom object detector model path",
description="Path to a custom detection model file (or plus:// for Frigate+ models).",
)
- labelmap_path: Optional[str] = Field(
+ labelmap_path: str | None = Field(
None,
title="Label map for custom object detector",
description="Path to a labelmap file that maps numeric classes to string labels for the detector.",
@@ -65,12 +65,12 @@ class ModelConfig(BaseModel):
title="Object detection model input height",
description="Height of the model input tensor in pixels.",
)
- labelmap: Dict[int, str] = Field(
+ labelmap: dict[int, str] = Field(
default_factory=dict,
title="Labelmap customization",
description="Overrides or remapping entries to merge into the standard labelmap.",
)
- attributes_map: Dict[str, list[str]] = Field(
+ attributes_map: dict[str, list[str]] = Field(
default=DEFAULT_ATTRIBUTE_LABEL_MAP,
title="Map of object labels to their attribute labels",
description="Mapping from object labels to attribute labels used to attach metadata (for example 'car' -> ['license_plate']).",
@@ -95,18 +95,18 @@ class ModelConfig(BaseModel):
title="Object Detection Model Type",
description="Detector model architecture type (ssd, yolox, yolonas) used by some detectors for optimization.",
)
- _merged_labelmap: Optional[Dict[int, str]] = PrivateAttr()
- _colormap: Dict[int, Tuple[int, int, int]] = PrivateAttr()
+ _merged_labelmap: dict[int, str] | None = PrivateAttr()
+ _colormap: dict[int, tuple[int, int, int]] = PrivateAttr()
_all_attributes: list[str] = PrivateAttr()
_all_attribute_logos: list[str] = PrivateAttr()
_model_hash: str = PrivateAttr()
@property
- def merged_labelmap(self) -> Dict[int, str]:
+ def merged_labelmap(self) -> dict[int, str]:
return self._merged_labelmap
@property
- def colormap(self) -> Dict[int, Tuple[int, int, int]]:
+ def colormap(self) -> dict[int, tuple[int, int, int]]:
return self._colormap
@property
@@ -171,7 +171,7 @@ class ModelConfig(BaseModel):
with open(model_info_path, "w") as f:
json.dump(model_info, f)
else:
- with open(model_info_path, "r") as f:
+ with open(model_info_path) as f:
model_info: dict[str, Any] = json.load(f)
if detector and detector not in model_info["supportedDetectors"]:
@@ -240,12 +240,12 @@ class BaseDetectorConfig(BaseModel):
title="Detector Type",
description="Type of detector to use for object detection (for example 'cpu', 'edgetpu', 'openvino').",
)
- model: Optional[ModelConfig] = Field(
+ model: ModelConfig | None = Field(
default=None,
title="Detector specific model configuration",
description="Detector-specific model configuration options (path, input size, etc.).",
)
- model_path: Optional[str] = Field(
+ model_path: str | None = Field(
default=None,
title="Detector specific model path",
description="File path to the detector model binary if required by the chosen detector.",
diff --git a/frigate/detectors/detector_types.py b/frigate/detectors/detector_types.py
index 418fcd625c..42129c5945 100644
--- a/frigate/detectors/detector_types.py
+++ b/frigate/detectors/detector_types.py
@@ -2,10 +2,9 @@ import importlib
import logging
import pkgutil
from enum import Enum
-from typing import Union
+from typing import Annotated, Union
from pydantic import Field
-from typing_extensions import Annotated
from . import plugins
from .detection_api import DetectionApi
@@ -37,6 +36,6 @@ class StrEnum(str, Enum):
DetectorTypeEnum = StrEnum("DetectorTypeEnum", {k: k for k in api_types})
DetectorConfig = Annotated[
- Union[tuple(BaseDetectorConfig.__subclasses__())],
+ Union[tuple(BaseDetectorConfig.__subclasses__())], # noqa: UP007
Field(discriminator="type"),
]
diff --git a/frigate/detectors/plugins/axengine.py b/frigate/detectors/plugins/axengine.py
index 383fcd0bf9..43a1fffe4d 100644
--- a/frigate/detectors/plugins/axengine.py
+++ b/frigate/detectors/plugins/axengine.py
@@ -39,8 +39,7 @@ class Axengine(DetectionApi):
try:
import axengine as axe
except ModuleNotFoundError:
- raise ImportError("AXEngine is not installed.")
- return
+ raise ImportError("AXEngine is not installed.") from None
logger.info("__init__ axengine")
super().__init__(config)
diff --git a/frigate/detectors/plugins/cpu_tfl.py b/frigate/detectors/plugins/cpu_tfl.py
index 2224a2bdad..d8cfe33e0d 100644
--- a/frigate/detectors/plugins/cpu_tfl.py
+++ b/frigate/detectors/plugins/cpu_tfl.py
@@ -1,7 +1,7 @@
import logging
+from typing import Literal
from pydantic import ConfigDict, Field
-from typing_extensions import Literal
from frigate.detectors.detection_api import DetectionApi
from frigate.detectors.detector_config import BaseDetectorConfig
diff --git a/frigate/detectors/plugins/deepstack.py b/frigate/detectors/plugins/deepstack.py
index 9b5fcd5af5..e87b07c444 100644
--- a/frigate/detectors/plugins/deepstack.py
+++ b/frigate/detectors/plugins/deepstack.py
@@ -1,11 +1,11 @@
import io
import logging
+from typing import Literal
import numpy as np
import requests
from PIL import Image
from pydantic import ConfigDict, Field
-from typing_extensions import Literal
from frigate.detectors.detection_api import DetectionApi
from frigate.detectors.detector_config import BaseDetectorConfig
diff --git a/frigate/detectors/plugins/degirum.py b/frigate/detectors/plugins/degirum.py
index 5afb32a3ad..9aaf0e6dc9 100644
--- a/frigate/detectors/plugins/degirum.py
+++ b/frigate/detectors/plugins/degirum.py
@@ -1,9 +1,9 @@
import logging
import queue
+from typing import Literal
import numpy as np
from pydantic import ConfigDict, Field
-from typing_extensions import Literal
from frigate.detectors.detection_api import DetectionApi
from frigate.detectors.detector_config import BaseDetectorConfig
@@ -46,7 +46,7 @@ class DGDetector(DetectionApi):
try:
import degirum as dg
except ModuleNotFoundError:
- raise ImportError("Unable to import DeGirum detector.")
+ raise ImportError("Unable to import DeGirum detector.") from None
self._queue = queue.Queue()
self._zoo = dg.connect(
diff --git a/frigate/detectors/plugins/edgetpu_tfl.py b/frigate/detectors/plugins/edgetpu_tfl.py
index 02bd9f5ec5..c4ea485d57 100644
--- a/frigate/detectors/plugins/edgetpu_tfl.py
+++ b/frigate/detectors/plugins/edgetpu_tfl.py
@@ -1,11 +1,11 @@
import logging
import math
import os
+from typing import Literal
import cv2
import numpy as np
from pydantic import ConfigDict, Field
-from typing_extensions import Literal
from frigate.detectors.detection_api import DetectionApi
from frigate.detectors.detector_config import BaseDetectorConfig, ModelTypeEnum
diff --git a/frigate/detectors/plugins/hailo8l.py b/frigate/detectors/plugins/hailo8l.py
index bbe84d52f0..63a3c9a2a8 100755
--- a/frigate/detectors/plugins/hailo8l.py
+++ b/frigate/detectors/plugins/hailo8l.py
@@ -4,12 +4,11 @@ import subprocess
import threading
import urllib.request
from functools import partial
-from typing import Dict, List, Optional, Tuple
+from typing import Literal
import cv2
import numpy as np
from pydantic import ConfigDict, Field
-from typing_extensions import Literal
from frigate.const import MODEL_CACHE_DIR
from frigate.detectors.detection_api import DetectionApi
@@ -83,8 +82,8 @@ class HailoAsyncInference:
input_store: RequestStore,
output_store: ResponseStore,
batch_size: int = 1,
- input_type: Optional[str] = None,
- output_type: Optional[Dict[str, str]] = None,
+ input_type: str | None = None,
+ output_type: dict[str, str] | None = None,
send_original_frame: bool = False,
) -> None:
# when importing hailo it activates the driver
@@ -125,9 +124,9 @@ class HailoAsyncInference:
def callback(
self,
completion_info,
- bindings_list: List,
- input_batch: List,
- request_ids: List[int],
+ bindings_list: list,
+ input_batch: list,
+ request_ids: list[int],
):
if completion_info.exception:
logger.error(f"Inference error: {completion_info.exception}")
@@ -163,7 +162,7 @@ class HailoAsyncInference:
}
return configured_infer_model.create_bindings(output_buffers=output_buffers)
- def get_input_shape(self) -> Tuple[int, ...]:
+ def get_input_shape(self) -> tuple[int, ...]:
return self.hef.get_input_vstream_infos()[0].shape
def run(self) -> None:
@@ -304,7 +303,7 @@ class HailoDetector(DetectionApi):
urllib.request.urlretrieve(url, destination)
logger.debug(f"Downloaded model to {destination}")
except Exception as e:
- raise RuntimeError(f"Failed to download model from {url}: {str(e)}")
+ raise RuntimeError(f"Failed to download model from {url}: {str(e)}") from e
def check_and_prepare(self) -> str:
if not os.path.exists(self.cache_dir):
@@ -350,7 +349,7 @@ class HailoDetector(DetectionApi):
if not self.inference_thread.is_alive():
raise RuntimeError(
"HailoRT inference thread has stopped, restart required."
- )
+ ) from None
return np.zeros((20, 6), dtype=np.float32)
diff --git a/frigate/detectors/plugins/memryx.py b/frigate/detectors/plugins/memryx.py
index 2c03d14a49..88537273ce 100644
--- a/frigate/detectors/plugins/memryx.py
+++ b/frigate/detectors/plugins/memryx.py
@@ -5,11 +5,11 @@ import shutil
import urllib.request
import zipfile
from queue import Queue
+from typing import Literal
import cv2
import numpy as np
from pydantic import BaseModel, ConfigDict, Field
-from typing_extensions import Literal
from frigate.detectors.detection_api import DetectionApi
from frigate.detectors.detector_config import (
@@ -61,7 +61,7 @@ class MemryXDetector(DetectionApi):
except ModuleNotFoundError:
raise ImportError(
"MemryX SDK is not installed. Install it and set up MIX environment."
- )
+ ) from None
return
# Initialize stop_event as None, will be set later by set_stop_event()
diff --git a/frigate/detectors/plugins/onnx.py b/frigate/detectors/plugins/onnx.py
index b9aa00fbdb..cbf189916b 100644
--- a/frigate/detectors/plugins/onnx.py
+++ b/frigate/detectors/plugins/onnx.py
@@ -1,8 +1,8 @@
import logging
+from typing import Literal
import numpy as np
from pydantic import ConfigDict, Field
-from typing_extensions import Literal
from frigate.detectors.detection_api import DetectionApi
from frigate.detectors.detection_runners import get_optimized_runner
diff --git a/frigate/detectors/plugins/openvino.py b/frigate/detectors/plugins/openvino.py
index f73b7cb0cc..5bd55d4a6b 100644
--- a/frigate/detectors/plugins/openvino.py
+++ b/frigate/detectors/plugins/openvino.py
@@ -1,9 +1,9 @@
import logging
+from typing import Literal
import numpy as np
import openvino as ov
from pydantic import ConfigDict, Field
-from typing_extensions import Literal
from frigate.detectors.detection_api import DetectionApi
from frigate.detectors.detection_runners import OpenVINOModelRunner
@@ -52,6 +52,12 @@ class OvDetector(DetectionApi):
self.h = detector_config.model.height
self.w = detector_config.model.width
+ logger.info(
+ "Loading OpenVINO model %s on device %s",
+ detector_config.model.path,
+ detector_config.device,
+ )
+
self.runner = OpenVINOModelRunner(
model_path=detector_config.model.path,
device=detector_config.device,
diff --git a/frigate/detectors/plugins/rknn.py b/frigate/detectors/plugins/rknn.py
index 15ab93dcbc..7bf5e03c15 100644
--- a/frigate/detectors/plugins/rknn.py
+++ b/frigate/detectors/plugins/rknn.py
@@ -90,7 +90,7 @@ class Rknn(DetectionApi):
with open("/proc/device-tree/compatible") as file:
soc = file.read().split(",")[-1].strip("\x00")
except FileNotFoundError:
- raise Exception("Make sure to run docker in privileged mode.")
+ raise Exception("Make sure to run docker in privileged mode.") from None
if soc not in SUPPORTED_RK_SOCS:
raise Exception(
diff --git a/frigate/detectors/plugins/synaptics.py b/frigate/detectors/plugins/synaptics.py
index e6983a29c3..6d0ac7f521 100644
--- a/frigate/detectors/plugins/synaptics.py
+++ b/frigate/detectors/plugins/synaptics.py
@@ -1,9 +1,9 @@
import logging
import os
+from typing import Literal
import numpy as np
from pydantic import ConfigDict
-from typing_extensions import Literal
from frigate.detectors.detection_api import DetectionApi
from frigate.detectors.detector_config import (
diff --git a/frigate/detectors/plugins/teflon_tfl.py b/frigate/detectors/plugins/teflon_tfl.py
index 370d08817c..7dd44e6e89 100644
--- a/frigate/detectors/plugins/teflon_tfl.py
+++ b/frigate/detectors/plugins/teflon_tfl.py
@@ -1,7 +1,7 @@
import logging
+from typing import Literal
from pydantic import ConfigDict
-from typing_extensions import Literal
from frigate.detectors.detection_api import DetectionApi
from frigate.detectors.detector_config import BaseDetectorConfig
diff --git a/frigate/detectors/plugins/tensorrt.py b/frigate/detectors/plugins/tensorrt.py
index 087331a2dc..6b39212804 100644
--- a/frigate/detectors/plugins/tensorrt.py
+++ b/frigate/detectors/plugins/tensorrt.py
@@ -14,8 +14,9 @@ try:
except ModuleNotFoundError:
TRT_SUPPORT = False
+from typing import Literal
+
from pydantic import ConfigDict, Field
-from typing_extensions import Literal
from frigate.detectors.detection_api import DetectionApi
from frigate.detectors.detector_config import BaseDetectorConfig
@@ -58,7 +59,7 @@ class TensorRTDetectorConfig(BaseDetectorConfig):
)
-class HostDeviceMem(object):
+class HostDeviceMem:
"""Simple helper data class that's a little nicer to use than a 2-tuple."""
def __init__(self, host_mem, device_mem, nbytes, size):
diff --git a/frigate/detectors/plugins/zmq_ipc.py b/frigate/detectors/plugins/zmq_ipc.py
index b0e568eff0..cc9a538c81 100644
--- a/frigate/detectors/plugins/zmq_ipc.py
+++ b/frigate/detectors/plugins/zmq_ipc.py
@@ -1,12 +1,11 @@
import json
import logging
import os
-from typing import Any, List
+from typing import Any, Literal
import numpy as np
import zmq
from pydantic import ConfigDict, Field
-from typing_extensions import Literal
from frigate.detectors.detection_api import DetectionApi
from frigate.detectors.detector_config import BaseDetectorConfig
@@ -274,7 +273,7 @@ class ZmqIpcDetector(DetectionApi):
}
return json.dumps(header).encode("utf-8")
- def _decode_response(self, frames: List[bytes]) -> np.ndarray:
+ def _decode_response(self, frames: list[bytes]) -> np.ndarray:
try:
if len(frames) == 1:
# Single-frame raw float32 (20x6)
diff --git a/frigate/embeddings/__init__.py b/frigate/embeddings/__init__.py
index 5e14d0d8c3..bf74df6811 100644
--- a/frigate/embeddings/__init__.py
+++ b/frigate/embeddings/__init__.py
@@ -4,10 +4,11 @@ import base64
import json
import logging
import os
+import sys
import threading
from json.decoder import JSONDecodeError
from multiprocessing.synchronize import Event as MpEvent
-from typing import Any, Union
+from typing import Any
import regex
from pathvalidate import ValidationError, sanitize_filename
@@ -52,6 +53,14 @@ class EmbeddingProcess(FrigateProcess):
self.stop_event,
)
maintainer.start()
+ maintainer.join()
+
+ # If the maintainer thread exited but no shutdown was requested, it
+ # crashed. Surface as a non-zero exit so the watchdog restarts us
+ # instead of treating the silent thread death as a clean shutdown.
+ if not self.stop_event.is_set():
+ logger.error("Embeddings maintainer thread exited unexpectedly")
+ sys.exit(1)
class EmbeddingsContext:
@@ -64,7 +73,7 @@ class EmbeddingsContext:
# load stats from disk
stats_file = os.path.join(CONFIG_DIR, ".search_stats.json")
try:
- with open(stats_file, "r") as f:
+ with open(stats_file) as f:
data = json.loads(f.read())
self.thumb_stats.from_dict(data["thumb_stats"])
self.desc_stats.from_dict(data["desc_stats"])
@@ -89,7 +98,7 @@ class EmbeddingsContext:
self.requestor.stop()
def search_thumbnail(
- self, query: Union[Event, str], event_ids: list[str] = None
+ self, query: Event | str, event_ids: list[str] = None
) -> list[tuple[str, float]]:
if query.__class__ == Event:
cursor = self.db.execute_sql(
@@ -246,7 +255,7 @@ class EmbeddingsContext:
sanitized_old_name = sanitize_filename(old_name, replacement_text="_")
sanitized_new_name = sanitize_filename(new_name, replacement_text="_")
except ValidationError as e:
- raise ValueError(f"Invalid face name: {str(e)}")
+ raise ValueError(f"Invalid face name: {str(e)}") from e
if not regex.match(valid_name_pattern, old_name):
raise ValueError(f"Invalid old face name: {old_name}")
diff --git a/frigate/embeddings/maintainer.py b/frigate/embeddings/maintainer.py
index ea1c9a1186..a9b9b837b7 100644
--- a/frigate/embeddings/maintainer.py
+++ b/frigate/embeddings/maintainer.py
@@ -60,7 +60,11 @@ from frigate.data_processing.real_time.license_plate import (
)
from frigate.data_processing.types import DataProcessorMetrics, PostProcessDataEnum
from frigate.db.sqlitevecq import SqliteVecQueueDatabase
-from frigate.events.types import EventTypeEnum, RegenerateDescriptionEnum
+from frigate.events.types import (
+ EventStateEnum,
+ EventTypeEnum,
+ RegenerateDescriptionEnum,
+)
from frigate.genai import GenAIClientManager
from frigate.models import Event, Recordings, ReviewSegment, Trigger
from frigate.types import TrackedObjectUpdateTypesEnum
@@ -94,10 +98,17 @@ class EmbeddingMaintainer(threading.Thread):
[
CameraConfigUpdateEnum.add,
CameraConfigUpdateEnum.remove,
+ CameraConfigUpdateEnum.detect,
+ CameraConfigUpdateEnum.face_recognition,
+ CameraConfigUpdateEnum.ffmpeg,
+ CameraConfigUpdateEnum.lpr,
+ CameraConfigUpdateEnum.motion,
+ CameraConfigUpdateEnum.objects,
CameraConfigUpdateEnum.object_genai,
CameraConfigUpdateEnum.review,
CameraConfigUpdateEnum.review_genai,
CameraConfigUpdateEnum.semantic_search,
+ CameraConfigUpdateEnum.zones,
],
)
self.enrichment_config_subscriber = ConfigSubscriber("config/")
@@ -189,6 +200,9 @@ class EmbeddingMaintainer(threading.Thread):
)
for model_config in self.config.classification.custom.values():
+ if not model_config.enabled:
+ continue
+
self.realtime_processors.append(
CustomStateClassificationProcessor(
self.config, model_config, self.requestor, self.metrics
@@ -228,7 +242,7 @@ class EmbeddingMaintainer(threading.Thread):
)
)
- if self.config.audio_transcription.enabled and any(
+ if any(
c.enabled_in_config and c.audio_transcription.enabled
for c in self.config.cameras.values()
):
@@ -310,6 +324,10 @@ class EmbeddingMaintainer(threading.Thread):
self._handle_custom_classification_update(topic, payload)
return
+ if topic == "config/genai":
+ self.config.genai = payload
+ self.genai_manager.update_config(self.config)
+
# Broadcast to all processors — each decides if the topic is relevant
for processor in self.realtime_processors:
processor.update_config(topic, payload)
@@ -317,6 +335,25 @@ class EmbeddingMaintainer(threading.Thread):
for processor in self.post_processors:
processor.update_config(topic, payload)
+ def _remove_custom_classification_processor(self, model_name: str) -> None:
+ """Shut down and drop any running processor for a custom model."""
+ remaining = []
+ for processor in self.realtime_processors:
+ if (
+ isinstance(
+ processor,
+ (
+ CustomStateClassificationProcessor,
+ CustomObjectClassificationProcessor,
+ ),
+ )
+ and processor.model_config.name == model_name
+ ):
+ processor.shutdown()
+ else:
+ remaining.append(processor)
+ self.realtime_processors = remaining
+
def _handle_custom_classification_update(
self, topic: str, model_config: Any
) -> None:
@@ -324,23 +361,7 @@ class EmbeddingMaintainer(threading.Thread):
model_name = topic.split("/")[-1]
if model_config is None:
- remaining = []
- for processor in self.realtime_processors:
- if (
- isinstance(
- processor,
- (
- CustomStateClassificationProcessor,
- CustomObjectClassificationProcessor,
- ),
- )
- and processor.model_config.name == model_name
- ):
- processor.shutdown()
- else:
- remaining.append(processor)
- self.realtime_processors = remaining
-
+ self._remove_custom_classification_processor(model_name)
logger.info(
f"Successfully removed classification processor for model: {model_name}"
)
@@ -348,20 +369,29 @@ class EmbeddingMaintainer(threading.Thread):
self.config.classification.custom[model_name] = model_config
- # Check if processor already exists
+ # A disabled model must not run; tear down any existing processor and
+ # do not register a new one.
+ if not model_config.enabled:
+ self._remove_custom_classification_processor(model_name)
+ logger.info(f"Disabled classification processor for model: {model_name}")
+ return
+
for processor in self.realtime_processors:
- if isinstance(
- processor,
- (
- CustomStateClassificationProcessor,
- CustomObjectClassificationProcessor,
- ),
+ if (
+ isinstance(
+ processor,
+ (
+ CustomStateClassificationProcessor,
+ CustomObjectClassificationProcessor,
+ ),
+ )
+ and processor.model_config.name == model_name
):
- if processor.model_config.name == model_name:
- logger.debug(
- f"Classification processor for model {model_name} already exists, skipping"
- )
- return
+ processor.model_config = model_config
+ logger.debug(
+ f"Updated config for classification processor: {model_name}"
+ )
+ return
if model_config.state_config is not None:
processor = CustomStateClassificationProcessor(
@@ -420,7 +450,7 @@ class EmbeddingMaintainer(threading.Thread):
logger.error(f"No processor handled the topic {topic}")
return None
except Exception as e:
- logger.error(f"Unable to handle embeddings request {e}", exc_info=True)
+ logger.exception(f"Unable to handle embeddings request {e}")
self.embeddings_responder.check_for_request(_handle_request)
@@ -431,7 +461,7 @@ class EmbeddingMaintainer(threading.Thread):
if update is None:
return
- source_type, _, camera, frame_name, data = update
+ source_type, event_type, camera, frame_name, data = update
logger.debug(
f"Received update - source_type: {source_type}, camera: {camera}, data label: {data.get('label') if data else 'None'}"
@@ -481,6 +511,12 @@ class EmbeddingMaintainer(threading.Thread):
for processor in self.post_processors:
if isinstance(processor, ObjectDescriptionProcessor):
+ # skip end events — _process_finalized handles them via event_end_subscriber.
+ # processing them here can re-create tracked_events entries after cleanup
+ # when the event_subscriber queue is backlogged behind event_end_subscriber.
+ if event_type == EventStateEnum.end:
+ continue
+
processor.process_data(
{
"camera": camera,
@@ -513,10 +549,16 @@ class EmbeddingMaintainer(threading.Thread):
try:
event: Event = Event.get(Event.id == event_id)
except DoesNotExist:
+ for processor in self.post_processors:
+ if isinstance(processor, ObjectDescriptionProcessor):
+ processor.cleanup_event(event_id)
continue
# Skip the event if not an object
if event.data.get("type") != "object":
+ for processor in self.post_processors:
+ if isinstance(processor, ObjectDescriptionProcessor):
+ processor.cleanup_event(event_id)
continue
# Extract valid thumbnail
@@ -675,7 +717,11 @@ class EmbeddingMaintainer(threading.Thread):
and "license_plate" not in camera_config.objects.track
)
- if not dedicated_lpr_enabled and len(self.config.classification.custom) == 0:
+ has_enabled_custom = any(
+ c.enabled for c in self.config.classification.custom.values()
+ )
+
+ if not dedicated_lpr_enabled and not has_enabled_custom:
# no active features that use this data
return
diff --git a/frigate/embeddings/onnx/base_embedding.py b/frigate/embeddings/onnx/base_embedding.py
index c0bd58475b..5b514d1f60 100644
--- a/frigate/embeddings/onnx/base_embedding.py
+++ b/frigate/embeddings/onnx/base_embedding.py
@@ -57,6 +57,12 @@ class BaseEmbedding(ABC):
def _preprocess_inputs(self, raw_inputs: Any) -> Any:
pass
+ @staticmethod
+ def _bgr_to_rgb(frame: Any) -> Any:
+ if isinstance(frame, np.ndarray) and frame.ndim == 3:
+ return np.ascontiguousarray(frame[:, :, ::-1])
+ return frame
+
def _process_image(self, image, output: str = "RGB") -> Image.Image:
if isinstance(image, str):
if image.startswith("http"):
diff --git a/frigate/embeddings/onnx/face_embedding.py b/frigate/embeddings/onnx/face_embedding.py
index 75dfedc94d..31d2bd2066 100644
--- a/frigate/embeddings/onnx/face_embedding.py
+++ b/frigate/embeddings/onnx/face_embedding.py
@@ -73,7 +73,7 @@ class FaceNetEmbedding(BaseEmbedding):
self.tensor_output_details = self.runner.get_output_details()
def _preprocess_inputs(self, raw_inputs):
- pil = self._process_image(raw_inputs[0])
+ pil = self._process_image(self._bgr_to_rgb(raw_inputs[0]))
# handle images larger than input size
width, height = pil.size
@@ -159,7 +159,7 @@ class ArcfaceEmbedding(BaseEmbedding):
)
def _preprocess_inputs(self, raw_inputs):
- pil = self._process_image(raw_inputs[0])
+ pil = self._process_image(self._bgr_to_rgb(raw_inputs[0]))
# handle images larger than input size
width, height = pil.size
diff --git a/frigate/events/audio.py b/frigate/events/audio.py
index f6c41fa30b..9af1bd26b2 100644
--- a/frigate/events/audio.py
+++ b/frigate/events/audio.py
@@ -7,7 +7,7 @@ import threading
import time
from multiprocessing.managers import DictProxy
from multiprocessing.synchronize import Event as MpEvent
-from typing import Any, Tuple
+from typing import Any
import numpy as np
@@ -84,7 +84,6 @@ class AudioProcessor(FrigateProcess):
def __init__(
self,
config: FrigateConfig,
- cameras: list[CameraConfig],
camera_metrics: DictProxy,
stop_event: MpEvent,
):
@@ -93,16 +92,30 @@ class AudioProcessor(FrigateProcess):
)
self.camera_metrics = camera_metrics
- self.cameras = cameras
self.config = config
+ def __stop_audio_thread(self, camera: str) -> None:
+ thread = self.audio_threads.pop(camera, None)
+ if thread is None:
+ return
+
+ thread.stop()
+ thread.join(10)
+ if thread.is_alive():
+ self.logger.warning(f"Audio maintainer thread for {camera} is still alive")
+ else:
+ self.logger.info(f"Audio maintainer stopped for {camera}")
+
def run(self) -> None:
self.pre_run_setup(self.config.logger)
- audio_threads: list[AudioEventMaintainer] = []
+ self.audio_threads: dict[str, AudioEventMaintainer] = {}
threading.current_thread().name = "process:audio_manager"
- if self.config.audio_transcription.enabled:
+ if any(
+ c.enabled_in_config and c.audio_transcription.enabled
+ for c in self.config.cameras.values()
+ ):
self.transcription_model_runner: AudioTranscriptionModelRunner | None = (
AudioTranscriptionModelRunner(
self.config.audio_transcription.device or "AUTO",
@@ -112,32 +125,67 @@ class AudioProcessor(FrigateProcess):
else:
self.transcription_model_runner = None
- if len(self.cameras) == 0:
- return
+ config_subscriber = CameraConfigUpdateSubscriber(
+ self.config,
+ self.config.cameras,
+ [
+ CameraConfigUpdateEnum.add,
+ CameraConfigUpdateEnum.audio,
+ CameraConfigUpdateEnum.ffmpeg,
+ CameraConfigUpdateEnum.remove,
+ ],
+ )
- for camera in self.cameras:
- audio_thread = AudioEventMaintainer(
+ def spawn_if_needed(camera: CameraConfig) -> None:
+ name = camera.name
+ if name is None or name in self.audio_threads:
+ return
+ if not camera.enabled or not camera.audio.enabled:
+ return
+ # ffmpeg update may not have arrived yet; wait for next poll
+ if not any("audio" in i.roles for i in camera.ffmpeg.inputs):
+ return
+ thread = AudioEventMaintainer(
camera,
self.config,
self.camera_metrics,
self.transcription_model_runner,
self.stop_event, # type: ignore[arg-type]
)
- audio_threads.append(audio_thread)
- audio_thread.start()
+ self.audio_threads[name] = thread
+ thread.start()
+ self.logger.info(f"Audio maintainer started for {name}")
+
+ for camera in self.config.cameras.values():
+ spawn_if_needed(camera)
self.logger.info(f"Audio processor started (pid: {self.pid})")
- while not self.stop_event.wait():
- pass
+ # poll for newly added/removed cameras or cameras flipped to
+ # audio.enabled at runtime
+ while not self.stop_event.wait(timeout=1.0):
+ updated_topics = config_subscriber.check_for_updates()
- for thread in audio_threads:
+ # stop maintainers for removed cameras so their ffmpeg process is
+ # torn down and they stop touching camera_metrics (which the camera
+ # maintainer has already popped for the removed camera)
+ for removed_camera in updated_topics.get(
+ CameraConfigUpdateEnum.remove.name, []
+ ):
+ self.__stop_audio_thread(removed_camera)
+
+ for camera in self.config.cameras.values():
+ spawn_if_needed(camera)
+
+ config_subscriber.stop()
+
+ for thread in self.audio_threads.values():
thread.join(1)
if thread.is_alive():
self.logger.info(f"Waiting for thread {thread.name:s} to exit")
thread.join(10)
- for thread in audio_threads:
+ for thread in self.audio_threads.values():
if thread.is_alive():
self.logger.warning(f"Thread {thread.name} is still alive")
@@ -159,6 +207,9 @@ class AudioEventMaintainer(threading.Thread):
self.camera_config = camera
self.camera_metrics = camera_metrics
self.stop_event = stop_event
+ # per-camera stop signal so a single maintainer can be torn down at
+ # runtime (e.g. on camera removal) without stopping the whole process
+ self.camera_stop_event = threading.Event()
self.detector = AudioTfl(stop_event, self.camera_config.audio.num_threads)
self.shape = (int(round(AUDIO_DURATION * AUDIO_SAMPLE_RATE)),)
self.chunk_size = int(round(AUDIO_DURATION * AUDIO_SAMPLE_RATE * 2))
@@ -184,7 +235,7 @@ class AudioEventMaintainer(threading.Thread):
self.detection_publisher = DetectionPublisher(DetectionTypeEnum.audio.value)
if (
- self.config.audio_transcription.enabled
+ self.camera_config.audio_transcription.enabled
and self.audio_transcription_model_runner is not None
):
# init the transcription processor for this camera
@@ -205,9 +256,14 @@ class AudioEventMaintainer(threading.Thread):
self.transcription_thread.start()
self.was_enabled = camera.enabled
+ self.was_audio_enabled = camera.audio.enabled
def detect_audio(self, audio: np.ndarray) -> None:
- if not self.camera_config.audio.enabled or self.stop_event.is_set():
+ if (
+ not self.camera_config.audio.enabled
+ or self.stop_event.is_set()
+ or self.camera_stop_event.is_set()
+ ):
return
audio_as_float: np.ndarray = audio.astype(np.float32)
@@ -216,7 +272,7 @@ 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]] = []
+ audio_detections: list[tuple[str, float]] = []
# only run audio detection when volume is above min_volume
if rms >= self.camera_config.audio.min_volume:
@@ -267,7 +323,7 @@ class AudioEventMaintainer(threading.Thread):
else:
self.transcription_processor.check_unload_model()
- def calculate_audio_levels(self, audio_as_float: np.ndarray) -> Tuple[float, float]:
+ def calculate_audio_levels(self, audio_as_float: np.ndarray) -> tuple[float, float]:
# Calculate RMS (Root-Mean-Square) which represents the average signal amplitude
# Note: np.float32 isn't serializable, we must use np.float64 to publish the message
rms = np.sqrt(np.mean(np.absolute(np.square(audio_as_float))))
@@ -326,11 +382,15 @@ class AudioEventMaintainer(threading.Thread):
self.logger.error(f"Error reading audio data from ffmpeg process: {e}")
log_and_restart()
+ def stop(self) -> None:
+ """Signal this maintainer to exit its run loop and clean up."""
+ self.camera_stop_event.set()
+
def run(self) -> None:
if self.camera_config.enabled:
self.start_or_restart_ffmpeg()
- while not self.stop_event.is_set():
+ while not self.stop_event.is_set() and not self.camera_stop_event.is_set():
# check if there is an updated config
self.config_subscriber.check_for_updates()
@@ -363,6 +423,17 @@ class AudioEventMaintainer(threading.Thread):
time.sleep(0.1)
continue
+ audio_enabled = self.camera_config.audio.enabled
+ if audio_enabled != self.was_audio_enabled:
+ if not audio_enabled:
+ self.logger.debug(
+ f"Disabling audio detections for {self.camera_config.name}, ending events"
+ )
+ self.requestor.send_data(
+ EXPIRE_AUDIO_ACTIVITY, self.camera_config.name
+ )
+ self.was_audio_enabled = audio_enabled
+
self.read_audio()
if self.audio_listener:
diff --git a/frigate/events/maintainer.py b/frigate/events/maintainer.py
index 80bdaccd3b..169b2139ea 100644
--- a/frigate/events/maintainer.py
+++ b/frigate/events/maintainer.py
@@ -2,7 +2,7 @@ import logging
import threading
from multiprocessing import Queue
from multiprocessing.synchronize import Event as MpEvent
-from typing import Any, Dict
+from typing import Any
from frigate.comms.events_updater import EventEndPublisher, EventUpdateSubscriber
from frigate.config import FrigateConfig
@@ -76,7 +76,7 @@ class EventProcessor(threading.Thread):
super().__init__(name="event_processor")
self.config = config
self.timeline_queue = timeline_queue
- self.events_in_process: Dict[str, dict[str, Any]] = {}
+ self.events_in_process: dict[str, dict[str, Any]] = {}
self.stop_event = stop_event
self.event_receiver = EventUpdateSubscriber()
diff --git a/frigate/ffmpeg_presets.py b/frigate/ffmpeg_presets.py
index c314b30eaf..445cfd4df3 100644
--- a/frigate/ffmpeg_presets.py
+++ b/frigate/ffmpeg_presets.py
@@ -3,7 +3,7 @@
import logging
import os
from enum import Enum
-from typing import Any, Optional
+from typing import Any
from frigate.const import (
FFMPEG_HVC1_ARGS,
@@ -150,7 +150,11 @@ PRESETS_HW_ACCEL_SCALE["preset-rk-h265"] = PRESETS_HW_ACCEL_SCALE[FFMPEG_HWACCEL
PRESETS_HW_ACCEL_ENCODE_BIRDSEYE = {
"preset-rpi-64-h264": "{0} -hide_banner {1} -c:v h264_v4l2m2m {2}",
"preset-rpi-64-h265": "{0} -hide_banner {1} -c:v hevc_v4l2m2m {2}",
- FFMPEG_HWACCEL_VAAPI: "{0} -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {3} {1} -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf format=vaapi|nv12,hwupload {2}",
+ # -vaapi_device is required in addition to -hwaccel_device: this is the only
+ # birdseye preset that uses hwupload, and ffmpeg 8 initializes filters before
+ # the decoder creates a device, so hwupload cannot see an -hwaccel_device one.
+ # See https://github.com/AlexxIT/go2rtc/issues/1984
+ FFMPEG_HWACCEL_VAAPI: "{0} -hide_banner -vaapi_device {3} -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {3} {1} -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf format=vaapi|nv12,hwupload {2}",
"preset-intel-qsv-h264": "{0} -hide_banner {1} -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 {2}",
"preset-intel-qsv-h265": "{0} -hide_banner {1} -c:v h264_qsv -g 50 -bf 0 -profile:v main -level:v 4.1 -async_depth:v 1 {2}",
FFMPEG_HWACCEL_NVIDIA: "{0} -hide_banner {1} -c:v h264_nvenc -g 50 -profile:v high -level:v auto -preset:v p2 -tune:v ll {2}",
@@ -215,7 +219,7 @@ def parse_preset_hardware_acceleration_decode(
width: int,
height: int,
gpu: int,
-) -> Optional[list[str]]:
+) -> list[str] | None:
"""Return the correct preset if in preset format otherwise return None."""
if not isinstance(arg, str):
return None
@@ -420,7 +424,7 @@ PRESETS_INPUT = {
}
-def parse_preset_input(arg: Any, detect_fps: int) -> Optional[list[str]]:
+def parse_preset_input(arg: Any, detect_fps: int) -> list[str] | None:
"""Return the correct preset if in preset format otherwise return None."""
if not isinstance(arg, str):
return None
@@ -465,16 +469,6 @@ PRESETS_RECORD_OUTPUT = {
"-c:a",
"aac",
],
- # NOTE: This preset originally used "-c:a copy" to pass through audio
- # without re-encoding. FFmpeg 7.x introduced a threaded pipeline where
- # demuxing, encoding, and muxing run in parallel via a Scheduler. This
- # broke audio streamcopy from RTSP sources: packets are demuxed correctly
- # but silently dropped before reaching the muxer (0 bytes written). The
- # issue is specific to RTSP + streamcopy; file inputs and transcoding both
- # work. Transcoding AAC audio is very lightweight (~30KiB per 10s segment)
- # and adds negligible CPU overhead, so this is an acceptable workaround.
- # The benefits of FFmpeg 7.x — particularly the removal of gamma correction
- # hacks required by earlier versions — outweigh this trade-off.
"preset-record-generic-audio-copy": [
"-f",
"segment",
@@ -486,10 +480,8 @@ PRESETS_RECORD_OUTPUT = {
"1",
"-strftime",
"1",
- "-c:v",
+ "-c",
"copy",
- "-c:a",
- "aac",
],
"preset-record-mjpeg": [
"-f",
@@ -542,9 +534,7 @@ PRESETS_RECORD_OUTPUT = {
}
-def parse_preset_output_record(
- arg: Any, force_record_hvc1: bool
-) -> Optional[list[str]]:
+def parse_preset_output_record(arg: Any, force_record_hvc1: bool) -> list[str] | None:
"""Return the correct preset if in preset format otherwise return None."""
if not isinstance(arg, str):
return None
diff --git a/frigate/genai/__init__.py b/frigate/genai/__init__.py
index d95dd2cae2..3aca4a8fb4 100644
--- a/frigate/genai/__init__.py
+++ b/frigate/genai/__init__.py
@@ -1,19 +1,27 @@
"""Generative AI module for Frigate."""
-import datetime
import importlib
+import json
import logging
import os
import re
-from typing import Any, Callable, Optional
+import time
+from collections.abc import AsyncGenerator, Callable
+from typing import Any
import numpy as np
-from playhouse.shortcuts import model_to_dict
+from pydantic import ValidationError
from frigate.config import CameraConfig, GenAIConfig, GenAIProviderEnum
from frigate.const import CLIPS_DIR
from frigate.data_processing.post.types import ReviewMetadata
from frigate.genai.manager import GenAIClientManager
+from frigate.genai.prompts import (
+ build_object_description_prompt,
+ build_review_description_prompt,
+ build_review_description_response_format,
+ build_review_summary_prompt,
+)
from frigate.models import Event
logger = logging.getLogger(__name__)
@@ -44,10 +52,48 @@ def register_genai_provider(key: GenAIProviderEnum) -> Callable:
class GenAIClient:
"""Generative AI client for Frigate."""
- def __init__(self, genai_config: GenAIConfig, timeout: int = 120) -> None:
+ # Minimum seconds between re-initialization attempts when the provider was
+ # offline at startup
+ REINIT_INTERVAL = 60.0
+
+ def __init__(
+ self,
+ genai_config: GenAIConfig,
+ timeout: int = 120,
+ validate_model: bool = True,
+ ) -> None:
self.genai_config: GenAIConfig = genai_config
self.timeout = timeout
+ self.validate_model = validate_model
self.provider = self._init_provider()
+ self._last_init_attempt = time.monotonic()
+
+ def ensure_provider(self) -> bool:
+ """Ensure a provider is available, retrying initialization if needed.
+
+ Providers can fail to initialize at startup when their backing service
+ isn't online yet (common when both are started together). This retries
+ ``_init_provider`` lazily — throttled to ``REINIT_INTERVAL`` — so the
+ client recovers on its own once the service is reachable, without a
+ config reload.
+
+ Returns True if a provider is available.
+ """
+ if self.provider is not None:
+ return True
+
+ now = time.monotonic()
+ if now - self._last_init_attempt < self.REINIT_INTERVAL:
+ return False
+
+ self._last_init_attempt = now
+ self.provider = self._init_provider()
+ if self.provider is not None:
+ logger.info(
+ "GenAI provider %s is now available",
+ self.genai_config.provider,
+ )
+ return self.provider is not None
def generate_review_description(
self,
@@ -59,74 +105,14 @@ class GenAIClient:
activity_context_prompt: str,
) -> ReviewMetadata | None:
"""Generate a description for the review item activity."""
+ context_prompt = build_review_description_prompt(
+ review_data,
+ thumbnails,
+ concerns,
+ preferred_language,
+ activity_context_prompt,
+ )
- def get_concern_prompt() -> str:
- if concerns:
- concern_list = "\n - ".join(concerns)
- return f"""- `other_concerns` (list of strings): Include a list of any of the following concerns that are occurring:
- - {concern_list}"""
- else:
- return ""
-
- def get_language_prompt() -> str:
- if preferred_language:
- return f"Provide your answer in {preferred_language}"
- else:
- return ""
-
- def get_objects_list() -> str:
- if review_data["unified_objects"]:
- return "\n- " + "\n- ".join(review_data["unified_objects"])
- else:
- return "\n- (No objects detected)"
-
- context_prompt = f"""
-Your task is to analyze a sequence of images taken in chronological order from a security camera.
-
-## Normal Activity Patterns for This Property
-
-{activity_context_prompt}
-
-## Task Instructions
-
-Describe the scene based on observable actions and movements, evaluate the activity against the Activity Indicators above, and assign a potential_threat_level (0, 1, or 2) by applying the threat level indicators consistently.
-
-## Analysis Guidelines
-
-When forming your description:
-- **CRITICAL: Only describe objects explicitly listed in "Objects in Scene" below.** Do not infer or mention additional people, vehicles, or objects not present in this list, even if visual patterns suggest them. If only a car is listed, do not describe a person interacting with it unless "person" is also in the objects list.
-- **Only describe actions actually visible in the frames.** Do not assume or infer actions that you don't observe happening. If someone walks toward furniture but you never see them sit, do not say they sat. Stick to what you can see across the sequence.
-- Describe what you observe: actions, movements, interactions with objects and the environment. Include any observable environmental changes (e.g., lighting changes triggered by activity).
-- Note visible details such as clothing, items being carried or placed, tools or equipment present, and how they interact with the property or objects.
-- Consider the full sequence chronologically: what happens from start to finish, how duration and actions relate to the location and objects involved.
-- **Use the actual timestamp provided in "Activity started at"** below for time of day context—do not infer time from image brightness or darkness. Unusual hours (late night/early morning) should increase suspicion when the observable behavior itself appears questionable. However, recognize that some legitimate activities can occur at any hour.
-- **Consider duration as a primary factor**: Apply the duration thresholds defined in the activity patterns above. Brief sequences during normal hours with apparent purpose typically indicate normal activity unless explicit suspicious actions are visible.
-- **Weigh all evidence holistically**: Match the activity against the normal and suspicious patterns defined above, then evaluate based on the complete context (zone, objects, time, actions, duration). Apply the threat level indicators consistently. Use your judgment for edge cases.
-
-## Response Field Guidelines
-
-Respond with a JSON object matching the provided schema. Field-specific guidance:
-- `scene`: Describe how the sequence begins, then the progression of events — all significant movements and actions in order. For example, if a vehicle arrives and then a person exits, describe both sequentially. For named subjects (those with a `←` separator in "Objects in Scene"), always use their name — do not replace them with generic terms. For unnamed objects (e.g., "person", "car"), refer to them naturally with articles (e.g., "a person", "the car"). Your description should align with and support the threat level you assign.
-- `title`: Characterize **what took place and where** — interpret the overall purpose or outcome, do not simply compress the scene description into fewer words. Include the relevant location (zone, area, or entry point). For named subjects, always use their name. For unnamed objects, refer to them naturally with articles. No editorial qualifiers like "routine" or "suspicious."
-- `potential_threat_level`: Must be consistent with your scene description and the activity patterns above.
-{get_concern_prompt()}
-
-## Sequence Details
-
-- Camera: {review_data["camera"]}
-- Total frames: {len(thumbnails)} (Frame 1 = earliest, Frame {len(thumbnails)} = latest)
-- Activity started at {review_data["start"]} and lasted {review_data["duration"]} seconds
-- Zones involved: {", ".join(review_data["zones"]) if review_data["zones"] else "None"}
-
-## Objects in Scene
-
-Each line represents a detection state, not necessarily unique individuals. The `←` symbol separates a recognized subject's name from their object type — use only the name (before the `←`) in your response, not the type after it. The same subject may appear across multiple lines if detected multiple times.
-
-**Note: Unidentified objects (without names) are NOT indicators of suspicious activity—they simply mean the system hasn't identified that object.**
-{get_objects_list()}
-
-{get_language_prompt()}
-"""
logger.debug(
f"Sending {len(thumbnails)} images to create review description on {review_data['camera']}"
)
@@ -140,28 +126,7 @@ Each line represents a detection state, not necessarily unique individuals. The
) as f:
f.write(context_prompt)
- # Build JSON schema for structured output from ReviewMetadata model
- schema = ReviewMetadata.model_json_schema()
- schema.get("properties", {}).pop("time", None)
-
- if "time" in schema.get("required", []):
- schema["required"].remove("time")
- if not concerns:
- schema.get("properties", {}).pop("other_concerns", None)
- if "other_concerns" in schema.get("required", []):
- schema["required"].remove("other_concerns")
-
- # OpenAI strict mode requires additionalProperties: false on all objects
- schema["additionalProperties"] = False
-
- response_format = {
- "type": "json_schema",
- "json_schema": {
- "name": "review_metadata",
- "strict": True,
- "schema": schema,
- },
- }
+ response_format = build_review_description_response_format(concerns)
response = self._send(context_prompt, thumbnails, response_format)
@@ -181,7 +146,36 @@ Each line represents a detection state, not necessarily unique individuals. The
try:
metadata = ReviewMetadata.model_validate_json(clean_json)
+ except ValidationError as ve:
+ # Constraint violations (length, item count, ranges) are logged
+ # at debug and the response is kept anyway — a slightly
+ # off-spec answer is still usable, and dropping the whole
+ # response loses the narrative content the model produced.
+ for err in ve.errors():
+ loc = ".".join(str(p) for p in err["loc"]) or ""
+ logger.debug(
+ "Review metadata soft validation: %s — %s (input: %r)",
+ loc,
+ err["msg"],
+ err.get("input"),
+ )
+ try:
+ raw = json.loads(clean_json)
+ except json.JSONDecodeError as je:
+ logger.error("Failed to parse review description JSON: %s", je)
+ return None
+ # observations and confidence are required on the model; fill an empty default
+ # if the response omitted it so attribute access stays safe.
+ raw.setdefault("observations", [])
+ raw.setdefault("confidence", 0.0)
+ metadata = ReviewMetadata.model_construct(**raw)
+ except Exception as e:
+ logger.error(
+ f"Failed to parse review description as the response did not match expected format. {e}"
+ )
+ return None
+ try:
# Normalize confidence if model returned a percentage (e.g. 85 instead of 0.85)
if metadata.confidence > 1.0:
metadata.confidence = min(metadata.confidence / 100.0, 1.0)
@@ -194,10 +188,7 @@ Each line represents a detection state, not necessarily unique individuals. The
metadata.time = review_data["start"]
return metadata
except Exception as e:
- # rarely LLMs can fail to follow directions on output format
- logger.warning(
- f"Failed to parse review description as the response did not match expected format. {e}"
- )
+ logger.error(f"Failed to post-process review metadata: {e}")
return None
else:
logger.debug(
@@ -214,61 +205,9 @@ Each line represents a detection state, not necessarily unique individuals. The
debug_save: bool,
) -> str | None:
"""Generate a summary of review item descriptions over a period of time."""
- time_range = f"{datetime.datetime.fromtimestamp(start_ts).strftime('%B %d, %Y at %I:%M %p')} to {datetime.datetime.fromtimestamp(end_ts).strftime('%B %d, %Y at %I:%M %p')}"
- timeline_summary_prompt = f"""
-You are a security officer writing a concise security report.
-
-Time range: {time_range}
-
-Input format: Each event is a JSON object with:
-- "title", "scene", "confidence", "potential_threat_level" (0-2), "other_concerns", "camera", "time", "start_time", "end_time"
-- "context": array of related events from other cameras that occurred during overlapping time periods
-
-**Note: Use the "scene" field for event descriptions in the report. Ignore any "shortSummary" field if present.**
-
-Report Structure - Use this EXACT format:
-
-# Security Summary - {time_range}
-
-## Overview
-[Write 1-2 sentences summarizing the overall activity pattern during this period.]
-
----
-
-## Timeline
-
-[Group events by time periods (e.g., "Morning (6:00 AM - 12:00 PM)", "Afternoon (12:00 PM - 5:00 PM)", "Evening (5:00 PM - 9:00 PM)", "Night (9:00 PM - 6:00 AM)"). Use appropriate time blocks based on when events occurred.]
-
-### [Time Block Name]
-
-**HH:MM AM/PM** | [Camera Name] | [Threat Level Indicator]
-- [Event title]: [Clear description incorporating contextual information from the "context" array]
-- Context: [If context array has items, mention them here, e.g., "Delivery truck present on Front Driveway Cam (HH:MM AM/PM)"]
-- Assessment: [Brief assessment incorporating context - if context explains the event, note it here]
-
-[Repeat for each event in chronological order within the time block]
-
----
-
-## Summary
-[One sentence summarizing the period. If all events are normal/explained: "Routine activity observed." If review needed: "Some activity requires review but no security concerns." If security concerns: "Security concerns requiring immediate attention."]
-
-Guidelines:
-- List ALL events in chronological order, grouped by time blocks
-- Threat level indicators: ✓ Normal, ⚠️ Needs review, 🔴 Security concern
-- Integrate contextual information naturally - use the "context" array to enrich each event's description
-- If context explains the event (e.g., delivery truck explains person at door), describe it accordingly (e.g., "delivery person" not "unidentified person")
-- Be concise but informative - focus on what happened and what it means
-- If contextual information makes an event clearly normal, reflect that in your assessment
-- Only create time blocks that have events - don't create empty sections
-"""
-
- timeline_summary_prompt += "\n\nEvents:\n"
- for event in events:
- timeline_summary_prompt += f"\n{event}\n"
-
- if preferred_language:
- timeline_summary_prompt += f"\nProvide your answer in {preferred_language}"
+ timeline_summary_prompt = build_review_summary_prompt(
+ start_ts, end_ts, events, preferred_language
+ )
if debug_save:
with open(
@@ -297,13 +236,10 @@ Guidelines:
camera_config: CameraConfig,
thumbnails: list[bytes],
event: Event,
- ) -> Optional[str]:
+ ) -> str | None:
"""Generate a description for the frame."""
try:
- prompt = camera_config.objects.genai.object_prompts.get(
- str(event.label),
- camera_config.objects.genai.prompt,
- ).format(**model_to_dict(event))
+ prompt = build_object_description_prompt(camera_config, event)
except KeyError as e:
logger.error(f"Invalid key in GenAI prompt: {e}")
return None
@@ -319,9 +255,16 @@ Guidelines:
self,
prompt: str,
images: list[bytes],
- response_format: Optional[dict] = None,
- ) -> Optional[str]:
- """Submit a request to the provider."""
+ response_format: dict | None = None,
+ enable_thinking: bool = False,
+ ) -> str | None:
+ """Submit a request to the provider.
+
+ ``enable_thinking`` is honored only by providers that report
+ ``supports_toggleable_thinking``. Description-style callers leave it
+ at the default (off) since synthesis tasks don't benefit from
+ reasoning traces.
+ """
return None
@property
@@ -333,6 +276,16 @@ Guidelines:
"""
return True
+ @property
+ def supports_toggleable_thinking(self) -> bool:
+ """Whether the configured model exposes a per-request thinking toggle."""
+ return False
+
+ @property
+ def supports_embeddings(self) -> bool:
+ """Whether the configured model can generate embeddings via embed()."""
+ return False
+
def list_models(self) -> list[str]:
"""Return the list of model names available from this provider.
@@ -344,6 +297,14 @@ Guidelines:
"""Get the context window size for this provider in tokens."""
return 4096
+ def estimate_image_tokens(self, width: int, height: int) -> float:
+ """Estimate prompt tokens consumed by a single image of the given dimensions.
+
+ Default heuristic: ~1 token per 1250 pixels. Providers that can measure or
+ know their model's exact image-token cost should override.
+ """
+ return (width * height) / 1250
+
def embed(
self,
texts: list[str] | None = None,
@@ -366,8 +327,9 @@ Guidelines:
def chat_with_tools(
self,
messages: list[dict[str, Any]],
- tools: Optional[list[dict[str, Any]]] = None,
- tool_choice: Optional[str] = "auto",
+ tools: list[dict[str, Any]] | None = None,
+ tool_choice: str | None = "auto",
+ enable_thinking: bool | None = None,
) -> dict[str, Any]:
"""
Send chat messages to LLM with optional tool definitions.
@@ -391,11 +353,17 @@ Guidelines:
- 'none': Model must not call tools
- 'required': Model must call at least one tool
- Or a dict specifying a specific tool to call
- **kwargs: Additional provider-specific parameters.
+ enable_thinking: Per-request thinking toggle. None means use the
+ provider default. Ignored by providers without a per-request
+ toggle (see `supports_toggleable_thinking`).
Returns:
Dictionary with:
- 'content': Optional[str] - The text response from the LLM, None if tool calls
+ - 'reasoning': Optional[str] - The separated reasoning/thinking trace
+ if the model emitted one (e.g. via OpenAI-compatible
+ `reasoning_content`). None when the model does not surface a
+ trace or the provider does not parse it.
- 'tool_calls': Optional[List[Dict]] - List of tool calls if LLM wants to call tools.
Each tool call dict has:
- 'id': str - Unique identifier for this tool call
@@ -407,6 +375,14 @@ Guidelines:
- 'length': Hit token limit
- 'error': An error occurred
+ Streaming counterpart `chat_with_tools_stream` yields
+ ``(kind, value)`` tuples where ``kind`` is one of:
+ - 'content_delta': value is a string fragment of the answer
+ - 'reasoning_delta': value is a string fragment of the reasoning
+ trace (emitted before content for thinking models)
+ - 'stats': value is a usage stats dict
+ - 'message': value is the final dict shape described above
+
Raises:
NotImplementedError: If the provider doesn't implement this method.
"""
@@ -417,14 +393,50 @@ Guidelines:
)
return {
"content": None,
+ "reasoning": None,
"tool_calls": None,
"finish_reason": "error",
}
+ async def chat_with_tools_stream(
+ self,
+ messages: list[dict[str, Any]],
+ tools: list[dict[str, Any]] | None = None,
+ tool_choice: str | None = "auto",
+ enable_thinking: bool | None = None,
+ ) -> AsyncGenerator[tuple[str, Any], None]:
+ """Streaming counterpart to `chat_with_tools`.
+
+ Yields ``(kind, value)`` tuples where ``kind`` is one of:
+ - 'content_delta': value is a string fragment of the answer
+ - 'reasoning_delta': value is a string fragment of the reasoning
+ trace (emitted before content for thinking models)
+ - 'stats': value is a usage stats dict
+ - 'message': value is the final dict shape described in
+ `chat_with_tools`
+
+ Argument semantics — including ``enable_thinking`` — match
+ `chat_with_tools`. Providers that don't support streaming should
+ override this and yield an error 'message' event.
+ """
+ logger.warning(
+ f"{self.__class__.__name__} does not support chat_with_tools_stream. "
+ "This method should be overridden by the provider implementation."
+ )
+ yield (
+ "message",
+ {
+ "content": None,
+ "reasoning": None,
+ "tool_calls": None,
+ "finish_reason": "error",
+ },
+ )
+
def load_providers() -> None:
- package_dir = os.path.dirname(__file__)
- for filename in os.listdir(package_dir):
+ plugins_dir = os.path.join(os.path.dirname(__file__), "plugins")
+ for filename in os.listdir(plugins_dir):
if filename.endswith(".py") and filename != "__init__.py":
- module_name = f"frigate.genai.{filename[:-3]}"
+ module_name = f"frigate.genai.plugins.{filename[:-3]}"
importlib.import_module(module_name)
diff --git a/frigate/genai/azure-openai.py b/frigate/genai/azure-openai.py
deleted file mode 100644
index 66d7d1568e..0000000000
--- a/frigate/genai/azure-openai.py
+++ /dev/null
@@ -1,305 +0,0 @@
-"""Azure OpenAI Provider for Frigate AI."""
-
-import base64
-import json
-import logging
-from typing import Any, AsyncGenerator, Optional
-from urllib.parse import parse_qs, urlparse
-
-from openai import AzureOpenAI
-
-from frigate.config import GenAIProviderEnum
-from frigate.genai import GenAIClient, register_genai_provider
-
-logger = logging.getLogger(__name__)
-
-
-@register_genai_provider(GenAIProviderEnum.azure_openai)
-class OpenAIClient(GenAIClient):
- """Generative AI client for Frigate using Azure OpenAI."""
-
- provider: AzureOpenAI
-
- def _init_provider(self) -> AzureOpenAI | None:
- """Initialize the client."""
- try:
- parsed_url = urlparse(self.genai_config.base_url or "")
- query_params = parse_qs(parsed_url.query)
- api_version = query_params.get("api-version", [None])[0]
- azure_endpoint = f"{parsed_url.scheme}://{parsed_url.netloc}/"
-
- if not api_version:
- logger.warning("Azure OpenAI url is missing API version.")
- return None
-
- except Exception as e:
- logger.warning("Error parsing Azure OpenAI url: %s", str(e))
- return None
-
- return AzureOpenAI(
- api_key=self.genai_config.api_key,
- api_version=api_version,
- azure_endpoint=azure_endpoint,
- )
-
- def _send(
- self,
- prompt: str,
- images: list[bytes],
- response_format: Optional[dict] = None,
- ) -> Optional[str]:
- """Submit a request to Azure OpenAI."""
- encoded_images = [base64.b64encode(image).decode("utf-8") for image in images]
- try:
- request_params = {
- "model": self.genai_config.model,
- "messages": [
- {
- "role": "user",
- "content": [{"type": "text", "text": prompt}]
- + [
- {
- "type": "image_url",
- "image_url": {
- "url": f"data:image/jpeg;base64,{image}",
- "detail": "low",
- },
- }
- for image in encoded_images
- ],
- },
- ],
- "timeout": self.timeout,
- **self.genai_config.runtime_options,
- }
- if response_format:
- request_params["response_format"] = response_format
- result = self.provider.chat.completions.create(**request_params)
- except Exception as e:
- logger.warning("Azure OpenAI returned an error: %s", str(e))
- return None
- if len(result.choices) > 0:
- return str(result.choices[0].message.content.strip())
- return None
-
- def list_models(self) -> list[str]:
- """Return available model IDs from Azure OpenAI."""
- try:
- return sorted(m.id for m in self.provider.models.list().data)
- except Exception as e:
- logger.warning("Failed to list Azure OpenAI models: %s", e)
- return []
-
- def get_context_size(self) -> int:
- """Get the context window size for Azure OpenAI."""
- return 128000
-
- def chat_with_tools(
- self,
- messages: list[dict[str, Any]],
- tools: Optional[list[dict[str, Any]]] = None,
- tool_choice: Optional[str] = "auto",
- ) -> dict[str, Any]:
- try:
- openai_tool_choice = None
- if tool_choice:
- if tool_choice == "none":
- openai_tool_choice = "none"
- elif tool_choice == "auto":
- openai_tool_choice = "auto"
- elif tool_choice == "required":
- openai_tool_choice = "required"
-
- request_params = {
- "model": self.genai_config.model,
- "messages": messages,
- "timeout": self.timeout,
- }
-
- if tools:
- request_params["tools"] = tools
- if openai_tool_choice is not None:
- request_params["tool_choice"] = openai_tool_choice
-
- result = self.provider.chat.completions.create(**request_params) # type: ignore[call-overload]
-
- if (
- result is None
- or not hasattr(result, "choices")
- or len(result.choices) == 0
- ):
- return {
- "content": None,
- "tool_calls": None,
- "finish_reason": "error",
- }
-
- choice = result.choices[0]
- message = choice.message
-
- content = message.content.strip() if message.content else None
-
- tool_calls = None
- if message.tool_calls:
- tool_calls = []
- for tool_call in message.tool_calls:
- try:
- arguments = json.loads(tool_call.function.arguments)
- except (json.JSONDecodeError, AttributeError) as e:
- logger.warning(
- f"Failed to parse tool call arguments: {e}, "
- f"tool: {tool_call.function.name if hasattr(tool_call.function, 'name') else 'unknown'}"
- )
- arguments = {}
-
- tool_calls.append(
- {
- "id": tool_call.id if hasattr(tool_call, "id") else "",
- "name": tool_call.function.name
- if hasattr(tool_call.function, "name")
- else "",
- "arguments": arguments,
- }
- )
-
- finish_reason = "error"
- if hasattr(choice, "finish_reason") and choice.finish_reason:
- finish_reason = choice.finish_reason
- elif tool_calls:
- finish_reason = "tool_calls"
- elif content:
- finish_reason = "stop"
-
- return {
- "content": content,
- "tool_calls": tool_calls,
- "finish_reason": finish_reason,
- }
-
- except Exception as e:
- logger.warning("Azure OpenAI returned an error: %s", str(e))
- return {
- "content": None,
- "tool_calls": None,
- "finish_reason": "error",
- }
-
- async def chat_with_tools_stream(
- self,
- messages: list[dict[str, Any]],
- tools: Optional[list[dict[str, Any]]] = None,
- tool_choice: Optional[str] = "auto",
- ) -> AsyncGenerator[tuple[str, Any], None]:
- """
- Stream chat with tools; yields content deltas then final message.
-
- Implements streaming function calling/tool usage for Azure OpenAI models.
- """
- try:
- openai_tool_choice = None
- if tool_choice:
- if tool_choice == "none":
- openai_tool_choice = "none"
- elif tool_choice == "auto":
- openai_tool_choice = "auto"
- elif tool_choice == "required":
- openai_tool_choice = "required"
-
- request_params = {
- "model": self.genai_config.model,
- "messages": messages,
- "timeout": self.timeout,
- "stream": True,
- }
-
- if tools:
- request_params["tools"] = tools
- if openai_tool_choice is not None:
- request_params["tool_choice"] = openai_tool_choice
-
- # Use streaming API
- content_parts: list[str] = []
- tool_calls_by_index: dict[int, dict[str, Any]] = {}
- finish_reason = "stop"
-
- stream = self.provider.chat.completions.create(**request_params) # type: ignore[call-overload]
-
- for chunk in stream:
- if not chunk or not chunk.choices:
- continue
-
- choice = chunk.choices[0]
- delta = choice.delta
-
- # Check for finish reason
- if choice.finish_reason:
- finish_reason = choice.finish_reason
-
- # Extract content deltas
- if delta.content:
- content_parts.append(delta.content)
- yield ("content_delta", delta.content)
-
- # Extract tool calls
- if delta.tool_calls:
- for tc in delta.tool_calls:
- idx = tc.index
- fn = tc.function
-
- if idx not in tool_calls_by_index:
- tool_calls_by_index[idx] = {
- "id": tc.id or "",
- "name": fn.name if fn and fn.name else "",
- "arguments": "",
- }
-
- t = tool_calls_by_index[idx]
- if tc.id:
- t["id"] = tc.id
- if fn and fn.name:
- t["name"] = fn.name
- if fn and fn.arguments:
- t["arguments"] += fn.arguments
-
- # Build final message
- full_content = "".join(content_parts).strip() or None
-
- # Convert tool calls to list format
- tool_calls_list = None
- if tool_calls_by_index:
- tool_calls_list = []
- for tc in tool_calls_by_index.values():
- try:
- # Parse accumulated arguments as JSON
- parsed_args = json.loads(tc["arguments"])
- except (json.JSONDecodeError, Exception):
- parsed_args = tc["arguments"]
-
- tool_calls_list.append(
- {
- "id": tc["id"],
- "name": tc["name"],
- "arguments": parsed_args,
- }
- )
- finish_reason = "tool_calls"
-
- yield (
- "message",
- {
- "content": full_content,
- "tool_calls": tool_calls_list,
- "finish_reason": finish_reason,
- },
- )
-
- except Exception as e:
- logger.warning("Azure OpenAI streaming returned an error: %s", str(e))
- yield (
- "message",
- {
- "content": None,
- "tool_calls": None,
- "finish_reason": "error",
- },
- )
diff --git a/frigate/genai/manager.py b/frigate/genai/manager.py
index 94719f4291..1301f1b7a1 100644
--- a/frigate/genai/manager.py
+++ b/frigate/genai/manager.py
@@ -6,7 +6,7 @@ no chat feature is active) are never initialized.
"""
import logging
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING, Any
from frigate.config import FrigateConfig
from frigate.config.camera.genai import GenAIConfig, GenAIRoleEnum
@@ -23,7 +23,7 @@ class GenAIClientManager:
def __init__(self, config: FrigateConfig) -> None:
self._configs: dict[str, GenAIConfig] = {}
self._role_map: dict[GenAIRoleEnum, str] = {}
- self._clients: dict[str, "GenAIClient"] = {}
+ self._clients: dict[str, GenAIClient] = {}
self.update_config(config)
def update_config(self, config: FrigateConfig) -> None:
@@ -59,10 +59,12 @@ class GenAIClientManager:
for role in genai_cfg.roles:
self._role_map[role] = name
- def _get_client(self, name: str) -> "Optional[GenAIClient]":
+ def _get_client(self, name: str) -> "GenAIClient | None":
"""Return the client for *name*, creating it on first access."""
if name in self._clients:
- return self._clients[name]
+ client = self._clients[name]
+ client.ensure_provider()
+ return client
from frigate.genai import PROVIDERS
@@ -78,7 +80,7 @@ class GenAIClientManager:
return None
try:
- client: "GenAIClient" = provider_cls(genai_cfg)
+ client = provider_cls(genai_cfg)
except Exception as e:
logger.exception(
"Failed to create GenAI client for provider %s: %s",
@@ -91,28 +93,34 @@ class GenAIClientManager:
return client
@property
- def chat_client(self) -> "Optional[GenAIClient]":
+ def chat_client(self) -> "GenAIClient | None":
"""Client configured for the chat role (e.g. chat with function calling)."""
name = self._role_map.get(GenAIRoleEnum.chat)
return self._get_client(name) if name else None
@property
- def description_client(self) -> "Optional[GenAIClient]":
+ def description_client(self) -> "GenAIClient | None":
"""Client configured for the descriptions role (e.g. review descriptions, object descriptions)."""
name = self._role_map.get(GenAIRoleEnum.descriptions)
return self._get_client(name) if name else None
@property
- def embeddings_client(self) -> "Optional[GenAIClient]":
+ def embeddings_client(self) -> "GenAIClient | None":
"""Client configured for the embeddings role."""
name = self._role_map.get(GenAIRoleEnum.embeddings)
return self._get_client(name) if name else None
- def list_models(self) -> dict[str, list[str]]:
- """Return available models keyed by config entry name."""
- result: dict[str, list[str]] = {}
- for name in self._configs:
+ def list_models(self) -> dict[str, dict[str, Any]]:
+ """Return per-entry model lists and capabilities, keyed by config entry name."""
+ result: dict[str, dict[str, Any]] = {}
+ for name, genai_cfg in self._configs.items():
client = self._get_client(name)
- if client:
- result[name] = client.list_models()
+ if not client:
+ continue
+ result[name] = {
+ "models": client.list_models(),
+ "roles": [r.value for r in genai_cfg.roles],
+ "supports_toggleable_thinking": client.supports_toggleable_thinking,
+ "supports_embeddings": client.supports_embeddings,
+ }
return result
diff --git a/frigate/genai/plugins/__init__.py b/frigate/genai/plugins/__init__.py
new file mode 100644
index 0000000000..e6d66077d3
--- /dev/null
+++ b/frigate/genai/plugins/__init__.py
@@ -0,0 +1 @@
+"""GenAI provider plugins."""
diff --git a/frigate/genai/plugins/azure-openai.py b/frigate/genai/plugins/azure-openai.py
new file mode 100644
index 0000000000..3599eb0dbd
--- /dev/null
+++ b/frigate/genai/plugins/azure-openai.py
@@ -0,0 +1,53 @@
+"""Azure OpenAI Provider for Frigate AI.
+
+Azure OpenAI exposes the same chat completions API as OpenAI once the
+client is constructed, so this provider inherits all transport, streaming,
+reasoning, and tool-calling logic from :class:`OpenAIClient` and only
+overrides what is genuinely Azure-specific:
+
+- Client construction: parses ``api-version`` out of the configured
+ ``base_url`` query string and instantiates :class:`openai.AzureOpenAI`
+ with ``azure_endpoint`` instead of ``base_url``. Raises if the URL is
+ malformed; :class:`GenAIClientManager` catches the exception and
+ disables the provider.
+- Context size: Azure does not expose a per-model ``max_model_len`` field
+ reliably, so we keep the historical 128K default rather than the
+ model-name heuristic used by OpenAI.
+"""
+
+import logging
+from urllib.parse import parse_qs, urlparse
+
+from openai import AzureOpenAI
+
+from frigate.config import GenAIProviderEnum
+from frigate.genai import register_genai_provider
+from frigate.genai.plugins.openai import OpenAIClient
+
+logger = logging.getLogger(__name__)
+
+
+@register_genai_provider(GenAIProviderEnum.azure_openai)
+class AzureOpenAIClient(OpenAIClient):
+ """Generative AI client for Frigate using Azure OpenAI."""
+
+ def _init_provider(self) -> AzureOpenAI:
+ """Initialize the AzureOpenAI client from the configured base_url."""
+ parsed_url = urlparse(self.genai_config.base_url or "")
+ query_params = parse_qs(parsed_url.query)
+ api_version = query_params.get("api-version", [None])[0]
+
+ if not api_version:
+ raise ValueError("Azure OpenAI base_url is missing api-version.")
+
+ azure_endpoint = f"{parsed_url.scheme}://{parsed_url.netloc}/"
+
+ return AzureOpenAI(
+ api_key=self.genai_config.api_key,
+ api_version=api_version,
+ azure_endpoint=azure_endpoint,
+ )
+
+ def get_context_size(self) -> int:
+ """Azure does not reliably surface per-model context size; use 128K."""
+ return 128000
diff --git a/frigate/genai/gemini.py b/frigate/genai/plugins/gemini.py
similarity index 60%
rename from frigate/genai/gemini.py
rename to frigate/genai/plugins/gemini.py
index cfa9cb8029..1c89806082 100644
--- a/frigate/genai/gemini.py
+++ b/frigate/genai/plugins/gemini.py
@@ -1,8 +1,11 @@
"""Gemini Provider for Frigate AI."""
+import base64
+import binascii
import json
import logging
-from typing import Any, AsyncGenerator, Optional
+from collections.abc import AsyncGenerator
+from typing import Any
from google import genai
from google.genai import errors, types
@@ -14,6 +17,72 @@ from frigate.genai import GenAIClient, register_genai_provider
logger = logging.getLogger(__name__)
+def _decode_thought_signature(value: Any) -> bytes | None:
+ """Decode a base64-encoded thought_signature carried across conversation turns."""
+ if not value:
+ return None
+ if isinstance(value, bytes):
+ return value
+ if isinstance(value, str):
+ try:
+ return base64.b64decode(value)
+ except (binascii.Error, ValueError):
+ return None
+ return None
+
+
+def _encode_thought_signature(signature: bytes | None) -> str | None:
+ """Encode bytes thought_signature as base64 so it survives JSON-friendly transport."""
+ if not signature:
+ return None
+ return base64.b64encode(signature).decode("ascii")
+
+
+def _decode_data_uri(url: str) -> tuple[str, bytes] | None:
+ """Decode a ``data:`` URI into ``(mime_type, bytes)``; None if not a data URI."""
+ if not isinstance(url, str) or not url.startswith("data:"):
+ return None
+ try:
+ header, b64 = url.split(",", 1)
+ mime = header[len("data:") :].split(";")[0] or "image/jpeg"
+ return mime, base64.b64decode(b64)
+ except (ValueError, binascii.Error):
+ return None
+
+
+def _parts_from_content(content: Any) -> list[types.Part]:
+ """Convert OpenAI-style message content (str or multimodal list) to Gemini parts."""
+ if isinstance(content, list):
+ parts: list[types.Part] = []
+ for item in content:
+ if not isinstance(item, dict):
+ continue
+ if item.get("type") == "text":
+ parts.append(types.Part.from_text(text=item.get("text") or ""))
+ elif item.get("type") == "image_url":
+ decoded = _decode_data_uri((item.get("image_url") or {}).get("url", ""))
+ if decoded is not None:
+ mime, data = decoded
+ parts.append(types.Part.from_bytes(data=data, mime_type=mime))
+ # Gemini rejects empty parts; fall back to a single space.
+ return parts or [types.Part.from_text(text=" ")]
+ return [types.Part.from_text(text=content or "")]
+
+
+def _stats_from_gemini_usage(usage: Any) -> dict[str, Any] | None:
+ """Build a stats dict from a Gemini usage_metadata object."""
+ prompt_tokens = getattr(usage, "prompt_token_count", None)
+ completion_tokens = getattr(usage, "candidates_token_count", None)
+ if prompt_tokens is None and completion_tokens is None:
+ return None
+ stats: dict[str, Any] = {}
+ if isinstance(prompt_tokens, int):
+ stats["prompt_tokens"] = prompt_tokens
+ if isinstance(completion_tokens, int):
+ stats["completion_tokens"] = completion_tokens
+ return stats or None
+
+
@register_genai_provider(GenAIProviderEnum.gemini)
class GeminiClient(GenAIClient):
"""Generative AI client for Frigate using Gemini."""
@@ -47,8 +116,9 @@ class GeminiClient(GenAIClient):
self,
prompt: str,
images: list[bytes],
- response_format: Optional[dict] = None,
- ) -> Optional[str]:
+ response_format: dict | None = None,
+ enable_thinking: bool = False,
+ ) -> str | None:
"""Submit a request to Gemini."""
contents = [prompt] + [
types.Part.from_bytes(data=img, mime_type="image/jpeg") for img in images
@@ -103,13 +173,16 @@ class GeminiClient(GenAIClient):
def chat_with_tools(
self,
messages: list[dict[str, Any]],
- tools: Optional[list[dict[str, Any]]] = None,
- tool_choice: Optional[str] = "auto",
+ tools: list[dict[str, Any]] | None = None,
+ tool_choice: str | None = "auto",
+ enable_thinking: bool | None = None,
) -> dict[str, Any]:
"""
Send chat messages to Gemini with optional tool definitions.
- Implements function calling/tool usage for Gemini models.
+ Implements function calling/tool usage for Gemini models. Thinking is
+ configured at the model level for Gemini, so ``enable_thinking`` is
+ accepted for interface parity and ignored.
"""
try:
# Convert messages to Gemini format
@@ -136,30 +209,56 @@ class GeminiClient(GenAIClient):
)
)
elif role == "assistant":
- gemini_messages.append(
- types.Content(
- role="model", parts=[types.Part.from_text(text=content)]
- )
- )
+ parts: list[types.Part] = []
+ if content:
+ parts.append(types.Part.from_text(text=content))
+ for tc in msg.get("tool_calls") or []:
+ func = tc.get("function") or {}
+ tc_name = func.get("name") or ""
+ tc_args: Any = func.get("arguments")
+ if isinstance(tc_args, str):
+ try:
+ tc_args = json.loads(tc_args)
+ except (json.JSONDecodeError, TypeError):
+ tc_args = {}
+ if not isinstance(tc_args, dict):
+ tc_args = {}
+ if tc_name:
+ fc_part = types.Part.from_function_call(
+ name=tc_name, args=tc_args
+ )
+ # Thinking-capable Gemini models require the original
+ # thought_signature to be echoed back on functionCall
+ # parts after a tool response, or the next request
+ # fails with INVALID_ARGUMENT.
+ sig = _decode_thought_signature(tc.get("thought_signature"))
+ if sig:
+ fc_part.thought_signature = sig
+ parts.append(fc_part)
+ if not parts:
+ parts.append(types.Part.from_text(text=" "))
+ gemini_messages.append(types.Content(role="model", parts=parts))
elif role == "tool":
# Handle tool response
- function_response = {
- "name": msg.get("name", ""),
- "response": content,
- }
+ response_payload = (
+ content if isinstance(content, dict) else {"result": content}
+ )
gemini_messages.append(
types.Content(
role="function",
parts=[
- types.Part.from_function_response(function_response) # type: ignore[misc,call-arg,arg-type]
+ types.Part.from_function_response(
+ name=msg.get("name")
+ or msg.get("tool_call_id")
+ or "",
+ response=response_payload,
+ )
],
)
)
else: # user
gemini_messages.append(
- types.Content(
- role="user", parts=[types.Part.from_text(text=content)]
- )
+ types.Content(role="user", parts=_parts_from_content(content))
)
# Convert tools to Gemini format
@@ -212,6 +311,13 @@ class GeminiClient(GenAIClient):
if tool_config:
config_params["tool_config"] = tool_config
+ # Ask thinking-capable models (Gemini 2.5+) to include their
+ # reasoning trace as separate `thought` parts so we can surface
+ # it on the reasoning channel. Older models ignore this field.
+ config_params["thinking_config"] = types.ThinkingConfig(
+ include_thoughts=True
+ )
+
# Merge runtime_options
if isinstance(self.genai_config.runtime_options, dict):
config_params.update(self.genai_config.runtime_options)
@@ -226,19 +332,24 @@ class GeminiClient(GenAIClient):
if not response or not response.candidates:
return {
"content": None,
+ "reasoning": None,
"tool_calls": None,
"finish_reason": "error",
}
candidate = response.candidates[0]
content = None
+ reasoning_parts: list[str] = []
tool_calls = None
- # Extract content and tool calls from response
+ # Extract content, reasoning, and tool calls from response
if candidate.content and candidate.content.parts:
for part in candidate.content.parts:
if part.text:
- content = part.text.strip()
+ if getattr(part, "thought", False):
+ reasoning_parts.append(part.text)
+ else:
+ content = part.text.strip()
elif part.function_call:
# Handle function call
if tool_calls is None:
@@ -258,9 +369,14 @@ class GeminiClient(GenAIClient):
"id": part.function_call.name or "",
"name": part.function_call.name or "",
"arguments": arguments,
+ "thought_signature": _encode_thought_signature(
+ getattr(part, "thought_signature", None)
+ ),
}
)
+ reasoning = "".join(reasoning_parts).strip() or None
+
# Determine finish reason
finish_reason = "error"
if hasattr(candidate, "finish_reason") and candidate.finish_reason:
@@ -286,6 +402,7 @@ class GeminiClient(GenAIClient):
return {
"content": content,
+ "reasoning": reasoning,
"tool_calls": tool_calls,
"finish_reason": finish_reason,
}
@@ -294,6 +411,7 @@ class GeminiClient(GenAIClient):
logger.warning("Gemini API error during chat_with_tools: %s", str(e))
return {
"content": None,
+ "reasoning": None,
"tool_calls": None,
"finish_reason": "error",
}
@@ -303,6 +421,7 @@ class GeminiClient(GenAIClient):
)
return {
"content": None,
+ "reasoning": None,
"tool_calls": None,
"finish_reason": "error",
}
@@ -310,13 +429,16 @@ class GeminiClient(GenAIClient):
async def chat_with_tools_stream(
self,
messages: list[dict[str, Any]],
- tools: Optional[list[dict[str, Any]]] = None,
- tool_choice: Optional[str] = "auto",
+ tools: list[dict[str, Any]] | None = None,
+ tool_choice: str | None = "auto",
+ enable_thinking: bool | None = None,
) -> AsyncGenerator[tuple[str, Any], None]:
"""
Stream chat with tools; yields content deltas then final message.
Implements streaming function calling/tool usage for Gemini models.
+ ``enable_thinking`` is accepted for interface parity; Gemini configures
+ thinking at the model level, so it is ignored here.
"""
try:
# Convert messages to Gemini format
@@ -343,30 +465,56 @@ class GeminiClient(GenAIClient):
)
)
elif role == "assistant":
- gemini_messages.append(
- types.Content(
- role="model", parts=[types.Part.from_text(text=content)]
- )
- )
+ parts: list[types.Part] = []
+ if content:
+ parts.append(types.Part.from_text(text=content))
+ for tc in msg.get("tool_calls") or []:
+ func = tc.get("function") or {}
+ tc_name = func.get("name") or ""
+ tc_args: Any = func.get("arguments")
+ if isinstance(tc_args, str):
+ try:
+ tc_args = json.loads(tc_args)
+ except (json.JSONDecodeError, TypeError):
+ tc_args = {}
+ if not isinstance(tc_args, dict):
+ tc_args = {}
+ if tc_name:
+ fc_part = types.Part.from_function_call(
+ name=tc_name, args=tc_args
+ )
+ # Thinking-capable Gemini models require the original
+ # thought_signature to be echoed back on functionCall
+ # parts after a tool response, or the next request
+ # fails with INVALID_ARGUMENT.
+ sig = _decode_thought_signature(tc.get("thought_signature"))
+ if sig:
+ fc_part.thought_signature = sig
+ parts.append(fc_part)
+ if not parts:
+ parts.append(types.Part.from_text(text=" "))
+ gemini_messages.append(types.Content(role="model", parts=parts))
elif role == "tool":
# Handle tool response
- function_response = {
- "name": msg.get("name", ""),
- "response": content,
- }
+ response_payload = (
+ content if isinstance(content, dict) else {"result": content}
+ )
gemini_messages.append(
types.Content(
role="function",
parts=[
- types.Part.from_function_response(function_response) # type: ignore[misc,call-arg,arg-type]
+ types.Part.from_function_response(
+ name=msg.get("name")
+ or msg.get("tool_call_id")
+ or "",
+ response=response_payload,
+ )
],
)
)
else: # user
gemini_messages.append(
- types.Content(
- role="user", parts=[types.Part.from_text(text=content)]
- )
+ types.Content(role="user", parts=_parts_from_content(content))
)
# Convert tools to Gemini format
@@ -419,14 +567,22 @@ class GeminiClient(GenAIClient):
if tool_config:
config_params["tool_config"] = tool_config
+ # Ask thinking-capable models to include their reasoning trace
+ # as separate `thought` parts (Gemini 2.5+; ignored elsewhere).
+ config_params["thinking_config"] = types.ThinkingConfig(
+ include_thoughts=True
+ )
+
# Merge runtime_options
if isinstance(self.genai_config.runtime_options, dict):
config_params.update(self.genai_config.runtime_options)
# Use streaming API
content_parts: list[str] = []
- tool_calls_by_index: dict[int, dict[str, Any]] = {}
+ reasoning_parts: list[str] = []
+ tool_calls_accum: list[dict[str, Any]] = []
finish_reason = "stop"
+ usage_stats: dict[str, Any] | None = None
stream = await self.provider.aio.models.generate_content_stream(
model=self.genai_config.model,
@@ -435,6 +591,12 @@ class GeminiClient(GenAIClient):
)
async for chunk in stream:
+ chunk_usage = getattr(chunk, "usage_metadata", None)
+ if chunk_usage is not None:
+ maybe_stats = _stats_from_gemini_usage(chunk_usage)
+ if maybe_stats is not None:
+ usage_stats = maybe_stats
+
if not chunk or not chunk.candidates:
continue
@@ -454,14 +616,22 @@ class GeminiClient(GenAIClient):
]:
finish_reason = "error"
- # Extract content and tool calls from chunk
+ # Extract content, reasoning, and tool calls from chunk
if candidate.content and candidate.content.parts:
for part in candidate.content.parts:
if part.text:
- content_parts.append(part.text)
- yield ("content_delta", part.text)
+ if getattr(part, "thought", False):
+ reasoning_parts.append(part.text)
+ yield ("reasoning_delta", part.text)
+ else:
+ content_parts.append(part.text)
+ yield ("content_delta", part.text)
elif part.function_call:
- # Handle function call
+ # Gemini streams complete function calls (not partial
+ # argument deltas), so each part is a distinct tool
+ # call. Append rather than accumulate by name — the
+ # latter concatenated parallel/repeated calls into one
+ # invalid arguments string (e.g. `{...}{...}`).
try:
arguments = (
dict(part.function_call.args)
@@ -471,60 +641,47 @@ class GeminiClient(GenAIClient):
except Exception:
arguments = {}
- # Store tool call
- tool_call_id = part.function_call.name or ""
- tool_call_name = part.function_call.name or ""
-
- # Check if we already have this tool call
- found_index = None
- for idx, tc in tool_calls_by_index.items():
- if tc["name"] == tool_call_name:
- found_index = idx
- break
-
- if found_index is None:
- found_index = len(tool_calls_by_index)
- tool_calls_by_index[found_index] = {
- "id": tool_call_id,
- "name": tool_call_name,
- "arguments": "",
+ tool_calls_accum.append(
+ {
+ "id": part.function_call.name or "",
+ "name": part.function_call.name or "",
+ "arguments": arguments,
+ "thought_signature": getattr(
+ part, "thought_signature", None
+ ),
}
-
- # Accumulate arguments
- if arguments:
- tool_calls_by_index[found_index]["arguments"] += (
- json.dumps(arguments)
- if isinstance(arguments, dict)
- else str(arguments)
- )
+ )
# Build final message
full_content = "".join(content_parts).strip() or None
+ full_reasoning = "".join(reasoning_parts).strip() or None
# Convert tool calls to list format
tool_calls_list = None
- if tool_calls_by_index:
- tool_calls_list = []
- for tc in tool_calls_by_index.values():
- try:
- # Try to parse accumulated arguments as JSON
- parsed_args = json.loads(tc["arguments"])
- except (json.JSONDecodeError, Exception):
- parsed_args = tc["arguments"]
-
- tool_calls_list.append(
- {
- "id": tc["id"],
- "name": tc["name"],
- "arguments": parsed_args,
- }
- )
+ if tool_calls_accum:
+ tool_calls_list = [
+ {
+ "id": tc["id"],
+ "name": tc["name"],
+ "arguments": tc["arguments"]
+ if isinstance(tc["arguments"], dict)
+ else {},
+ "thought_signature": _encode_thought_signature(
+ tc.get("thought_signature")
+ ),
+ }
+ for tc in tool_calls_accum
+ ]
finish_reason = "tool_calls"
+ if usage_stats is not None:
+ yield ("stats", usage_stats)
+
yield (
"message",
{
"content": full_content,
+ "reasoning": full_reasoning,
"tool_calls": tool_calls_list,
"finish_reason": finish_reason,
},
@@ -536,6 +693,7 @@ class GeminiClient(GenAIClient):
"message",
{
"content": None,
+ "reasoning": None,
"tool_calls": None,
"finish_reason": "error",
},
@@ -548,6 +706,7 @@ class GeminiClient(GenAIClient):
"message",
{
"content": None,
+ "reasoning": None,
"tool_calls": None,
"finish_reason": "error",
},
diff --git a/frigate/genai/llama_cpp.py b/frigate/genai/plugins/llama_cpp.py
similarity index 53%
rename from frigate/genai/llama_cpp.py
rename to frigate/genai/plugins/llama_cpp.py
index e5e9883b8f..af3ecc9b18 100644
--- a/frigate/genai/llama_cpp.py
+++ b/frigate/genai/plugins/llama_cpp.py
@@ -4,7 +4,8 @@ import base64
import io
import json
import logging
-from typing import Any, AsyncGenerator, Optional
+from collections.abc import AsyncGenerator
+from typing import Any, cast
import httpx
import numpy as np
@@ -18,6 +19,63 @@ from frigate.genai.utils import parse_tool_calls_from_message
logger = logging.getLogger(__name__)
+def _stats_from_llama_cpp_chunk(data: dict[str, Any]) -> dict[str, Any] | None:
+ """Build a stats dict from a llama.cpp streaming chunk.
+
+ Final-chunk `usage` carries authoritative token counts. Per-chunk
+ `timings` (enabled via timings_per_token) carries the running token
+ counts (prompt_n, predicted_n) and generation rate, so live updates
+ work mid-stream.
+ """
+ usage = data.get("usage") or {}
+ timings = data.get("timings") or {}
+ prompt_tokens = usage.get("prompt_tokens")
+ completion_tokens = usage.get("completion_tokens")
+ predicted_ms = timings.get("predicted_ms")
+ tps = timings.get("predicted_per_second")
+ stats: dict[str, Any] = {}
+
+ if not isinstance(prompt_tokens, int):
+ prompt_n = timings.get("prompt_n")
+
+ if isinstance(prompt_n, int):
+ prompt_tokens = prompt_n
+
+ if not isinstance(completion_tokens, int):
+ predicted_n = timings.get("predicted_n")
+
+ if isinstance(predicted_n, int):
+ completion_tokens = predicted_n
+
+ if not isinstance(prompt_tokens, int) and not isinstance(completion_tokens, int):
+ return None
+
+ if isinstance(prompt_tokens, int):
+ stats["prompt_tokens"] = prompt_tokens
+
+ if isinstance(completion_tokens, int):
+ stats["completion_tokens"] = completion_tokens
+
+ if isinstance(predicted_ms, (int, float)) and predicted_ms > 0:
+ stats["completion_duration_ms"] = float(predicted_ms)
+
+ if isinstance(tps, (int, float)) and tps > 0:
+ stats["tokens_per_second"] = float(tps)
+
+ return stats or None
+
+
+def _parse_launch_arg(args: list[str], flag: str) -> str | None:
+ """Return the value following `flag` in a positional argv list, or None."""
+ try:
+ idx = args.index(flag)
+ except ValueError:
+ return None
+ if idx + 1 >= len(args):
+ return None
+ return args[idx + 1]
+
+
def _to_jpeg(img_bytes: bytes) -> bytes | None:
"""Convert image bytes to JPEG. llama.cpp/STB does not support WebP."""
try:
@@ -42,6 +100,52 @@ class LlamaCppClient(GenAIClient):
_supports_vision: bool
_supports_audio: bool
_supports_tools: bool
+ _supports_reasoning: bool
+ _image_token_cache: dict[tuple[int, int], int]
+ _text_baseline_tokens: int | None
+ _media_marker: str
+
+ @property
+ def supports_embeddings(self) -> bool:
+ """llama.cpp exposes an /embeddings endpoint for any loaded model."""
+ return True
+
+ def _auth_headers(self) -> dict | None:
+ """Bearer auth header when an API key is configured, else None."""
+ if self.genai_config.api_key:
+ return {"Authorization": "Bearer " + self.genai_config.api_key}
+
+ return None
+
+ def _get(self, url: str, **kwargs: Any) -> requests.Response:
+ """GET with the configured auth headers injected."""
+ return requests.get(url, headers=self._auth_headers(), **kwargs)
+
+ def _post(self, url: str, **kwargs: Any) -> requests.Response:
+ """POST with the configured auth headers injected."""
+ return requests.post(url, headers=self._auth_headers(), **kwargs)
+
+ def _fetch_llama_props(self, base_url: str, model: str) -> dict[str, Any]:
+ """Fetch /props from a llama.cpp server, with llama-swap fallback.
+
+ Raises the underlying RequestException if both endpoints fail; callers
+ decide how to surface the failure.
+ """
+ try:
+ response = self._get(
+ f"{base_url}/props",
+ params={"model": model},
+ timeout=10,
+ )
+ response.raise_for_status()
+ return cast(dict[str, Any], response.json())
+ except Exception:
+ response = self._get(
+ f"{base_url}/upstream/{model}/props",
+ timeout=10,
+ )
+ response.raise_for_status()
+ return cast(dict[str, Any], response.json())
def _init_provider(self) -> str | None:
"""Initialize the client and query model metadata from the server."""
@@ -52,6 +156,10 @@ class LlamaCppClient(GenAIClient):
self._supports_vision = False
self._supports_audio = False
self._supports_tools = False
+ self._supports_reasoning = False
+ self._image_token_cache = {}
+ self._text_baseline_tokens = None
+ self._media_marker = "<__media__>"
base_url = (
self.genai_config.base_url.rstrip("/")
@@ -61,28 +169,80 @@ class LlamaCppClient(GenAIClient):
if base_url is None:
return None
+ else:
+ base_url = base_url.replace("/v1", "") # Strip /v1 if included in base_url
+
+ if not self.validate_model:
+ # Probe path
+ return base_url
configured_model = self.genai_config.model
+ info = self._get_model_info(base_url, configured_model)
- # Query /v1/models to validate the configured model exists
+ if info is None:
+ return None
+
+ self._context_size = info["context_size"]
+ self._supports_vision = info["supports_vision"]
+ self._supports_audio = info["supports_audio"]
+ self._supports_tools = info["supports_tools"]
+ self._supports_reasoning = info["supports_reasoning"]
+ self._media_marker = info["media_marker"]
+
+ logger.info(
+ "llama.cpp model '%s' initialized — context: %s, vision: %s, audio: %s, tools: %s, reasoning: %s",
+ configured_model,
+ self._context_size or "unknown",
+ self._supports_vision,
+ self._supports_audio,
+ self._supports_tools,
+ self._supports_reasoning,
+ )
+
+ return base_url
+
+ def _get_model_info(
+ self, base_url: str, configured_model: str
+ ) -> dict[str, Any] | None:
+ """Resolve model metadata from /v1/models with /props fallback.
+
+ Returns a dict of capability fields, or None if the server's model
+ registry was reachable and reported the configured model as missing.
+ A reachable-but-unparseable /v1/models is treated as soft-pass and
+ falls through to /props, matching prior behavior.
+
+ After ggml-org/llama.cpp#22952, /v1/models exposes per-model
+ `architecture.input_modalities` (text/image/audio) — the primary
+ source. When proxied through llama-swap, the same entry carries
+ `status.args` (server launch argv) and, for the loaded model,
+ `meta.n_ctx`. /props remains the only source for `media_marker`,
+ which the server randomizes per startup unless LLAMA_MEDIA_MARKER
+ is set.
+ """
+ info: dict[str, Any] = {
+ "context_size": None,
+ "supports_vision": False,
+ "supports_audio": False,
+ "supports_tools": False,
+ "supports_reasoning": False,
+ "media_marker": "<__media__>",
+ }
+
+ model_entry: dict[str, Any] | None = None
try:
- response = requests.get(
- f"{base_url}/v1/models",
- timeout=10,
- )
+ response = self._get(f"{base_url}/v1/models", timeout=10)
response.raise_for_status()
models_data = response.json()
- model_found = False
for model in models_data.get("data", []):
model_ids = {model.get("id")}
for alias in model.get("aliases", []):
model_ids.add(alias)
if configured_model in model_ids:
- model_found = True
+ model_entry = model
break
- if not model_found:
+ if model_entry is None:
available = []
for m in models_data.get("data", []):
available.append(m.get("id", "unknown"))
@@ -101,65 +261,79 @@ class LlamaCppClient(GenAIClient):
e,
)
- # Query /props for context size, modalities, and tool support.
- # The standard /props?model= endpoint works with llama-server.
- # If it fails, try the llama-swap per-model passthrough endpoint which
- # returns props for a specific model without requiring it to be loaded.
- try:
- try:
- response = requests.get(
- f"{base_url}/props",
- params={"model": configured_model},
- timeout=10,
- )
- response.raise_for_status()
- props = response.json()
- except Exception:
- response = requests.get(
- f"{base_url}/upstream/{configured_model}/props",
- timeout=10,
- )
- response.raise_for_status()
- props = response.json()
+ if model_entry is not None:
+ architecture = model_entry.get("architecture") or {}
+ input_modalities = architecture.get("input_modalities") or []
+
+ if isinstance(input_modalities, list):
+ info["supports_vision"] = "image" in input_modalities
+ info["supports_audio"] = "audio" in input_modalities
+
+ status = model_entry.get("status") or {}
+ launch_args = status.get("args") if isinstance(status, dict) else None
+ if not isinstance(launch_args, list):
+ launch_args = []
+
+ meta = model_entry.get("meta") if isinstance(model_entry, dict) else None
+ n_ctx = meta.get("n_ctx") if isinstance(meta, dict) else None
+
+ if not n_ctx:
+ n_ctx = _parse_launch_arg(launch_args, "--ctx-size")
- # Context size from server runtime config
- default_settings = props.get("default_generation_settings", {})
- n_ctx = default_settings.get("n_ctx")
if n_ctx:
- self._context_size = int(n_ctx)
+ try:
+ info["context_size"] = int(n_ctx)
+ except (TypeError, ValueError):
+ pass
- # Modalities (vision, audio)
- modalities = props.get("modalities", {})
- self._supports_vision = modalities.get("vision", False)
- self._supports_audio = modalities.get("audio", False)
+ # Tool calling on llama-server requires --jinja.
+ if "--jinja" in launch_args:
+ info["supports_tools"] = True
- # Tool support from chat template capabilities
- chat_caps = props.get("chat_template_caps", {})
- self._supports_tools = chat_caps.get("supports_tools", False)
+ try:
+ props = self._fetch_llama_props(base_url, configured_model)
- logger.info(
- "llama.cpp model '%s' initialized — context: %s, vision: %s, audio: %s, tools: %s",
- configured_model,
- self._context_size or "unknown",
- self._supports_vision,
- self._supports_audio,
- self._supports_tools,
- )
+ if info["context_size"] is None:
+ default_settings = props.get("default_generation_settings", {})
+ n_ctx = default_settings.get("n_ctx")
+ if n_ctx:
+ info["context_size"] = int(n_ctx)
+
+ if not (info["supports_vision"] or info["supports_audio"]):
+ modalities = props.get("modalities", {})
+ info["supports_vision"] = bool(modalities.get("vision", False))
+ info["supports_audio"] = bool(modalities.get("audio", False))
+
+ chat_caps = props.get("chat_template_caps") or {}
+
+ if not info["supports_tools"]:
+ info["supports_tools"] = bool(chat_caps.get("supports_tools", False))
+
+ # llama.cpp does not advertise per-template reasoning support, so
+ # detect it by looking for the `enable_thinking` toggle variable
+ # in the Jinja chat template itself.
+ chat_template = props.get("chat_template") or ""
+ info["supports_reasoning"] = "enable_thinking" in chat_template
+
+ media_marker = props.get("media_marker")
+ if isinstance(media_marker, str) and media_marker:
+ info["media_marker"] = media_marker
except Exception as e:
logger.warning(
"Failed to query llama.cpp /props endpoint: %s. "
- "Using defaults for context size and capabilities.",
+ "Image embeddings may fail if the server randomized its media marker.",
e,
)
- return base_url
+ return info
def _send(
self,
prompt: str,
images: list[bytes],
- response_format: Optional[dict] = None,
- ) -> Optional[str]:
+ response_format: dict | None = None,
+ enable_thinking: bool = False,
+ ) -> str | None:
"""Submit a request to llama.cpp server."""
if self.provider is None:
logger.warning(
@@ -186,7 +360,7 @@ class LlamaCppClient(GenAIClient):
)
# Build request payload with llama.cpp native options
- payload = {
+ payload: dict[str, Any] = {
"model": self.genai_config.model,
"messages": [
{
@@ -200,7 +374,10 @@ class LlamaCppClient(GenAIClient):
if response_format:
payload["response_format"] = response_format
- response = requests.post(
+ if self.supports_toggleable_thinking:
+ payload["chat_template_kwargs"] = {"enable_thinking": enable_thinking}
+
+ response = self._post(
f"{self.provider}/v1/chat/completions",
json=payload,
timeout=self.timeout,
@@ -236,6 +413,10 @@ class LlamaCppClient(GenAIClient):
"""Whether the loaded model supports tool/function calling."""
return self._supports_tools
+ @property
+ def supports_toggleable_thinking(self) -> bool:
+ return self._supports_reasoning
+
def list_models(self) -> list[str]:
"""Return available model IDs from the llama.cpp server."""
base_url = self.provider or (
@@ -246,7 +427,7 @@ class LlamaCppClient(GenAIClient):
if base_url is None:
return []
try:
- response = requests.get(f"{base_url}/v1/models", timeout=10)
+ response = self._get(f"{base_url}/v1/models", timeout=10)
response.raise_for_status()
models = []
for m in response.json().get("data", []):
@@ -272,12 +453,98 @@ class LlamaCppClient(GenAIClient):
return self._context_size
return 4096
+ def estimate_image_tokens(self, width: int, height: int) -> float:
+ """Probe the llama.cpp server to learn the model's image-token cost at the
+ requested dimensions.
+
+ llama.cpp's image tokenization is a deterministic function of dimensions and
+ the loaded mmproj, so the result is cached per (width, height) for the
+ lifetime of the process. Falls back to the base pixel heuristic if the
+ server is unreachable or the response is malformed.
+ """
+ if self.provider is None:
+ return super().estimate_image_tokens(width, height)
+
+ cached = self._image_token_cache.get((width, height))
+
+ if cached is not None:
+ return cached
+
+ try:
+ baseline = self._probe_baseline_tokens()
+ with_image = self._probe_image_prompt_tokens(width, height)
+ tokens = max(1, with_image - baseline)
+ except Exception as e:
+ logger.debug(
+ "llama.cpp image-token probe failed for %dx%d (%s); using heuristic",
+ width,
+ height,
+ e,
+ )
+ return super().estimate_image_tokens(width, height)
+
+ self._image_token_cache[(width, height)] = tokens
+ logger.debug(
+ "llama.cpp model '%s' uses ~%d tokens for %dx%d images",
+ self.genai_config.model,
+ tokens,
+ width,
+ height,
+ )
+ return tokens
+
+ def _probe_baseline_tokens(self) -> int:
+ """Return prompt_tokens for a minimal text-only request. Cached after first call."""
+ if self._text_baseline_tokens is not None:
+ return self._text_baseline_tokens
+
+ self._text_baseline_tokens = self._probe_prompt_tokens(
+ [{"type": "text", "text": "."}]
+ )
+ return self._text_baseline_tokens
+
+ def _probe_image_prompt_tokens(self, width: int, height: int) -> int:
+ """Return prompt_tokens for a single synthetic image plus minimal text."""
+ img = Image.new("RGB", (width, height), (128, 128, 128))
+ buf = io.BytesIO()
+ img.save(buf, format="JPEG", quality=60)
+ encoded = base64.b64encode(buf.getvalue()).decode("utf-8")
+ return self._probe_prompt_tokens(
+ [
+ {"type": "text", "text": "."},
+ {
+ "type": "image_url",
+ "image_url": {"url": f"data:image/jpeg;base64,{encoded}"},
+ },
+ ]
+ )
+
+ def _probe_prompt_tokens(self, content: list[dict[str, Any]]) -> int:
+ """POST a 1-token chat completion and return reported prompt_tokens.
+
+ Uses a generous timeout to absorb a cold model load on the first probe
+ when the server lazily loads models on demand (e.g. llama-swap).
+ """
+ payload = {
+ "model": self.genai_config.model,
+ "messages": [{"role": "user", "content": content}],
+ "max_tokens": 1,
+ }
+ response = self._post(
+ f"{self.provider}/v1/chat/completions",
+ json=payload,
+ timeout=60,
+ )
+ response.raise_for_status()
+ return int(response.json()["usage"]["prompt_tokens"])
+
def _build_payload(
self,
messages: list[dict[str, Any]],
- tools: Optional[list[dict[str, Any]]],
- tool_choice: Optional[str],
+ tools: list[dict[str, Any]] | None,
+ tool_choice: str | None,
stream: bool = False,
+ enable_thinking: bool | None = None,
) -> dict[str, Any]:
"""Build request payload for chat completions (sync or stream)."""
openai_tool_choice = None
@@ -293,29 +560,47 @@ class LlamaCppClient(GenAIClient):
"messages": messages,
"model": self.genai_config.model,
}
+
if stream:
payload["stream"] = True
+ payload["stream_options"] = {"include_usage": True}
+ payload["timings_per_token"] = True
+
if tools:
payload["tools"] = tools
+
if openai_tool_choice is not None:
payload["tool_choice"] = openai_tool_choice
+
+ if enable_thinking is not None and self._supports_reasoning:
+ payload["chat_template_kwargs"] = {"enable_thinking": enable_thinking}
+
provider_opts = {
k: v for k, v in self.provider_options.items() if k != "context_size"
}
payload.update(provider_opts)
+ payload.update(self.genai_config.runtime_options)
return payload
def _message_from_choice(self, choice: dict[str, Any]) -> dict[str, Any]:
- """Parse OpenAI-style choice into {content, tool_calls, finish_reason}."""
+ """Parse OpenAI-style choice into {content, reasoning, tool_calls, finish_reason}.
+
+ llama.cpp's `--reasoning-format` puts the trace in
+ `message.reasoning_content` (preferred) or `message.thinking`; both
+ keys are accepted so different builds work without configuration.
+ """
message = choice.get("message", {})
content = message.get("content")
content = content.strip() if content else None
+ reasoning = message.get("reasoning_content") or message.get("thinking")
+ reasoning = reasoning.strip() if reasoning else None
tool_calls = parse_tool_calls_from_message(message)
finish_reason = choice.get("finish_reason") or (
"tool_calls" if tool_calls else "stop" if content else "error"
)
return {
"content": content,
+ "reasoning": reasoning,
"tool_calls": tool_calls,
"finish_reason": finish_reason,
}
@@ -323,7 +608,7 @@ class LlamaCppClient(GenAIClient):
@staticmethod
def _streamed_tool_calls_to_list(
tool_calls_by_index: dict[int, dict[str, Any]],
- ) -> Optional[list[dict[str, Any]]]:
+ ) -> list[dict[str, Any]] | None:
"""Convert streamed tool_calls index map to list of {id, name, arguments}."""
if not tool_calls_by_index:
return None
@@ -344,6 +629,31 @@ class LlamaCppClient(GenAIClient):
)
return result if result else None
+ def _refresh_media_marker(self) -> bool:
+ """Re-fetch /props and update the cached media marker if it changed.
+
+ The server randomizes the marker per startup (unless LLAMA_MEDIA_MARKER
+ is set), so a stale marker indicates a restart. Returns True iff the
+ marker was updated to a new value — used to gate a one-shot retry of
+ a failed embeddings request.
+ """
+ if self.provider is None:
+ return False
+ try:
+ props = self._fetch_llama_props(self.provider, self.genai_config.model)
+ except Exception as e:
+ logger.warning("Failed to refresh llama.cpp media marker: %s", e)
+ return False
+
+ marker = props.get("media_marker")
+
+ if not isinstance(marker, str) or not marker or marker == self._media_marker:
+ return False
+
+ logger.info("llama.cpp media marker changed (server restart); refreshed")
+ self._media_marker = marker
+ return True
+
def embed(
self,
texts: list[str] | None = None,
@@ -368,29 +678,46 @@ class LlamaCppClient(GenAIClient):
EMBEDDING_DIM = 768
- content = []
- for text in texts:
- content.append({"prompt_string": text})
+ encoded_images: list[str] = []
for img in images:
# llama.cpp uses STB which does not support WebP; convert to JPEG
jpeg_bytes = _to_jpeg(img)
to_encode = jpeg_bytes if jpeg_bytes is not None else img
- encoded = base64.b64encode(to_encode).decode("utf-8")
- # prompt_string must contain <__media__> placeholder for image tokenization
- content.append(
- {
- "prompt_string": "<__media__>\n",
- "multimodal_data": [encoded], # type: ignore[dict-item]
- }
+ encoded_images.append(base64.b64encode(to_encode).decode("utf-8"))
+
+ def build_content() -> list[dict[str, Any]]:
+ # prompt_string must contain the server's media marker placeholder
+ # for each image. The marker is randomized per server startup.
+ content: list[dict[str, Any]] = []
+ for text in texts:
+ content.append({"prompt_string": text})
+ for encoded in encoded_images:
+ content.append(
+ {
+ "prompt_string": f"{self._media_marker}\n",
+ "multimodal_data": [encoded],
+ }
+ )
+ return content
+
+ def post_embeddings() -> requests.Response:
+ return self._post(
+ f"{self.provider}/embeddings",
+ json={"model": self.genai_config.model, "content": build_content()},
+ timeout=self.timeout,
)
try:
- response = requests.post(
- f"{self.provider}/embeddings",
- json={"model": self.genai_config.model, "content": content},
- timeout=self.timeout,
- )
- response.raise_for_status()
+ try:
+ response = post_embeddings()
+ response.raise_for_status()
+ except requests.exceptions.RequestException:
+ # The server may have restarted with a new media marker.
+ # Refresh from /props; only retry if the marker actually changed.
+ if not encoded_images or not self._refresh_media_marker():
+ raise
+ response = post_embeddings()
+ response.raise_for_status()
result = response.json()
items = result.get("data", result) if isinstance(result, dict) else result
@@ -451,8 +778,9 @@ class LlamaCppClient(GenAIClient):
def chat_with_tools(
self,
messages: list[dict[str, Any]],
- tools: Optional[list[dict[str, Any]]] = None,
- tool_choice: Optional[str] = "auto",
+ tools: list[dict[str, Any]] | None = None,
+ tool_choice: str | None = "auto",
+ enable_thinking: bool | None = None,
) -> dict[str, Any]:
"""
Send chat messages to llama.cpp server with optional tool definitions.
@@ -470,8 +798,14 @@ class LlamaCppClient(GenAIClient):
"finish_reason": "error",
}
try:
- payload = self._build_payload(messages, tools, tool_choice, stream=False)
- response = requests.post(
+ payload = self._build_payload(
+ messages,
+ tools,
+ tool_choice,
+ stream=False,
+ enable_thinking=enable_thinking,
+ )
+ response = self._post(
f"{self.provider}/v1/chat/completions",
json=payload,
timeout=self.timeout,
@@ -516,8 +850,9 @@ class LlamaCppClient(GenAIClient):
async def chat_with_tools_stream(
self,
messages: list[dict[str, Any]],
- tools: Optional[list[dict[str, Any]]] = None,
- tool_choice: Optional[str] = "auto",
+ tools: list[dict[str, Any]] | None = None,
+ tool_choice: str | None = "auto",
+ enable_thinking: bool | None = None,
) -> AsyncGenerator[tuple[str, Any], None]:
"""Stream chat with tools via OpenAI-compatible streaming API."""
if self.provider is None:
@@ -534,8 +869,15 @@ class LlamaCppClient(GenAIClient):
)
return
try:
- payload = self._build_payload(messages, tools, tool_choice, stream=True)
+ payload = self._build_payload(
+ messages,
+ tools,
+ tool_choice,
+ stream=True,
+ enable_thinking=enable_thinking,
+ )
content_parts: list[str] = []
+ reasoning_parts: list[str] = []
tool_calls_by_index: dict[int, dict[str, Any]] = {}
finish_reason = "stop"
@@ -544,6 +886,7 @@ class LlamaCppClient(GenAIClient):
"POST",
f"{self.provider}/v1/chat/completions",
json=payload,
+ headers=self._auth_headers(),
) as response:
response.raise_for_status()
async for line in response.aiter_lines():
@@ -556,12 +899,24 @@ class LlamaCppClient(GenAIClient):
data = json.loads(data_str)
except json.JSONDecodeError:
continue
+ maybe_stats = _stats_from_llama_cpp_chunk(data)
+ if maybe_stats is not None:
+ yield ("stats", maybe_stats)
choices = data.get("choices") or []
if not choices:
continue
delta = choices[0].get("delta", {})
if choices[0].get("finish_reason"):
finish_reason = choices[0]["finish_reason"]
+ # llama.cpp emits separated thinking under
+ # reasoning_content (preferred) or thinking before any
+ # content tokens arrive
+ reasoning_delta = delta.get("reasoning_content") or delta.get(
+ "thinking"
+ )
+ if reasoning_delta:
+ reasoning_parts.append(reasoning_delta)
+ yield ("reasoning_delta", reasoning_delta)
if delta.get("content"):
content_parts.append(delta["content"])
yield ("content_delta", delta["content"])
@@ -587,6 +942,7 @@ class LlamaCppClient(GenAIClient):
)
full_content = "".join(content_parts).strip() or None
+ full_reasoning = "".join(reasoning_parts).strip() or None
tool_calls_list = self._streamed_tool_calls_to_list(tool_calls_by_index)
if tool_calls_list:
finish_reason = "tool_calls"
@@ -594,6 +950,7 @@ class LlamaCppClient(GenAIClient):
"message",
{
"content": full_content,
+ "reasoning": full_reasoning,
"tool_calls": tool_calls_list,
"finish_reason": finish_reason,
},
diff --git a/frigate/genai/ollama.py b/frigate/genai/plugins/ollama.py
similarity index 63%
rename from frigate/genai/ollama.py
rename to frigate/genai/plugins/ollama.py
index 7524d54e38..f323b0faa7 100644
--- a/frigate/genai/ollama.py
+++ b/frigate/genai/plugins/ollama.py
@@ -1,8 +1,11 @@
"""Ollama Provider for Frigate AI."""
+import base64
+import binascii
import json
import logging
-from typing import Any, AsyncGenerator, Optional
+from collections.abc import AsyncGenerator
+from typing import Any
from httpx import RemoteProtocolError, TimeoutException
from ollama import AsyncClient as OllamaAsyncClient
@@ -16,6 +19,72 @@ from frigate.genai.utils import parse_tool_calls_from_message
logger = logging.getLogger(__name__)
+def _extract_ollama_stats(response: Any) -> dict[str, Any] | None:
+ """Build a stats dict from Ollama's response metadata.
+
+ Ollama reports eval_count/eval_duration (generation) and
+ prompt_eval_count (context size). Durations are nanoseconds.
+ """
+ if not response:
+ return None
+ if hasattr(response, "get"):
+ getter = response.get
+ else:
+ getter = lambda key: getattr(response, key, None) # noqa: E731
+
+ eval_count = getter("eval_count")
+ eval_duration_ns = getter("eval_duration")
+ prompt_eval_count = getter("prompt_eval_count")
+ if eval_count is None and prompt_eval_count is None:
+ return None
+
+ stats: dict[str, Any] = {}
+ if isinstance(prompt_eval_count, int):
+ stats["prompt_tokens"] = prompt_eval_count
+ if isinstance(eval_count, int):
+ stats["completion_tokens"] = eval_count
+ if isinstance(eval_duration_ns, int) and eval_duration_ns > 0:
+ stats["completion_duration_ms"] = eval_duration_ns / 1_000_000
+ if isinstance(eval_count, int) and eval_count > 0:
+ stats["tokens_per_second"] = eval_count / (eval_duration_ns / 1_000_000_000)
+ return stats or None
+
+
+def _normalize_multimodal_content(
+ content: Any,
+) -> tuple[str | None, list[bytes] | None]:
+ """Convert OpenAI-style multimodal content to Ollama's (text, images) shape.
+
+ The chat API constructs user messages with content as a list of
+ ``{"type": "text"}`` and ``{"type": "image_url"}`` parts when a tool
+ returns a live frame. Ollama's SDK requires content to be a string and
+ images to be passed in a separate field, so we extract each.
+ """
+ if not isinstance(content, list):
+ return content, None
+
+ text_parts: list[str] = []
+ images: list[bytes] = []
+ for part in content:
+ if not isinstance(part, dict):
+ continue
+ part_type = part.get("type")
+ if part_type == "text":
+ text = part.get("text")
+ if text:
+ text_parts.append(str(text))
+ elif part_type == "image_url":
+ url = (part.get("image_url") or {}).get("url", "")
+ if isinstance(url, str) and url.startswith("data:"):
+ try:
+ encoded = url.split(",", 1)[1]
+ images.append(base64.b64decode(encoded, validate=True))
+ except (ValueError, IndexError, binascii.Error) as e:
+ logger.debug("Failed to decode multimodal image url: %s", e)
+
+ return ("\n".join(text_parts) if text_parts else None), (images or None)
+
+
@register_genai_provider(GenAIProviderEnum.ollama)
class OllamaClient(GenAIClient):
"""Generative AI client for Frigate using Ollama."""
@@ -30,6 +99,28 @@ class OllamaClient(GenAIClient):
provider: ApiClient | None
provider_options: dict[str, Any]
+ _supports_thinking_cache: bool | None = None
+
+ @property
+ def supports_toggleable_thinking(self) -> bool:
+ if self._supports_thinking_cache is not None:
+ return self._supports_thinking_cache
+ if self.provider is None:
+ return False
+ try:
+ response = self.provider.show(self.genai_config.model)
+ capabilities = response.get("capabilities") or []
+ self._supports_thinking_cache = "thinking" in capabilities
+ except Exception as e:
+ logger.debug("Failed to query Ollama model capabilities: %s", e)
+ self._supports_thinking_cache = False
+ return self._supports_thinking_cache
+
+ def _auth_headers(self) -> dict | None:
+ if self.genai_config.api_key:
+ return {"Authorization": "Bearer " + self.genai_config.api_key}
+
+ return None
def _init_provider(self) -> ApiClient | None:
"""Initialize the client."""
@@ -39,7 +130,14 @@ class OllamaClient(GenAIClient):
}
try:
- client = ApiClient(host=self.genai_config.base_url, timeout=self.timeout)
+ client = ApiClient(
+ host=self.genai_config.base_url,
+ timeout=self.timeout,
+ headers=self._auth_headers(),
+ )
+ if not self.validate_model:
+ # Probe path
+ return client
# ensure the model is available locally
response = client.show(self.genai_config.model)
if response.get("error"):
@@ -96,8 +194,9 @@ class OllamaClient(GenAIClient):
self,
prompt: str,
images: list[bytes],
- response_format: Optional[dict] = None,
- ) -> Optional[str]:
+ response_format: dict | None = None,
+ enable_thinking: bool = False,
+ ) -> str | None:
"""Submit a request to Ollama"""
if self.provider is None:
logger.warning(
@@ -113,6 +212,17 @@ class OllamaClient(GenAIClient):
schema = response_format.get("json_schema", {}).get("schema")
if schema:
ollama_options["format"] = self._clean_schema_for_ollama(schema)
+ if self.supports_toggleable_thinking:
+ ollama_options["think"] = enable_thinking
+ logger.debug(
+ "Ollama generate request: model=%s, prompt_len=%s, image_count=%s, "
+ "has_format=%s, options=%s",
+ self.genai_config.model,
+ len(prompt),
+ len(images) if images else 0,
+ "format" in ollama_options,
+ {k: v for k, v in ollama_options.items() if k != "format"},
+ )
result = self.provider.generate(
self.genai_config.model,
prompt,
@@ -120,9 +230,24 @@ class OllamaClient(GenAIClient):
**ollama_options,
)
logger.debug(
- f"Ollama tokens used: eval_count={result.get('eval_count')}, prompt_eval_count={result.get('prompt_eval_count')}"
+ "Ollama generate response: done=%s, done_reason=%s, eval_count=%s, "
+ "prompt_eval_count=%s, response_len=%s",
+ result.get("done"),
+ result.get("done_reason"),
+ result.get("eval_count"),
+ result.get("prompt_eval_count"),
+ len(result.get("response", "") or ""),
)
- return str(result["response"]).strip()
+ response_text = str(result["response"]).strip()
+ if not response_text:
+ logger.warning(
+ "Ollama returned a blank response for model %s (done_reason=%s, "
+ "eval_count=%s). Check model output, ensure thinking is disabled.",
+ self.genai_config.model,
+ result.get("done_reason"),
+ result.get("eval_count"),
+ )
+ return response_text
except (
TimeoutException,
ResponseError,
@@ -142,7 +267,9 @@ class OllamaClient(GenAIClient):
return []
try:
client = ApiClient(
- host=self.genai_config.base_url, timeout=self.timeout
+ host=self.genai_config.base_url,
+ timeout=self.timeout,
+ headers=self._auth_headers(),
)
except Exception:
return []
@@ -164,17 +291,21 @@ class OllamaClient(GenAIClient):
def _build_request_params(
self,
messages: list[dict[str, Any]],
- tools: Optional[list[dict[str, Any]]],
- tool_choice: Optional[str],
+ tools: list[dict[str, Any]] | None,
+ tool_choice: str | None,
stream: bool = False,
+ enable_thinking: bool | None = None,
) -> dict[str, Any]:
"""Build request_messages and params for chat (sync or stream)."""
request_messages = []
for msg in messages:
- msg_dict = {
+ content, images = _normalize_multimodal_content(msg.get("content", ""))
+ msg_dict: dict[str, Any] = {
"role": msg.get("role"),
- "content": msg.get("content", ""),
+ "content": content if content is not None else "",
}
+ if images:
+ msg_dict["images"] = images
if msg.get("tool_call_id"):
msg_dict["tool_call_id"] = msg["tool_call_id"]
if msg.get("name"):
@@ -202,11 +333,14 @@ class OllamaClient(GenAIClient):
"model": self.genai_config.model,
"messages": request_messages,
**self.provider_options,
+ **self.genai_config.runtime_options,
}
if stream:
request_params["stream"] = True
if tools:
request_params["tools"] = tools
+ if enable_thinking is not None and self.supports_toggleable_thinking:
+ request_params["think"] = enable_thinking
return request_params
def _message_from_response(self, response: dict[str, Any]) -> dict[str, Any]:
@@ -229,6 +363,9 @@ class OllamaClient(GenAIClient):
response.get("done"),
)
content = message.get("content", "").strip() if message.get("content") else None
+ reasoning = (
+ message.get("thinking", "").strip() if message.get("thinking") else None
+ )
tool_calls = parse_tool_calls_from_message(message)
finish_reason = "error"
if response.get("done"):
@@ -241,6 +378,7 @@ class OllamaClient(GenAIClient):
finish_reason = "stop"
return {
"content": content,
+ "reasoning": reasoning,
"tool_calls": tool_calls,
"finish_reason": finish_reason,
}
@@ -248,8 +386,9 @@ class OllamaClient(GenAIClient):
def chat_with_tools(
self,
messages: list[dict[str, Any]],
- tools: Optional[list[dict[str, Any]]] = None,
- tool_choice: Optional[str] = "auto",
+ tools: list[dict[str, Any]] | None = None,
+ tool_choice: str | None = "auto",
+ enable_thinking: bool | None = None,
) -> dict[str, Any]:
if self.provider is None:
logger.warning(
@@ -262,7 +401,11 @@ class OllamaClient(GenAIClient):
}
try:
request_params = self._build_request_params(
- messages, tools, tool_choice, stream=False
+ messages,
+ tools,
+ tool_choice,
+ stream=False,
+ enable_thinking=enable_thinking,
)
response = self.provider.chat(**request_params)
return self._message_from_response(response)
@@ -284,8 +427,9 @@ class OllamaClient(GenAIClient):
async def chat_with_tools_stream(
self,
messages: list[dict[str, Any]],
- tools: Optional[list[dict[str, Any]]] = None,
- tool_choice: Optional[str] = "auto",
+ tools: list[dict[str, Any]] | None = None,
+ tool_choice: str | None = "auto",
+ enable_thinking: bool | None = None,
) -> AsyncGenerator[tuple[str, Any], None]:
"""Stream chat with tools; yields content deltas then final message.
@@ -315,47 +459,76 @@ class OllamaClient(GenAIClient):
"Ollama: tools provided, using non-streaming call for tool support"
)
request_params = self._build_request_params(
- messages, tools, tool_choice, stream=False
+ messages,
+ tools,
+ tool_choice,
+ stream=False,
+ enable_thinking=enable_thinking,
)
async_client = OllamaAsyncClient(
host=self.genai_config.base_url,
timeout=self.timeout,
+ headers=self._auth_headers(),
)
response = await async_client.chat(**request_params)
result = self._message_from_response(response)
+ reasoning = result.get("reasoning")
+ if reasoning:
+ yield ("reasoning_delta", reasoning)
content = result.get("content")
if content:
yield ("content_delta", content)
+ stats = _extract_ollama_stats(response)
+ if stats is not None:
+ yield ("stats", stats)
yield ("message", result)
return
request_params = self._build_request_params(
- messages, tools, tool_choice, stream=True
+ messages,
+ tools,
+ tool_choice,
+ stream=True,
+ enable_thinking=enable_thinking,
)
async_client = OllamaAsyncClient(
host=self.genai_config.base_url,
timeout=self.timeout,
+ headers=self._auth_headers(),
)
content_parts: list[str] = []
+ reasoning_parts: list[str] = []
final_message: dict[str, Any] | None = None
+ final_chunk: Any = None
stream = await async_client.chat(**request_params)
async for chunk in stream:
if not chunk or "message" not in chunk:
continue
msg = chunk.get("message", {})
+ reasoning_delta = msg.get("thinking") or ""
+ if reasoning_delta:
+ reasoning_parts.append(reasoning_delta)
+ yield ("reasoning_delta", reasoning_delta)
delta = msg.get("content") or ""
if delta:
content_parts.append(delta)
yield ("content_delta", delta)
if chunk.get("done"):
+ final_chunk = chunk
full_content = "".join(content_parts).strip() or None
+ full_reasoning = "".join(reasoning_parts).strip() or None
final_message = {
"content": full_content,
+ "reasoning": full_reasoning,
"tool_calls": None,
"finish_reason": "stop",
}
break
+ stats = _extract_ollama_stats(final_chunk)
+ if stats is not None:
+ yield ("stats", stats)
+
if final_message is not None:
yield ("message", final_message)
else:
@@ -363,6 +536,7 @@ class OllamaClient(GenAIClient):
"message",
{
"content": "".join(content_parts).strip() or None,
+ "reasoning": "".join(reasoning_parts).strip() or None,
"tool_calls": None,
"finish_reason": "stop",
},
diff --git a/frigate/genai/openai.py b/frigate/genai/plugins/openai.py
similarity index 72%
rename from frigate/genai/openai.py
rename to frigate/genai/plugins/openai.py
index 88108e730d..e89ab93922 100644
--- a/frigate/genai/openai.py
+++ b/frigate/genai/plugins/openai.py
@@ -3,7 +3,8 @@
import base64
import json
import logging
-from typing import Any, AsyncGenerator, Optional
+from collections.abc import AsyncGenerator
+from typing import Any
from httpx import TimeoutException
from openai import OpenAI
@@ -14,15 +15,35 @@ from frigate.genai import GenAIClient, register_genai_provider
logger = logging.getLogger(__name__)
+def _stats_from_openai_usage(usage: Any) -> dict[str, Any] | None:
+ """Build a stats dict from an OpenAI-compatible usage object."""
+ if usage is None:
+ return None
+ prompt_tokens = getattr(usage, "prompt_tokens", None)
+ completion_tokens = getattr(usage, "completion_tokens", None)
+ if prompt_tokens is None and completion_tokens is None:
+ return None
+ stats: dict[str, Any] = {}
+ if isinstance(prompt_tokens, int):
+ stats["prompt_tokens"] = prompt_tokens
+ if isinstance(completion_tokens, int):
+ stats["completion_tokens"] = completion_tokens
+ return stats or None
+
+
@register_genai_provider(GenAIProviderEnum.openai)
class OpenAIClient(GenAIClient):
"""Generative AI client for Frigate using OpenAI."""
provider: OpenAI
- context_size: Optional[int] = None
+ context_size: int | None = None
def _init_provider(self) -> OpenAI:
- """Initialize the client."""
+ """Initialize the client.
+
+ Subclasses (e.g. Azure) should raise on configuration errors; the
+ manager catches construction failures and disables the provider.
+ """
# Extract context_size from provider_options as it's not a valid OpenAI client parameter
# It will be used in get_context_size() instead
provider_opts = {
@@ -40,8 +61,9 @@ class OpenAIClient(GenAIClient):
self,
prompt: str,
images: list[bytes],
- response_format: Optional[dict] = None,
- ) -> Optional[str]:
+ response_format: dict | None = None,
+ enable_thinking: bool = False,
+ ) -> str | None:
"""Submit a request to OpenAI."""
encoded_images = [base64.b64encode(image).decode("utf-8") for image in images]
messages_content: list[dict] = [
@@ -73,14 +95,39 @@ class OpenAIClient(GenAIClient):
**self.genai_config.runtime_options,
}
if response_format:
+ # OpenAI strict mode requires additionalProperties: false on the schema
+ if response_format.get("type") == "json_schema" and response_format.get(
+ "json_schema", {}
+ ).get("strict"):
+ schema = response_format.get("json_schema", {}).get("schema")
+ if isinstance(schema, dict):
+ schema["additionalProperties"] = False
request_params["response_format"] = response_format
+
result = self.provider.chat.completions.create(**request_params)
+
if (
result is not None
and hasattr(result, "choices")
and len(result.choices) > 0
):
- return str(result.choices[0].message.content.strip())
+ message = result.choices[0].message
+ content = message.content
+
+ if not content:
+ # When reasoning is enabled for some OpenAI backends the actual response
+ # is incorrectly placed in reasoning_content instead of content.
+ # This is buggy/incorrect behavior — reasoning should not be
+ # enabled for these models.
+ reasoning_content = getattr(message, "reasoning_content", None)
+ if reasoning_content:
+ logger.warning(
+ "Response content was empty but reasoning_content was provided; "
+ "reasoning appears to be enabled and should be disabled for this model."
+ )
+ content = reasoning_content
+
+ return str(content.strip()) if content else None
return None
except (TimeoutException, Exception) as e:
logger.warning("OpenAI returned an error: %s", str(e))
@@ -140,13 +187,16 @@ class OpenAIClient(GenAIClient):
def chat_with_tools(
self,
messages: list[dict[str, Any]],
- tools: Optional[list[dict[str, Any]]] = None,
- tool_choice: Optional[str] = "auto",
+ tools: list[dict[str, Any]] | None = None,
+ tool_choice: str | None = "auto",
+ enable_thinking: bool | None = None,
) -> dict[str, Any]:
"""
Send chat messages to OpenAI with optional tool definitions.
- Implements function calling/tool usage for OpenAI models.
+ Implements function calling/tool usage for OpenAI models. The OpenAI
+ chat completions API does not expose a per-request thinking toggle,
+ so ``enable_thinking`` is accepted for interface parity and ignored.
"""
try:
openai_tool_choice = None
@@ -162,6 +212,7 @@ class OpenAIClient(GenAIClient):
"model": self.genai_config.model,
"messages": messages,
"timeout": self.timeout,
+ **self.genai_config.runtime_options,
}
if tools:
@@ -178,7 +229,7 @@ class OpenAIClient(GenAIClient):
}
request_params.update(provider_opts)
- result = self.provider.chat.completions.create(**request_params) # type: ignore[call-overload]
+ result = self.provider.chat.completions.create(**request_params)
if (
result is None
@@ -194,6 +245,10 @@ class OpenAIClient(GenAIClient):
choice = result.choices[0]
message = choice.message
content = message.content.strip() if message.content else None
+ raw_reasoning = getattr(message, "reasoning_content", None) or getattr(
+ message, "reasoning", None
+ )
+ reasoning = raw_reasoning.strip() if raw_reasoning else None
tool_calls = None
if message.tool_calls:
@@ -228,6 +283,7 @@ class OpenAIClient(GenAIClient):
return {
"content": content,
+ "reasoning": reasoning,
"tool_calls": tool_calls,
"finish_reason": finish_reason,
}
@@ -236,6 +292,7 @@ class OpenAIClient(GenAIClient):
logger.warning("OpenAI request timed out: %s", str(e))
return {
"content": None,
+ "reasoning": None,
"tool_calls": None,
"finish_reason": "error",
}
@@ -243,6 +300,7 @@ class OpenAIClient(GenAIClient):
logger.warning("OpenAI returned an error: %s", str(e))
return {
"content": None,
+ "reasoning": None,
"tool_calls": None,
"finish_reason": "error",
}
@@ -250,13 +308,17 @@ class OpenAIClient(GenAIClient):
async def chat_with_tools_stream(
self,
messages: list[dict[str, Any]],
- tools: Optional[list[dict[str, Any]]] = None,
- tool_choice: Optional[str] = "auto",
+ tools: list[dict[str, Any]] | None = None,
+ tool_choice: str | None = "auto",
+ enable_thinking: bool | None = None,
) -> AsyncGenerator[tuple[str, Any], None]:
"""
Stream chat with tools; yields content deltas then final message.
Implements streaming function calling/tool usage for OpenAI models.
+ The OpenAI chat completions API does not expose a per-request thinking
+ toggle, so ``enable_thinking`` is accepted for interface parity and
+ ignored.
"""
try:
openai_tool_choice = None
@@ -273,6 +335,8 @@ class OpenAIClient(GenAIClient):
"messages": messages,
"timeout": self.timeout,
"stream": True,
+ "stream_options": {"include_usage": True},
+ **self.genai_config.runtime_options,
}
if tools:
@@ -291,12 +355,18 @@ class OpenAIClient(GenAIClient):
# Use streaming API
content_parts: list[str] = []
+ reasoning_parts: list[str] = []
tool_calls_by_index: dict[int, dict[str, Any]] = {}
finish_reason = "stop"
+ usage_stats: dict[str, Any] | None = None
- stream = self.provider.chat.completions.create(**request_params) # type: ignore[call-overload]
+ stream = self.provider.chat.completions.create(**request_params)
for chunk in stream:
+ chunk_usage = getattr(chunk, "usage", None)
+ if chunk_usage is not None:
+ usage_stats = _stats_from_openai_usage(chunk_usage)
+
if not chunk or not chunk.choices:
continue
@@ -307,6 +377,15 @@ class OpenAIClient(GenAIClient):
if choice.finish_reason:
finish_reason = choice.finish_reason
+ # Extract reasoning deltas (reasoning_content or reasoning,
+ # depending on the server)
+ reasoning_delta = getattr(delta, "reasoning_content", None) or getattr(
+ delta, "reasoning", None
+ )
+ if reasoning_delta:
+ reasoning_parts.append(reasoning_delta)
+ yield ("reasoning_delta", reasoning_delta)
+
# Extract content deltas
if delta.content:
content_parts.append(delta.content)
@@ -335,6 +414,7 @@ class OpenAIClient(GenAIClient):
# Build final message
full_content = "".join(content_parts).strip() or None
+ full_reasoning = "".join(reasoning_parts).strip() or None
# Convert tool calls to list format
tool_calls_list = None
@@ -343,9 +423,18 @@ class OpenAIClient(GenAIClient):
for tc in tool_calls_by_index.values():
try:
# Parse accumulated arguments as JSON
- parsed_args = json.loads(tc["arguments"])
- except (json.JSONDecodeError, Exception):
- parsed_args = tc["arguments"]
+ parsed_args = json.loads(tc["arguments"] or "{}")
+ except (json.JSONDecodeError, ValueError):
+ logger.warning(
+ "Failed to parse streamed tool call arguments for %s",
+ tc["name"],
+ )
+ parsed_args = {}
+
+ # Downstream (ToolCall model) requires a dict; never leak a
+ # partial/invalid arguments string.
+ if not isinstance(parsed_args, dict):
+ parsed_args = {}
tool_calls_list.append(
{
@@ -356,10 +445,14 @@ class OpenAIClient(GenAIClient):
)
finish_reason = "tool_calls"
+ if usage_stats is not None:
+ yield ("stats", usage_stats)
+
yield (
"message",
{
"content": full_content,
+ "reasoning": full_reasoning,
"tool_calls": tool_calls_list,
"finish_reason": finish_reason,
},
@@ -371,6 +464,7 @@ class OpenAIClient(GenAIClient):
"message",
{
"content": None,
+ "reasoning": None,
"tool_calls": None,
"finish_reason": "error",
},
@@ -381,6 +475,7 @@ class OpenAIClient(GenAIClient):
"message",
{
"content": None,
+ "reasoning": None,
"tool_calls": None,
"finish_reason": "error",
},
diff --git a/frigate/genai/prompts.py b/frigate/genai/prompts.py
new file mode 100644
index 0000000000..33045606eb
--- /dev/null
+++ b/frigate/genai/prompts.py
@@ -0,0 +1,758 @@
+"""Prompt and response-format builders for GenAI features.
+
+Centralizes the per-feature prompt framing and structured-output schema
+shaping so provider clients in :mod:`frigate.genai.plugins` only handle
+transport.
+"""
+
+import datetime
+from typing import Any, Literal
+
+from playhouse.shortcuts import model_to_dict
+
+from frigate.config import CameraConfig, FrigateConfig
+from frigate.config.classification import ObjectClassificationType
+from frigate.config.ui import UnitSystemEnum
+from frigate.data_processing.post.types import ReviewMetadata
+from frigate.models import Event
+
+
+def build_review_description_prompt(
+ review_data: dict[str, Any],
+ thumbnails: list[bytes],
+ concerns: list[str],
+ preferred_language: str | None,
+ activity_context_prompt: str,
+) -> str:
+ """Build the prompt for review activity description generation."""
+
+ def get_concern_prompt() -> str:
+ if concerns:
+ concern_list = "\n - ".join(concerns)
+ return (
+ "\n- `other_concerns` (list of strings): Include a list of any of "
+ "the following concerns that are occurring:\n"
+ f" - {concern_list}"
+ )
+ else:
+ return ""
+
+ def get_language_prompt() -> str:
+ if preferred_language:
+ return f"Provide your answer in {preferred_language}"
+ else:
+ return ""
+
+ def get_objects_list() -> str:
+ if review_data["unified_objects"]:
+ return "\n- " + "\n- ".join(review_data["unified_objects"])
+ else:
+ return "\n- (No objects detected)"
+
+ return f"""
+Your task is to analyze a sequence of images taken in chronological order from a security camera.
+
+## Normal Activity Patterns for This Property
+
+{activity_context_prompt}
+
+## Task Instructions
+
+Describe the scene based on observable actions and movements, evaluate the activity against the Activity Indicators above, and assign a potential_threat_level (0, 1, or 2) by applying the threat level indicators consistently.
+
+## Analysis Guidelines
+
+When forming your description:
+- **Treat "Objects in Scene" as the list of tracked subjects to describe.** Do not introduce additional people or vehicles that are not present in this list. You may freely reference other items, surfaces, and environmental details visible in the frames when describing what the listed subjects are doing.
+- **Describe the most likely activity from visible cues across the sequence** — the subject's path, what they are carrying, and what they interact with. Avoid asserting completed outcomes you do not observe; describe in-progress actions rather than results.
+- Describe what you observe: actions, movements, interactions with objects and the environment. Include any observable environmental changes (e.g., lighting changes triggered by activity).
+- Note visible details such as clothing, items being carried or placed, tools or equipment present, and how they interact with the property or objects.
+- Consider the full sequence chronologically: what happens from start to finish, how duration and actions relate to the location and objects involved.
+- **Use the actual timestamp provided in "Activity started at"** below for time of day context—do not infer time from image brightness or darkness. Unusual hours (late night/early morning) should increase suspicion when the observable behavior itself appears questionable. However, recognize that some legitimate activities can occur at any hour.
+- **Consider duration as a primary factor**: Apply the duration thresholds defined in the activity patterns above. Brief sequences during normal hours with apparent purpose typically indicate normal activity unless explicit suspicious actions are visible.
+- **Weigh all evidence holistically**: Match the activity against the normal and suspicious patterns defined above, then evaluate based on the complete context (zone, objects, time, actions, duration). Apply the threat level indicators consistently. Use your judgment for edge cases.
+
+## Response Field Guidelines
+
+Respond with a JSON object matching the provided schema. Field-specific guidance:
+- `observations`: Include the very start of the activity — for example, a vehicle entering the frame or pulling into the driveway — even if it lasts only a few frames and the rest of the clip is dominated by a longer activity. Include each arrival, departure, object handled, and notable change in position or state. Each item is a single concrete fact written as a complete sentence.
+- `scene`: Describe how the sequence begins, then the progression of events — all significant movements and actions in order. For example, if a vehicle arrives and then a person exits, describe both sequentially. For named subjects (those with a `←` separator in "Objects in Scene"), always use their name — do not replace them with generic terms. For unnamed objects (e.g., "person", "car"), refer to them naturally with articles (e.g., "a person", "the car"). Your description should align with and support the threat level you assign.
+- `title`: Name the primary activity across the observations, together with the location. An activity is what is being done with objects, tools, or surfaces; locomotion through the scene qualifies as the activity only when no other interaction is observed. For named subjects, always use their name. For unnamed objects, refer to them naturally with articles.
+- `shortSummary`: Briefly summarize the primary activity across the observations.
+- `potential_threat_level`: Must be consistent with your scene description and the activity patterns above.
+{get_concern_prompt()}
+
+## Sequence Details
+
+- Camera: {review_data["camera"]}
+- Total frames: {len(thumbnails)} (Frame 1 = earliest, Frame {len(thumbnails)} = latest)
+- Activity started at {review_data["start"]} and lasted {review_data["duration"]} seconds
+- Zones involved: {", ".join(review_data["zones"]) if review_data["zones"] else "None"}
+
+## Objects in Scene
+
+Each line represents a detection state, not necessarily unique individuals. The `←` symbol separates a recognized subject's name from their object type — use only the name (before the `←`) in your response, not the type after it. The same subject may appear across multiple lines if detected multiple times.
+
+**Note: Unidentified objects (without names) are NOT indicators of suspicious activity—they simply mean the system hasn't identified that object.**
+{get_objects_list()}
+
+{get_language_prompt()}
+"""
+
+
+def build_review_description_response_format(concerns: list[str]) -> dict[str, Any]:
+ """Build the structured-output JSON schema for review descriptions.
+
+ Strips the `time` field (populated server-side) and drops
+ `other_concerns` when no concerns are configured.
+ """
+ schema = ReviewMetadata.model_json_schema()
+ schema.get("properties", {}).pop("time", None)
+
+ if "time" in schema.get("required", []):
+ schema["required"].remove("time")
+ if not concerns:
+ schema.get("properties", {}).pop("other_concerns", None)
+ if "other_concerns" in schema.get("required", []):
+ schema["required"].remove("other_concerns")
+
+ return {
+ "type": "json_schema",
+ "json_schema": {
+ "name": "review_metadata",
+ "strict": True,
+ "schema": schema,
+ },
+ }
+
+
+def build_review_summary_prompt(
+ start_ts: float,
+ end_ts: float,
+ events: list[dict[str, Any]],
+ preferred_language: str | None,
+) -> str:
+ """Build the prompt for a multi-event review summary."""
+ time_range = (
+ f"{datetime.datetime.fromtimestamp(start_ts).strftime('%B %d, %Y at %I:%M %p')}"
+ f" to "
+ f"{datetime.datetime.fromtimestamp(end_ts).strftime('%B %d, %Y at %I:%M %p')}"
+ )
+ prompt = f"""
+You are a security officer writing a concise security report.
+
+Time range: {time_range}
+
+Input format: Each event is a JSON object with:
+- "title", "scene", "confidence", "potential_threat_level" (0-2), "other_concerns", "camera", "time", "start_time", "end_time"
+- "context": array of related events from other cameras that occurred during overlapping time periods
+
+**Note: Use the "scene" field for event descriptions in the report. Ignore any "shortSummary" field if present.**
+
+Report Structure - Use this EXACT format:
+
+# Security Summary - {time_range}
+
+## Overview
+[Write 1-2 sentences summarizing the overall activity pattern during this period.]
+
+---
+
+## Timeline
+
+[Group events by time periods (e.g., "Morning (6:00 AM - 12:00 PM)", "Afternoon (12:00 PM - 5:00 PM)", "Evening (5:00 PM - 9:00 PM)", "Night (9:00 PM - 6:00 AM)"). Use appropriate time blocks based on when events occurred.]
+
+### [Time Block Name]
+
+**HH:MM AM/PM** | [Camera Name] | [Threat Level Indicator]
+- [Event title]: [Clear description incorporating contextual information from the "context" array]
+- Context: [If context array has items, mention them here, e.g., "Delivery truck present on Front Driveway Cam (HH:MM AM/PM)"]
+- Assessment: [Brief assessment incorporating context - if context explains the event, note it here]
+
+[Repeat for each event in chronological order within the time block]
+
+---
+
+## Summary
+[One sentence summarizing the period. If all events are normal/explained: "Routine activity observed." If review needed: "Some activity requires review but no security concerns." If security concerns: "Security concerns requiring immediate attention."]
+
+Guidelines:
+- List ALL events in chronological order, grouped by time blocks
+- Threat level indicators: ✓ Normal, ⚠️ Needs review, 🔴 Security concern
+- Integrate contextual information naturally - use the "context" array to enrich each event's description
+- If context explains the event (e.g., delivery truck explains person at door), describe it accordingly (e.g., "delivery person" not "unidentified person")
+- Be concise but informative - focus on what happened and what it means
+- If contextual information makes an event clearly normal, reflect that in your assessment
+- Only create time blocks that have events - don't create empty sections
+"""
+
+ prompt += "\n\nEvents:\n"
+ for event in events:
+ prompt += f"\n{event}\n"
+
+ if preferred_language:
+ prompt += f"\nProvide your answer in {preferred_language}"
+
+ return prompt
+
+
+def build_object_description_prompt(
+ camera_config: CameraConfig,
+ event: Event,
+) -> str:
+ """Build the prompt for a per-object description.
+
+ Pulls the per-label override from `objects.genai.object_prompts`, falling
+ back to the camera default, and interpolates event fields.
+
+ Raises:
+ KeyError: if the user-defined prompt template references an unknown
+ event field.
+ """
+ template = camera_config.objects.genai.object_prompts.get(
+ str(event.label),
+ camera_config.objects.genai.prompt,
+ )
+ return template.format(**model_to_dict(event))
+
+
+def get_attribute_classifications(config: FrigateConfig) -> list[dict[str, Any]]:
+ """Return enabled custom classification models of `attribute` type.
+
+ Each entry: {"name": , "objects": [, ...]}.
+ These models attach attribute metadata to events on the listed object
+ types, which can later be filtered via the search_objects `attribute`
+ field.
+ """
+ result: list[dict[str, Any]] = []
+
+ for model_key, model_config in config.classification.custom.items():
+ if not model_config.enabled or model_config.object_config is None:
+ continue
+
+ if (
+ model_config.object_config.classification_type
+ != ObjectClassificationType.attribute
+ ):
+ continue
+
+ result.append(
+ {
+ "name": model_config.name or model_key,
+ "objects": list(model_config.object_config.objects or []),
+ }
+ )
+
+ return result
+
+
+def get_tool_definitions(
+ semantic_search_enabled: bool = False,
+ attribute_classifications: list[dict[str, Any]] | None = None,
+ embeddings_language: Literal["english", "multi"] = "multi",
+) -> list[dict[str, Any]]:
+ """
+ Get OpenAI-compatible tool definitions for Frigate.
+
+ Returns a list of tool definitions that can be used with OpenAI-compatible
+ function calling APIs. When semantic search is enabled, the search_objects
+ tool exposes an additional `semantic_query` parameter for descriptive
+ queries (e.g. "person riding a lawn mower") and find_similar_objects is
+ included. When attribute classification models are configured, an
+ `attribute` parameter is exposed for filtering by their labels. When the
+ embeddings model only understands English (JinaV1), the `semantic_query`
+ description instructs the model to write the query in English.
+ """
+ search_objects_properties: dict[str, Any] = {
+ "camera": {
+ "type": "string",
+ "description": "Camera name to filter by (optional).",
+ },
+ "label": {
+ "type": "string",
+ "description": (
+ "Generic object class to filter by — one of the tracked detector "
+ "labels such as 'person', 'package', 'car', 'dog', 'bird'. Use "
+ "this for broad queries like 'show me all cars today'. Combine "
+ "with semantic_query when the user also describes appearance or "
+ "behavior (e.g. label='person', semantic_query='riding a lawn "
+ "mower')."
+ ),
+ },
+ "sub_label": {
+ "type": "string",
+ "description": (
+ "Filter by a DISCRETE NAMED entity recognized in the detection. "
+ "Use this for: a known person's name ('John'), a delivery "
+ "company ('Amazon', 'UPS'), a recognized animal species or "
+ "breed ('blue jay', 'cardinal', 'golden retriever'), or a "
+ "license plate string. When filtering by a specific name, set "
+ "only sub_label and leave label unset. Do NOT use sub_label "
+ "for descriptions of appearance, clothing, or actions — those "
+ "belong in semantic_query."
+ ),
+ },
+ "after": {
+ "type": "string",
+ "description": "Start time in ISO 8601 format (e.g., '2024-01-01T00:00:00Z').",
+ },
+ "before": {
+ "type": "string",
+ "description": "End time in ISO 8601 format (e.g., '2024-01-01T23:59:59Z').",
+ },
+ "zones": {
+ "type": "array",
+ "items": {"type": "string"},
+ "description": "List of zone names to filter by.",
+ },
+ "limit": {
+ "type": "integer",
+ "description": "Maximum number of objects to return (default: 25).",
+ "default": 25,
+ },
+ }
+
+ if attribute_classifications:
+ model_outline = "; ".join(
+ f"{m['name']} (applies to {', '.join(m['objects']) or 'any object'})"
+ for m in attribute_classifications
+ )
+ search_objects_properties["attribute"] = {
+ "type": "string",
+ "description": (
+ "Filter by a classification attribute label produced by a "
+ "configured attribute classification model. Use this INSTEAD "
+ "of semantic_query when the user's request matches one of "
+ "these classifications. Configured models: "
+ f"{model_outline}. "
+ "Set the value to the attribute label that matches the user's "
+ "phrasing (case-sensitive)."
+ ),
+ }
+
+ if semantic_search_enabled:
+ search_objects_properties["semantic_query"] = {
+ "type": "string",
+ "description": (
+ "Optional natural-language description of a PHYSICAL "
+ "CHARACTERISTIC, APPEARANCE, or ACTIVITY the user mentioned, "
+ "used to semantically narrow results. Only set this when the "
+ "user describes something beyond what label and sub_label can "
+ "express on their own.\n"
+ "USE for descriptive phrases like: 'riding a lawn mower', "
+ "'wearing a red jacket', 'carrying a package', 'walking a "
+ "dog', 'on a bicycle', 'holding an umbrella'.\n"
+ "DO NOT USE for:\n"
+ "- specific named people, pets, or delivery companies → use sub_label\n"
+ "- animal species or breed names like 'blue jay', 'cardinal', "
+ "'golden retriever' → use sub_label\n"
+ "- license plate strings → use sub_label\n"
+ "- generic object queries like 'all cars today' or 'every "
+ "person' → use label alone with no semantic_query\n"
+ "When set, combine with label/time/camera/zone filters as "
+ "usual (e.g. label='person', semantic_query='riding a lawn "
+ "mower', after='2024-05-01T00:00:00Z')."
+ + (
+ " The configured embeddings model only understands "
+ "English, so always write semantic_query in English, "
+ "translating the user's description if they phrased it "
+ "in another language."
+ if embeddings_language == "english"
+ else ""
+ )
+ ),
+ }
+
+ search_objects_description = (
+ "Search the historical record of detected objects in Frigate. "
+ "Use this ONLY for questions about the PAST — e.g. 'did anyone come by today?', "
+ "'when was the last car?', 'show me detections from yesterday'. "
+ "Do NOT use this for monitoring or alerting requests about future events — "
+ "use start_camera_watch instead for those. "
+ "An 'object' in Frigate represents a tracked detection (e.g., a person, package, car).\n\n"
+ "Choose filters based on what the user is asking for:\n"
+ "- Generic class query ('show me all cars today'): set `label` only.\n"
+ "- Specific NAMED entity (known person, delivery company, animal "
+ "species/breed like 'blue jay' or 'golden retriever', license "
+ "plate): set `sub_label` only and leave `label` unset.\n"
+ )
+ if semantic_search_enabled:
+ search_objects_description += (
+ "- Physical CHARACTERISTIC, APPEARANCE, or ACTIVITY that is not a "
+ "discrete name ('person riding a lawn mower', 'someone in a red "
+ "jacket', 'person carrying a package'): set `semantic_query` with "
+ "the descriptive phrase, optionally alongside `label` for the "
+ "object class. Do NOT put descriptive phrases in sub_label."
+ )
+
+ return [
+ {
+ "type": "function",
+ "function": {
+ "name": "search_objects",
+ "description": search_objects_description,
+ "parameters": {
+ "type": "object",
+ "properties": search_objects_properties,
+ },
+ "required": [],
+ },
+ },
+ {
+ "type": "function",
+ "function": {
+ "name": "find_similar_objects",
+ "description": (
+ "Find tracked objects that are visually and semantically similar "
+ "to a specific past event. Use this when the user references a "
+ "particular object they have seen and wants to find other "
+ "sightings of the same or similar one ('that green car', 'the "
+ "person in the red jacket', 'the package that was delivered'). "
+ "Prefer this over search_objects whenever the user's intent is "
+ "'find more like this specific one.' Use search_objects first "
+ "only if you need to locate the anchor event. Requires semantic "
+ "search to be enabled."
+ ),
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "event_id": {
+ "type": "string",
+ "description": "The id of the anchor event to find similar objects to.",
+ },
+ "after": {
+ "type": "string",
+ "description": "Start time in ISO 8601 format (e.g., '2024-01-01T00:00:00Z').",
+ },
+ "before": {
+ "type": "string",
+ "description": "End time in ISO 8601 format (e.g., '2024-01-01T23:59:59Z').",
+ },
+ "cameras": {
+ "type": "array",
+ "items": {"type": "string"},
+ "description": "Optional list of cameras to restrict to. Defaults to all.",
+ },
+ "labels": {
+ "type": "array",
+ "items": {"type": "string"},
+ "description": "Optional list of labels to restrict to. Defaults to the anchor event's label.",
+ },
+ "sub_labels": {
+ "type": "array",
+ "items": {"type": "string"},
+ "description": "Optional list of sub_labels (names) to restrict to.",
+ },
+ "zones": {
+ "type": "array",
+ "items": {"type": "string"},
+ "description": "Optional list of zones. An event matches if any of its zones overlap.",
+ },
+ "similarity_mode": {
+ "type": "string",
+ "enum": ["visual", "semantic", "fused"],
+ "description": "Which similarity signal(s) to use. 'fused' (default) combines visual and semantic.",
+ "default": "fused",
+ },
+ "min_score": {
+ "type": "number",
+ "description": "Drop matches with a similarity score below this threshold (0.0-1.0).",
+ },
+ "limit": {
+ "type": "integer",
+ "description": "Maximum number of matches to return (default: 10).",
+ "default": 10,
+ },
+ },
+ "required": ["event_id"],
+ },
+ },
+ },
+ {
+ "type": "function",
+ "function": {
+ "name": "set_camera_state",
+ "description": (
+ "Change a camera's feature state (e.g., turn detection on/off, enable/disable recordings). "
+ "Use camera='*' to apply to all cameras at once. "
+ "Only call this tool when the user explicitly asks to change a camera setting. "
+ "Requires admin privileges."
+ ),
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "camera": {
+ "type": "string",
+ "description": "Camera name to target, or '*' to target all cameras.",
+ },
+ "feature": {
+ "type": "string",
+ "enum": [
+ "detect",
+ "record",
+ "snapshots",
+ "audio",
+ "motion",
+ "enabled",
+ "birdseye",
+ "birdseye_mode",
+ "improve_contrast",
+ "ptz_autotracker",
+ "motion_contour_area",
+ "motion_threshold",
+ "notifications",
+ "audio_transcription",
+ "review_alerts",
+ "review_detections",
+ "object_descriptions",
+ "review_descriptions",
+ "profile",
+ ],
+ "description": (
+ "The feature to change. Most features accept ON or OFF. "
+ "birdseye_mode accepts CONTINUOUS, MOTION, or OBJECTS. "
+ "motion_contour_area and motion_threshold accept a number. "
+ "profile accepts a profile name or 'none' to deactivate (requires camera='*')."
+ ),
+ },
+ "value": {
+ "type": "string",
+ "description": "The value to set. ON or OFF for toggles, a number for thresholds, a profile name or 'none' for profile.",
+ },
+ },
+ "required": ["camera", "feature", "value"],
+ },
+ },
+ },
+ {
+ "type": "function",
+ "function": {
+ "name": "get_live_context",
+ "description": (
+ "Get the current live image and detection information for a single camera: objects being tracked, "
+ "zones, timestamps. Use this to understand what is visible in the live view. "
+ "Call this when answering questions about what is happening right now on a specific camera. "
+ "Operates on one camera at a time; call the tool again for each additional camera. "
+ "Wildcards and empty values are not accepted."
+ ),
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "camera": {
+ "type": "string",
+ "description": (
+ "Exact name of a single camera to get live context for. "
+ "Wildcards (e.g. '*', 'all') and empty strings are not accepted."
+ ),
+ },
+ },
+ "required": ["camera"],
+ },
+ },
+ },
+ {
+ "type": "function",
+ "function": {
+ "name": "start_camera_watch",
+ "description": (
+ "Start a continuous VLM watch job that monitors a camera and sends a notification "
+ "when a specified condition is met. Use this when the user wants to be alerted about "
+ "a future event, e.g. 'tell me when guests arrive' or 'notify me when the package is picked up'. "
+ "Only one watch job can run at a time. Returns a job ID."
+ ),
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "camera": {
+ "type": "string",
+ "description": "Camera ID to monitor.",
+ },
+ "condition": {
+ "type": "string",
+ "description": (
+ "Natural-language description of the condition to watch for, "
+ "e.g. 'a person arrives at the front door'."
+ ),
+ },
+ "max_duration_minutes": {
+ "type": "integer",
+ "description": "Maximum time to watch before giving up (minutes, default 60).",
+ "default": 60,
+ },
+ "labels": {
+ "type": "array",
+ "items": {"type": "string"},
+ "description": "Object labels that should trigger a VLM check (e.g. ['person', 'car']). If omitted, any detection on the camera triggers a check.",
+ },
+ "zones": {
+ "type": "array",
+ "items": {"type": "string"},
+ "description": "Zone names to filter by. If specified, only detections in these zones trigger a VLM check.",
+ },
+ },
+ "required": ["camera", "condition"],
+ },
+ },
+ },
+ {
+ "type": "function",
+ "function": {
+ "name": "stop_camera_watch",
+ "description": (
+ "Cancel the currently running VLM watch job. Use this when the user wants to "
+ "stop a previously started watch, e.g. 'stop watching the front door'."
+ ),
+ "parameters": {
+ "type": "object",
+ "properties": {},
+ "required": [],
+ },
+ },
+ },
+ {
+ "type": "function",
+ "function": {
+ "name": "get_profile_status",
+ "description": (
+ "Get the current profile status including the active profile and "
+ "timestamps of when each profile was last activated. Use this to "
+ "determine time periods for recap requests — e.g. when the user asks "
+ "'what happened while I was away?', call this first to find the relevant "
+ "time window based on profile activation history."
+ ),
+ "parameters": {
+ "type": "object",
+ "properties": {},
+ "required": [],
+ },
+ },
+ },
+ {
+ "type": "function",
+ "function": {
+ "name": "get_recap",
+ "description": (
+ "Get a recap of all activity (alerts and detections) for a given time period. "
+ "Use this after calling get_profile_status to retrieve what happened during "
+ "a specific window — e.g. 'what happened while I was away?'. Returns a "
+ "chronological list of activity with camera, objects, zones, and GenAI-generated "
+ "descriptions when available. Summarize the results for the user."
+ ),
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "after": {
+ "type": "string",
+ "description": "Start of the time period in ISO 8601 format (e.g. '2025-03-15T08:00:00').",
+ },
+ "before": {
+ "type": "string",
+ "description": "End of the time period in ISO 8601 format (e.g. '2025-03-15T17:00:00').",
+ },
+ "cameras": {
+ "type": "string",
+ "description": "Comma-separated camera IDs to include, or 'all' for all cameras. Default is 'all'.",
+ },
+ "severity": {
+ "type": "string",
+ "enum": ["alert", "detection"],
+ "description": "Filter by severity level. Omit to include both alerts and detections.",
+ },
+ },
+ "required": ["after", "before"],
+ },
+ },
+ },
+ ]
+
+
+def build_chat_system_prompt(
+ config: FrigateConfig,
+ allowed_cameras: list[str],
+ semantic_search_enabled: bool,
+ attribute_classifications: list[dict[str, Any]],
+) -> str:
+ """Build the system prompt for the chat completion endpoint.
+
+ Composes the static framing with conditional sections describing the
+ available cameras, speed units, semantic-search routing guidance, and
+ configured attribute classifications.
+ """
+ current_datetime = datetime.datetime.now()
+ current_date_str = current_datetime.strftime("%Y-%m-%d")
+ current_time_str = current_datetime.strftime("%I:%M:%S %p")
+
+ cameras_info: list[str] = []
+ has_speed_zone = False
+ for camera_id in allowed_cameras:
+ if camera_id not in config.cameras:
+ continue
+ camera_config = config.cameras[camera_id]
+ friendly_name = (
+ camera_config.friendly_name
+ if camera_config.friendly_name
+ else camera_id.replace("_", " ").title()
+ )
+ zone_descriptors = [
+ f"{zone_config.get_formatted_name(zone_name)} (ID: {zone_name})"
+ for zone_name, zone_config in camera_config.zones.items()
+ ]
+ if not has_speed_zone:
+ has_speed_zone = any(
+ zone.distances for zone in camera_config.zones.values()
+ )
+ if zone_descriptors:
+ cameras_info.append(
+ f" - {friendly_name} (ID: {camera_id}, zones: {', '.join(zone_descriptors)})"
+ )
+ else:
+ cameras_info.append(f" - {friendly_name} (ID: {camera_id})")
+
+ cameras_section = ""
+ if cameras_info:
+ cameras_section = (
+ "\n\nAvailable cameras:\n"
+ + "\n".join(cameras_info)
+ + "\n\nWhen users refer to cameras or zones by their friendly name (e.g., 'Back Deck Camera', 'Front Walkway'), use the corresponding ID (e.g., 'back_deck_cam', 'front_walk') in tool calls. Tool results also identify zones by their ID, so when presenting cameras or zones back to the user, translate the ID to its friendly name."
+ )
+
+ speed_units_section = ""
+ if has_speed_zone:
+ speed_unit = (
+ "mph" if config.ui.unit_system == UnitSystemEnum.imperial else "km/h"
+ )
+ speed_units_section = f"\n\nReport object speeds to the user in {speed_unit}."
+
+ semantic_search_section = ""
+ if semantic_search_enabled:
+ semantic_search_section = (
+ "\n\nWhen routing a search_objects call, pick filters by the shape of the user's request:\n"
+ "- Generic class ('show me all cars today'): set `label` only.\n"
+ "- Specific named entity — a known person ('John'), delivery company ('Amazon'), animal species/breed ('blue jay', 'cardinal', 'golden retriever'), or license plate: set `sub_label` only and leave `label` unset.\n"
+ "- Physical characteristic, appearance, or activity that is NOT a discrete name ('find me people riding a lawn mower', 'someone in a red jacket', 'a person carrying a package'): set `semantic_query` with the descriptive phrase, optionally combined with `label` for the object class. Never put descriptive phrases in `sub_label`."
+ )
+
+ attribute_classification_section = ""
+ if attribute_classifications:
+ model_lines = "\n".join(
+ f"- {m['name']}: applies to {', '.join(m['objects']) or 'any object'}"
+ for m in attribute_classifications
+ )
+ attribute_classification_section = (
+ "\n\nAttribute classification models are configured for the following object types:\n"
+ f"{model_lines}\n"
+ "When the user's request matches one of these classifications, set the search_objects `attribute` field to the matching label rather than using `semantic_query`. Reserve `semantic_query` for descriptive phrases that fall outside the configured attribute labels."
+ )
+
+ return f"""You are a helpful assistant for Frigate, a security camera NVR system. You help users answer questions about their cameras, detected objects, and events.
+
+Current server local date and time: {current_date_str} at {current_time_str}
+
+Do not start your response with phrases like "I will check...", "Let me see...", or "Let me look...". Answer directly.
+
+Always present times to the user in the server's local timezone. When tool results include start_time_local and end_time_local, use those exact strings when listing or describing detection times—do not convert or invent timestamps. Do not use UTC or ISO format with Z for the user-facing answer unless the tool result only provides Unix timestamps without local time fields.
+When users ask about "today", "yesterday", "this week", etc., use the current date above as reference.
+When searching for objects or events, use ISO 8601 format for dates (e.g., {current_date_str}T00:00:00Z for the start of today).
+Always be accurate with time calculations based on the current date provided.
+
+When a user refers to a specific object they have seen or describe with identifying details ("that green car", "the person in the red jacket", "a package left today"), prefer the find_similar_objects tool over search_objects. Use search_objects first only to locate the anchor event, then pass its id to find_similar_objects. For generic queries like "show me all cars today", keep using search_objects. If a user message begins with [attached_event:], treat that event id as the anchor for any similarity or "tell me more" request in the same message and call find_similar_objects with that id.{semantic_search_section}{attribute_classification_section}{cameras_section}{speed_units_section}"""
diff --git a/frigate/genai/utils.py b/frigate/genai/utils.py
index 44f982059b..fbd6f2110c 100644
--- a/frigate/genai/utils.py
+++ b/frigate/genai/utils.py
@@ -2,14 +2,14 @@
import json
import logging
-from typing import Any, List, Optional
+from typing import Any
logger = logging.getLogger(__name__)
def parse_tool_calls_from_message(
message: dict[str, Any],
-) -> Optional[list[dict[str, Any]]]:
+) -> list[dict[str, Any]] | None:
"""
Parse tool_calls from an OpenAI-style message dict.
@@ -52,7 +52,7 @@ def parse_tool_calls_from_message(
def build_assistant_message_for_conversation(
content: Any,
- tool_calls_raw: Optional[List[dict[str, Any]]],
+ tool_calls_raw: list[dict[str, Any]] | None,
) -> dict[str, Any]:
"""
Build the assistant message dict in OpenAI format for appending to a conversation.
@@ -69,6 +69,14 @@ def build_assistant_message_for_conversation(
"name": tc["name"],
"arguments": json.dumps(tc.get("arguments") or {}),
},
+ # Gemini-only: opaque signature that must be echoed back on
+ # the same functionCall part in the next turn. Other providers
+ # do not set or read this.
+ **(
+ {"thought_signature": tc["thought_signature"]}
+ if tc.get("thought_signature")
+ else {}
+ ),
}
for tc in tool_calls_raw
]
diff --git a/frigate/jobs/debug_replay.py b/frigate/jobs/debug_replay.py
new file mode 100644
index 0000000000..c5e4ed8761
--- /dev/null
+++ b/frigate/jobs/debug_replay.py
@@ -0,0 +1,493 @@
+"""Debug replay startup job: ffmpeg remux + camera config publish.
+
+The runner orchestrates the async portion of starting a debug replay
+session. The DebugReplayManager (in frigate.debug_replay) owns session
+presence so the status bar can keep reading a single `active` flag from
+/debug_replay/status for the entire session window — which is broader
+than this job's lifetime.
+"""
+
+import logging
+import os
+import subprocess as sp
+import threading
+import time
+from abc import ABC, abstractmethod
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Any, Optional, cast
+
+from peewee import ModelSelect
+
+from frigate.config import FrigateConfig
+from frigate.config.camera.updater import CameraConfigUpdatePublisher
+from frigate.const import REPLAY_CAMERA_PREFIX, REPLAY_DIR
+from frigate.jobs.export import JobStatePublisher
+from frigate.jobs.job import Job
+from frigate.jobs.manager import job_is_running, set_current_job
+from frigate.models import Export, Recordings
+from frigate.types import JobStatusTypesEnum
+from frigate.util.ffmpeg import run_ffmpeg_with_progress
+
+if TYPE_CHECKING:
+ from frigate.debug_replay import DebugReplayManager
+
+logger = logging.getLogger(__name__)
+
+# Coalesce frequent ffmpeg progress callbacks so the WS isn't flooded.
+PROGRESS_BROADCAST_MIN_INTERVAL = 1.0
+
+JOB_TYPE = "debug_replay"
+
+STEP_PREPARING_CLIP = "preparing_clip"
+STEP_STARTING_CAMERA = "starting_camera"
+
+
+_active_runner: Optional["DebugReplayJobRunner"] = None
+_runner_lock = threading.Lock()
+
+
+def _set_active_runner(runner: Optional["DebugReplayJobRunner"]) -> None:
+ global _active_runner
+ with _runner_lock:
+ _active_runner = runner
+
+
+def get_active_runner() -> Optional["DebugReplayJobRunner"]:
+ with _runner_lock:
+ return _active_runner
+
+
+@dataclass
+class DebugReplayJob(Job):
+ """Job state for a debug replay startup."""
+
+ job_type: str = JOB_TYPE
+ source_camera: str = ""
+ replay_camera_name: str = ""
+ start_ts: float = 0.0
+ end_ts: float = 0.0
+ current_step: str | None = None
+ progress_percent: float = 0.0
+
+ def to_dict(self) -> dict[str, Any]:
+ """Whitelisted payload for the job_state WS topic.
+
+ Replay-specific fields land in results so the frontend's
+ generic Job type can be parameterised cleanly.
+ """
+ return {
+ "id": self.id,
+ "job_type": self.job_type,
+ "status": self.status,
+ "start_time": self.start_time,
+ "end_time": self.end_time,
+ "error_message": self.error_message,
+ "results": {
+ "current_step": self.current_step,
+ "progress_percent": self.progress_percent,
+ "source_camera": self.source_camera,
+ "replay_camera_name": self.replay_camera_name,
+ "start_ts": self.start_ts,
+ "end_ts": self.end_ts,
+ },
+ }
+
+
+def query_recordings(source_camera: str, start_ts: float, end_ts: float) -> ModelSelect:
+ """Return the Recordings query for the time range.
+
+ Module-level so tests can patch it without instantiating a runner.
+ """
+ query = (
+ Recordings.select(
+ Recordings.path,
+ Recordings.start_time,
+ Recordings.end_time,
+ )
+ .where(
+ Recordings.start_time.between(start_ts, end_ts)
+ | Recordings.end_time.between(start_ts, end_ts)
+ | ((start_ts > Recordings.start_time) & (end_ts < Recordings.end_time))
+ )
+ .where(Recordings.camera == source_camera)
+ .order_by(Recordings.start_time.asc())
+ )
+ return cast(ModelSelect, query)
+
+
+class DebugReplaySource(ABC):
+ """Abstract source for a debug replay session.
+
+ Provides the camera identity and time range the replay represents,
+ validates that usable content exists, and supplies the ffmpeg input
+ args used to build the replay clip.
+ """
+
+ @property
+ @abstractmethod
+ def source_camera(self) -> str:
+ """Camera name the replay is derived from."""
+
+ @property
+ @abstractmethod
+ def start_ts(self) -> float:
+ """Unix timestamp marking the start of the replay range."""
+
+ @property
+ @abstractmethod
+ def end_ts(self) -> float:
+ """Unix timestamp marking the end of the replay range."""
+
+ @abstractmethod
+ def validate(self) -> None:
+ """Raise ValueError if the source has no usable content."""
+
+ @abstractmethod
+ def ffmpeg_input_args(self, working_dir: str) -> list[str]:
+ """Return ffmpeg input args (including -i). May write temp files in working_dir."""
+
+ def cleanup(self, working_dir: str) -> None:
+ """Remove any temp files the source created in working_dir. Default no-op."""
+
+
+class RecordingDebugReplaySource(DebugReplaySource):
+ """Replay source backed by the Recordings table.
+
+ Feeds ffmpeg the internal VOD endpoint so segments with mismatched
+ SPS/PPS (e.g. across day/night transitions) stitch cleanly via HLS
+ discontinuities.
+ """
+
+ def __init__(
+ self,
+ source_camera: str,
+ start_ts: float,
+ end_ts: float,
+ internal_port: int,
+ ) -> None:
+ self._camera = source_camera
+ self._start_ts = start_ts
+ self._end_ts = end_ts
+ self._internal_port = internal_port
+
+ @property
+ def source_camera(self) -> str:
+ return self._camera
+
+ @property
+ def start_ts(self) -> float:
+ return self._start_ts
+
+ @property
+ def end_ts(self) -> float:
+ return self._end_ts
+
+ def validate(self) -> None:
+ if self._end_ts <= self._start_ts:
+ raise ValueError("End time must be after start time")
+
+ if not query_recordings(self._camera, self._start_ts, self._end_ts).count():
+ raise ValueError(
+ f"No recordings found for camera '{self._camera}' in the specified time range"
+ )
+
+ def ffmpeg_input_args(self, working_dir: str) -> list[str]:
+ playlist_url = (
+ f"http://127.0.0.1:{self._internal_port}/vod/{self._camera}"
+ f"/start/{self._start_ts}/end/{self._end_ts}/index.m3u8"
+ )
+ return [
+ "-protocol_whitelist",
+ "pipe,file,http,tcp",
+ "-i",
+ playlist_url,
+ ]
+
+
+class ExportDebugReplaySource(DebugReplaySource):
+ """Replay source backed by an existing Export.
+
+ Uses the export's video file directly as the ffmpeg input — does not
+ require recordings to still exist for the time range.
+ """
+
+ def __init__(self, export: Export, duration: float) -> None:
+ self._camera = cast(str, export.camera)
+ # Export.date is declared DateTimeField but Frigate writes raw unix
+ # timestamps to the column.
+ self._start_ts = float(cast(Any, export.date))
+ self._video_path = cast(str, export.video_path)
+ self._duration = duration
+
+ @property
+ def source_camera(self) -> str:
+ return self._camera
+
+ @property
+ def start_ts(self) -> float:
+ return self._start_ts
+
+ @property
+ def end_ts(self) -> float:
+ return self._start_ts + self._duration
+
+ def validate(self) -> None:
+ if not os.path.exists(self._video_path):
+ raise ValueError(f"Export video file not found: {self._video_path}")
+
+ def ffmpeg_input_args(self, working_dir: str) -> list[str]:
+ return ["-i", self._video_path]
+
+
+class DebugReplayJobRunner(threading.Thread):
+ """Worker thread that drives the startup job to completion.
+
+ Owns the live ffmpeg Popen reference for cancellation. Cancellation
+ is two-step (threading.Event + proc.terminate()) so the runner
+ both knows it should stop and is unblocked from its blocking subprocess
+ wait.
+ """
+
+ def __init__(
+ self,
+ job: DebugReplayJob,
+ source: DebugReplaySource,
+ frigate_config: FrigateConfig,
+ config_publisher: CameraConfigUpdatePublisher,
+ replay_manager: "DebugReplayManager",
+ publisher: JobStatePublisher | None = None,
+ ) -> None:
+ super().__init__(daemon=True, name=f"debug_replay_{job.id}")
+ self.job = job
+ self.source = source
+ self.frigate_config = frigate_config
+ self.config_publisher = config_publisher
+ self.replay_manager = replay_manager
+ self.publisher = publisher if publisher is not None else JobStatePublisher()
+ self._cancel_event = threading.Event()
+ self._active_process: sp.Popen | None = None
+ self._proc_lock = threading.Lock()
+ self._last_broadcast_monotonic: float = 0.0
+
+ def cancel(self) -> None:
+ """Request cancellation. Idempotent."""
+ self._cancel_event.set()
+ with self._proc_lock:
+ proc = self._active_process
+ if proc is not None:
+ try:
+ proc.terminate()
+ except Exception as exc:
+ logger.warning("Failed to terminate ffmpeg subprocess: %s", exc)
+
+ def is_cancelled(self) -> bool:
+ return self._cancel_event.is_set()
+
+ def _record_proc(self, proc: sp.Popen) -> None:
+ with self._proc_lock:
+ self._active_process = proc
+ # Race: cancel arrived between Popen and _record_proc.
+ if self._cancel_event.is_set():
+ try:
+ proc.terminate()
+ except Exception:
+ pass
+
+ def _broadcast(self, force: bool = False) -> None:
+ now = time.monotonic()
+ if (
+ not force
+ and now - self._last_broadcast_monotonic < PROGRESS_BROADCAST_MIN_INTERVAL
+ ):
+ return
+ self._last_broadcast_monotonic = now
+
+ try:
+ self.publisher.publish(self.job.to_dict())
+ except Exception as err:
+ logger.warning("Publisher raised during job state broadcast: %s", err)
+
+ def run(self) -> None:
+ replay_name = self.job.replay_camera_name
+ os.makedirs(REPLAY_DIR, exist_ok=True)
+ clip_path = os.path.join(REPLAY_DIR, f"{replay_name}.mp4")
+
+ self.job.status = JobStatusTypesEnum.running
+ self.job.start_time = time.time()
+ self.job.current_step = STEP_PREPARING_CLIP
+ self._broadcast(force=True)
+
+ try:
+ input_args = self.source.ffmpeg_input_args(REPLAY_DIR)
+
+ ffmpeg_cmd = [
+ self.frigate_config.ffmpeg.ffmpeg_path,
+ "-hide_banner",
+ "-y",
+ *input_args,
+ "-c",
+ "copy",
+ "-movflags",
+ "+faststart",
+ clip_path,
+ ]
+
+ logger.info(
+ "Generating replay clip for %s (%.1f - %.1f)",
+ self.job.source_camera,
+ self.job.start_ts,
+ self.job.end_ts,
+ )
+
+ def _on_progress(percent: float) -> None:
+ self.job.progress_percent = percent
+ self._broadcast()
+
+ try:
+ returncode, stderr = run_ffmpeg_with_progress(
+ ffmpeg_cmd,
+ expected_duration_seconds=max(
+ 0.0, self.job.end_ts - self.job.start_ts
+ ),
+ on_progress=_on_progress,
+ process_started=self._record_proc,
+ use_low_priority=True,
+ )
+ finally:
+ with self._proc_lock:
+ self._active_process = None
+
+ if self._cancel_event.is_set():
+ self._finalize_cancelled(clip_path)
+ return
+
+ if returncode != 0:
+ raise RuntimeError(f"FFmpeg failed: {stderr[-500:]}")
+
+ if not os.path.exists(clip_path):
+ raise RuntimeError("Clip file was not created")
+
+ self.job.current_step = STEP_STARTING_CAMERA
+ self.job.progress_percent = 100.0
+ self._broadcast(force=True)
+
+ if self._cancel_event.is_set():
+ self._finalize_cancelled(clip_path)
+ return
+
+ self.replay_manager.publish_camera(
+ source_camera=self.job.source_camera,
+ replay_name=replay_name,
+ clip_path=clip_path,
+ frigate_config=self.frigate_config,
+ config_publisher=self.config_publisher,
+ )
+ self.replay_manager.mark_session_ready(clip_path)
+
+ self.job.status = JobStatusTypesEnum.success
+ self.job.end_time = time.time()
+ self._broadcast(force=True)
+ logger.info(
+ "Debug replay started: %s -> %s",
+ self.job.source_camera,
+ replay_name,
+ )
+ except Exception as exc:
+ logger.exception("Debug replay startup failed")
+ self.job.status = JobStatusTypesEnum.failed
+ self.job.error_message = str(exc)
+ self.job.end_time = time.time()
+ self._broadcast(force=True)
+ self.replay_manager.clear_session()
+ _remove_silent(clip_path)
+ finally:
+ self.source.cleanup(REPLAY_DIR)
+ _set_active_runner(None)
+
+ def _finalize_cancelled(self, clip_path: str) -> None:
+ logger.info("Debug replay startup cancelled")
+ self.job.status = JobStatusTypesEnum.cancelled
+ self.job.end_time = time.time()
+ self._broadcast(force=True)
+ # The caller of cancel_debug_replay_job (DebugReplayManager.stop) owns
+ # session cleanup — db rows, filesystem artifacts, clear_session. We
+ # only clean up the partial concat output we created.
+ _remove_silent(clip_path)
+
+
+def _remove_silent(path: str) -> None:
+ try:
+ if os.path.exists(path):
+ os.remove(path)
+ except OSError:
+ pass
+
+
+def start_debug_replay_job(
+ *,
+ source: DebugReplaySource,
+ frigate_config: FrigateConfig,
+ config_publisher: CameraConfigUpdatePublisher,
+ replay_manager: "DebugReplayManager",
+) -> str:
+ """Validate, create job, start runner. Returns the job id.
+
+ Raises ValueError for an invalid source (camera missing, source has
+ no usable content) and RuntimeError if a session is already active.
+ """
+ if job_is_running(JOB_TYPE) or replay_manager.active:
+ raise RuntimeError("A replay session is already active")
+
+ if source.source_camera not in frigate_config.cameras:
+ raise ValueError(f"Camera '{source.source_camera}' not found")
+
+ source.validate()
+
+ replay_name = f"{REPLAY_CAMERA_PREFIX}{source.source_camera}"
+ replay_manager.mark_starting(
+ source_camera=source.source_camera,
+ replay_camera_name=replay_name,
+ start_ts=source.start_ts,
+ end_ts=source.end_ts,
+ )
+
+ job = DebugReplayJob(
+ source_camera=source.source_camera,
+ replay_camera_name=replay_name,
+ start_ts=source.start_ts,
+ end_ts=source.end_ts,
+ )
+ set_current_job(job)
+
+ runner = DebugReplayJobRunner(
+ job=job,
+ source=source,
+ frigate_config=frigate_config,
+ config_publisher=config_publisher,
+ replay_manager=replay_manager,
+ )
+ _set_active_runner(runner)
+ runner.start()
+
+ return job.id
+
+
+def cancel_debug_replay_job() -> bool:
+ """Signal the active runner to cancel.
+
+ Returns True if a runner was signalled, False if no job was active.
+ """
+ runner = get_active_runner()
+ if runner is None:
+ return False
+ runner.cancel()
+ return True
+
+
+def wait_for_runner(timeout: float = 2.0) -> bool:
+ """Join the active runner. Returns True if the runner ended in time."""
+ runner = get_active_runner()
+ if runner is None:
+ return True
+ runner.join(timeout=timeout)
+ return not runner.is_alive()
diff --git a/frigate/jobs/export.py b/frigate/jobs/export.py
index a74b91713e..1c88ec5849 100644
--- a/frigate/jobs/export.py
+++ b/frigate/jobs/export.py
@@ -4,15 +4,17 @@ import logging
import os
import threading
import time
+from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
from queue import Full, Queue
-from typing import Any, Callable, Optional
+from typing import Any
from peewee import DoesNotExist
from frigate.comms.inter_process import InterProcessRequestor
from frigate.config import FrigateConfig
+from frigate.config.camera.record import ChaptersEnum
from frigate.const import UPDATE_JOB_STATE
from frigate.jobs.job import Job
from frigate.models import Export
@@ -46,15 +48,16 @@ class ExportJob(Job):
job_type: str = "export"
camera: str = ""
- name: Optional[str] = None
- image_path: Optional[str] = None
- export_case_id: Optional[str] = None
+ name: str | None = None
+ image_path: str | None = None
+ export_case_id: str | None = None
request_start_time: float = 0.0
request_end_time: float = 0.0
playback_source: str = PlaybackSourceEnum.recordings.value
- ffmpeg_input_args: Optional[str] = None
- ffmpeg_output_args: Optional[str] = None
+ ffmpeg_input_args: str | None = None
+ ffmpeg_output_args: str | None = None
cpu_fallback: bool = False
+ chapters: ChaptersEnum | None = None
current_step: str = "queued"
progress_percent: float = 0.0
@@ -147,7 +150,7 @@ class ExportJobManager:
config: FrigateConfig,
max_concurrent: int,
max_queued: int = MAX_QUEUED_EXPORT_JOBS,
- publisher: Optional[JobStatePublisher] = None,
+ publisher: JobStatePublisher | None = None,
) -> None:
self.config = config
self.max_concurrent = max(1, max_concurrent)
@@ -266,7 +269,7 @@ class ExportJobManager:
return job.id
- def get_job(self, job_id: str) -> Optional[ExportJob]:
+ def get_job(self, job_id: str) -> ExportJob | None:
"""Get a job by ID."""
with self.lock:
return self.jobs.get(job_id)
@@ -343,6 +346,7 @@ class ExportJobManager:
job.ffmpeg_input_args,
job.ffmpeg_output_args,
job.cpu_fallback,
+ job.chapters,
on_progress=self._make_progress_callback(job),
)
@@ -376,7 +380,7 @@ class ExportJobManager:
self._schedule_job_cleanup(job.id)
-_job_manager: Optional[ExportJobManager] = None
+_job_manager: ExportJobManager | None = None
_job_manager_lock = threading.Lock()
@@ -482,7 +486,7 @@ def start_export_job(config: FrigateConfig, job: ExportJob) -> str:
return get_export_job_manager(config).enqueue(job)
-def get_export_job(config: FrigateConfig, job_id: str) -> Optional[ExportJob]:
+def get_export_job(config: FrigateConfig, job_id: str) -> ExportJob | None:
"""Get a queued or completed export job by ID."""
return get_export_job_manager(config).get_job(job_id)
diff --git a/frigate/jobs/job.py b/frigate/jobs/job.py
index a445eebf53..c40087d0cd 100644
--- a/frigate/jobs/job.py
+++ b/frigate/jobs/job.py
@@ -1,7 +1,7 @@
"""Generic base class for long-running background jobs."""
from dataclasses import asdict, dataclass, field
-from typing import Any, Optional
+from typing import Any
@dataclass
@@ -11,10 +11,10 @@ class Job:
id: str = field(default_factory=lambda: __import__("uuid").uuid4().__str__()[:12])
job_type: str = "" # Must be set by subclasses
status: str = "queued" # queued, running, success, failed, cancelled
- results: Optional[dict[str, Any]] = None
- start_time: Optional[float] = None
- end_time: Optional[float] = None
- error_message: Optional[str] = None
+ results: dict[str, Any] | None = None
+ start_time: float | None = None
+ end_time: float | None = None
+ error_message: str | None = None
def to_dict(self) -> dict[str, Any]:
"""Convert to dictionary for WebSocket transmission."""
diff --git a/frigate/jobs/manager.py b/frigate/jobs/manager.py
index 8aa77b3c7a..c2fb44af38 100644
--- a/frigate/jobs/manager.py
+++ b/frigate/jobs/manager.py
@@ -1,14 +1,13 @@
"""Generic job management for long-running background tasks."""
import threading
-from typing import Optional
from frigate.jobs.job import Job
from frigate.types import JobStatusTypesEnum
# Global state and locks for enforcing single concurrent job per job type
_job_locks: dict[str, threading.Lock] = {}
-_current_jobs: dict[str, Optional[Job]] = {}
+_current_jobs: dict[str, Job | None] = {}
# Keep completed jobs for retrieval, keyed by (job_type, job_id)
_completed_jobs: dict[tuple[str, str], Job] = {}
@@ -35,7 +34,7 @@ def set_current_job(job: Job) -> None:
_current_jobs[job.job_type] = job
-def clear_current_job(job_type: str, job_id: Optional[str] = None) -> None:
+def clear_current_job(job_type: str, job_id: str | None = None) -> None:
"""Clear the current job for a given job type, optionally checking the ID."""
lock = _get_lock(job_type)
with lock:
@@ -45,14 +44,14 @@ def clear_current_job(job_type: str, job_id: Optional[str] = None) -> None:
_current_jobs[job_type] = None
-def get_current_job(job_type: str) -> Optional[Job]:
+def get_current_job(job_type: str) -> Job | None:
"""Get the current running/queued job for a given job type, if any."""
lock = _get_lock(job_type)
with lock:
return _current_jobs.get(job_type)
-def get_job_by_id(job_type: str, job_id: str) -> Optional[Job]:
+def get_job_by_id(job_type: str, job_id: str) -> Job | None:
"""Get job by ID. Checks current job first, then completed jobs."""
lock = _get_lock(job_type)
with lock:
diff --git a/frigate/jobs/media_sync.py b/frigate/jobs/media_sync.py
index 4a3fdc3557..1cd8209869 100644
--- a/frigate/jobs/media_sync.py
+++ b/frigate/jobs/media_sync.py
@@ -5,7 +5,7 @@ import os
import threading
from dataclasses import dataclass, field
from datetime import datetime
-from typing import Optional, cast
+from typing import cast
from frigate.comms.inter_process import InterProcessRequestor
from frigate.const import CONFIG_DIR, UPDATE_JOB_STATE
@@ -87,7 +87,7 @@ class MediaSyncRunner(threading.Thread):
self._broadcast_status()
except Exception as e:
- logger.error(f"Media sync job {self.job.id} failed: {e}", exc_info=True)
+ logger.exception(f"Media sync job {self.job.id} failed: {e}")
self.job.status = JobStatusTypesEnum.failed
self.job.error_message = str(e)
self.job.end_time = datetime.now().timestamp()
@@ -110,10 +110,10 @@ class MediaSyncRunner(threading.Thread):
def start_media_sync_job(
dry_run: bool = False,
- media_types: Optional[list[str]] = None,
+ media_types: list[str] | None = None,
force: bool = False,
verbose: bool = False,
-) -> Optional[str]:
+) -> str | None:
"""Start a new media sync job if none is currently running.
Returns job ID on success, None if job already running.
@@ -144,11 +144,11 @@ def start_media_sync_job(
return job.id
-def get_current_media_sync_job() -> Optional[MediaSyncJob]:
+def get_current_media_sync_job() -> MediaSyncJob | None:
"""Get the current running/queued media sync job, if any."""
- return cast(Optional[MediaSyncJob], get_current_job("media_sync"))
+ return cast(MediaSyncJob | None, get_current_job("media_sync"))
-def get_media_sync_job_by_id(job_id: str) -> Optional[MediaSyncJob]:
+def get_media_sync_job_by_id(job_id: str) -> MediaSyncJob | None:
"""Get media sync job by ID. Currently only tracks the current job."""
- return cast(Optional[MediaSyncJob], get_job_by_id("media_sync", job_id))
+ return cast(MediaSyncJob | None, get_job_by_id("media_sync", job_id))
diff --git a/frigate/jobs/motion_search.py b/frigate/jobs/motion_search.py
index 1a90f0bb9e..15cd104f7b 100644
--- a/frigate/jobs/motion_search.py
+++ b/frigate/jobs/motion_search.py
@@ -3,10 +3,12 @@
import logging
import os
import threading
+import time
+from collections.abc import Callable, Generator, Iterable
from concurrent.futures import Future, ThreadPoolExecutor, as_completed
from dataclasses import asdict, dataclass, field
from datetime import datetime
-from typing import Any, Optional, cast
+from typing import Any, cast
import cv2
import numpy as np
@@ -19,6 +21,18 @@ from frigate.jobs.manager import (
get_job_by_id,
set_current_job,
)
+from frigate.jobs.motion_search_batch import (
+ build_segment_time_map,
+ coalesce_runs,
+ stream_time_to_absolute,
+)
+from frigate.jobs.motion_search_decode import (
+ iter_vod_frames,
+ keyframe_sampling_eligible,
+ probe_video_dimensions,
+ probe_vod_keyframe_pts,
+ resolve_motion_decode_args,
+)
from frigate.models import Recordings
from frigate.types import JobStatusTypesEnum
@@ -26,6 +40,18 @@ logger = logging.getLogger(__name__)
# Constants
HEATMAP_GRID_SIZE = 16
+# Max wall-clock span of one VOD run request (seconds). Bounds per-request size
+# and gives streaming/cancel/early-exit granularity.
+MAX_RUN_SECONDS = 600.0
+# Treat segments within this many seconds end-to-start as time-contiguous.
+RUN_GAP_EPSILON = 1.0
+# Longest-side pixels for the ROI downscale before motion detection.
+SCALE_TARGET = 400
+# Minimum wall seconds between intra-run progress broadcasts.
+PROGRESS_BROADCAST_INTERVAL = 1.0
+# Output frame rate for the fixed-cadence fallback used on long-GOP cameras
+# (where keyframe sampling is too sparse). Keyframe cameras ignore this.
+FALLBACK_SAMPLE_FPS = 2.0
@dataclass
@@ -69,15 +95,18 @@ class MotionSearchJob(Job):
polygon_points: list[list[float]] = field(default_factory=list)
threshold: int = 30
min_area: float = 5.0
- frame_skip: int = 5
parallel: bool = False
max_results: int = 25
# Track progress
total_frames_processed: int = 0
+ # Live progress (ride the existing to_dict() websocket broadcast)
+ scanning_timestamp: float | None = None
+ progress: float = 0.0
+
# Metrics for observability
- metrics: Optional[MotionSearchMetrics] = None
+ metrics: MotionSearchMetrics | None = None
def to_dict(self) -> dict[str, Any]:
"""Convert to dictionary for WebSocket transmission."""
@@ -100,6 +129,113 @@ def create_polygon_mask(
return mask
+def compute_roi_crop_and_scale(
+ polygon_points: list[list[float]],
+ frame_width: int,
+ frame_height: int,
+ scale_target: int,
+) -> tuple[tuple[int, int, int, int], tuple[int, int]]:
+ """Compute the ROI crop box and never-upscale scaled dimensions.
+
+ Returns ((crop_w, crop_h, crop_x, crop_y), (scaled_w, scaled_h)) in pixels.
+ The crop is the polygon's bounding box in frame pixels; the scaled size fits
+ the crop's longest side to ``scale_target`` without ever enlarging it.
+ """
+ xs = [p[0] for p in polygon_points]
+ ys = [p[1] for p in polygon_points]
+ # nv12 (4:2:0) hwdownload requires even crop offsets and even crop/scale
+ # dimensions; otherwise ffmpeg rounds the chroma planes and the raw byte
+ # stream stops matching the expected frame size. Force even values, and the
+ # mask is built from these same values so the two stay aligned.
+ crop_x = int(min(xs) * frame_width)
+ crop_y = int(min(ys) * frame_height)
+ crop_x -= crop_x % 2
+ crop_y -= crop_y % 2
+ crop_w = max(2, int(max(xs) * frame_width) - crop_x)
+ crop_h = max(2, int(max(ys) * frame_height) - crop_y)
+ crop_w -= crop_w % 2
+ crop_h -= crop_h % 2
+
+ longest = max(crop_w, crop_h)
+ factor = min(1.0, scale_target / longest)
+ scaled_w = max(2, round(crop_w * factor))
+ scaled_h = max(2, round(crop_h * factor))
+ scaled_w -= scaled_w % 2
+ scaled_h -= scaled_h % 2
+ return (crop_w, crop_h, crop_x, crop_y), (scaled_w, scaled_h)
+
+
+def build_scaled_roi_mask(
+ polygon_points: list[list[float]],
+ frame_width: int,
+ frame_height: int,
+ crop: tuple[int, int, int, int],
+ scaled: tuple[int, int],
+) -> np.ndarray:
+ """Rasterize the polygon mask at the scaled ROI size.
+
+ Builds the full-resolution mask, crops it to the ROI box, and nearest-
+ neighbor resizes it to the scaled dimensions so it lines up exactly with the
+ frames ffmpeg crops and scales.
+ """
+ crop_w, crop_h, crop_x, crop_y = crop
+ scaled_w, scaled_h = scaled
+ full_mask = create_polygon_mask(polygon_points, frame_width, frame_height)
+ cropped = full_mask[crop_y : crop_y + crop_h, crop_x : crop_x + crop_w]
+ return cv2.resize(cropped, (scaled_w, scaled_h), interpolation=cv2.INTER_NEAREST)
+
+
+def detect_motion_scaled(
+ frames: Iterable[tuple[int, np.ndarray]],
+ mask: np.ndarray,
+ threshold: int,
+ min_area: float,
+ timestamp_fn: Callable[[int], float],
+) -> list[MotionSearchResult]:
+ """Detect motion across pre-cropped, pre-scaled gray frames.
+
+ ``frames`` yields (absolute_frame_index, gray_roi_frame); ``mask`` is the
+ scaled ROI mask. ``min_area`` is a percentage of the masked ROI. Mirrors the
+ full-res detection math (absdiff -> blur -> threshold -> dilate -> contours)
+ on the already-reduced frames.
+ """
+ results: list[MotionSearchResult] = []
+ mask_area = np.count_nonzero(mask)
+ if mask_area == 0:
+ return results
+ min_area_pixels = int((min_area / 100.0) * mask_area)
+
+ prev: np.ndarray | None = None
+ for frame_idx, gray in frames:
+ masked = cv2.bitwise_and(gray, gray, mask=mask)
+ if prev is not None:
+ diff = cv2.absdiff(prev, masked)
+ diff_blurred = cv2.GaussianBlur(diff, (3, 3), 0)
+ _, thresh = cv2.threshold(diff_blurred, threshold, 255, cv2.THRESH_BINARY)
+ thresh_dilated = cv2.dilate(thresh, None, iterations=1) # type: ignore[call-overload]
+ thresh_masked = cv2.bitwise_and(thresh_dilated, thresh_dilated, mask=mask)
+ change_pixels = cv2.countNonZero(thresh_masked)
+ if change_pixels > min_area_pixels:
+ contours, _ = cv2.findContours(
+ thresh_masked, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
+ )
+ total_change_area = sum(
+ cv2.contourArea(c)
+ for c in contours
+ if cv2.contourArea(c) >= min_area_pixels
+ )
+ if total_change_area > 0:
+ change_percentage = (total_change_area / mask_area) * 100
+ results.append(
+ MotionSearchResult(
+ timestamp=timestamp_fn(frame_idx),
+ change_percentage=round(change_percentage, 2),
+ )
+ )
+ prev = masked
+ return results
+
+
def compute_roi_bbox_normalized(
polygon_points: list[list[float]],
) -> tuple[float, float, float, float]:
@@ -184,6 +320,22 @@ def segment_passes_heatmap_gate(
return heatmap_overlaps_roi(heatmap, roi_bbox)
+def resolve_internal_port(config: FrigateConfig) -> int:
+ """Return the unauthenticated internal nginx port for VOD requests."""
+ listen = config.networking.listen.internal
+ if isinstance(listen, str):
+ return int(listen.split(":")[-1])
+ return int(listen)
+
+
+def build_vod_url(internal_port: int, camera: str, start: float, end: float) -> str:
+ """Build the internal VOD HLS URL for a camera time range."""
+ return (
+ f"http://127.0.0.1:{internal_port}/vod/{camera}"
+ f"/start/{start}/end/{end}/index.m3u8"
+ )
+
+
class MotionSearchRunner(threading.Thread):
"""Thread-based runner for motion search jobs with parallel verification."""
@@ -206,6 +358,23 @@ class MotionSearchRunner(threading.Thread):
cpu_count = os.cpu_count() or 1
self.max_workers = min(4, cpu_count)
+ # Resolved once per job in _execute_search
+ self.ffmpeg_path: str = "ffmpeg"
+ self.ffprobe_path: str = "ffprobe"
+ self.decode_args: list[str] = []
+ # Keyframe sampling decision, decided once per job from the first run's
+ # GOP. The fallback cadence is a fixed rate (see FALLBACK_SAMPLE_FPS).
+ self.use_keyframe: bool = True
+ self.fps_rate: float = FALLBACK_SAMPLE_FPS
+ # ROI crop/scale + scaled mask, computed once from the VOD-stream
+ # dimensions (which can differ from the detect resolution).
+ self.crop: tuple[int, int, int, int] = (0, 0, 0, 0)
+ self.scaled: tuple[int, int] = (0, 0)
+ self.scaled_mask: np.ndarray = np.zeros((0, 0), dtype=np.uint8)
+ self.channels: int = 1
+ self.internal_port: int = 5000
+ self._last_progress_broadcast: float = 0.0
+
def run(self) -> None:
"""Execute the motion search job."""
try:
@@ -281,6 +450,9 @@ class MotionSearchRunner(threading.Thread):
if frame_width is None or frame_height is None:
raise ValueError(f"Camera {camera_name} detect dimensions not configured")
+ self.ffmpeg_path = camera_config.ffmpeg.ffmpeg_path
+ self.ffprobe_path = camera_config.ffmpeg.ffprobe_path
+
# Create polygon mask
polygon_mask = create_polygon_mask(
self.job.polygon_points, frame_width, frame_height
@@ -384,205 +556,274 @@ class MotionSearchRunner(threading.Thread):
self.metrics.heatmap_roi_skip_segments,
)
- if self.job.parallel:
- return self._search_motion_parallel(filtered_recordings, polygon_mask)
+ # Resolve decode backend (allowlisted hwaccel or software), coalesce the
+ # gate-passing segments into time-contiguous runs, and probe the first
+ # run's VOD stream once for dimensions + keyframe layout. VOD output is
+ # what we decode, so crop/scale/mask are computed against it.
+ self.internal_port = resolve_internal_port(self.config)
+ self.decode_args = resolve_motion_decode_args(camera_config)
+ ffprobe_path = self.ffprobe_path
- return self._search_motion_sequential(filtered_recordings, polygon_mask)
+ runs = coalesce_runs(filtered_recordings, MAX_RUN_SECONDS, RUN_GAP_EPSILON)
+ if not runs:
+ return []
- def _search_motion_parallel(
- self,
- recordings: list[Recordings],
- polygon_mask: np.ndarray,
- ) -> list[MotionSearchResult]:
- """Search for motion in parallel across segments, streaming results."""
- all_results: list[MotionSearchResult] = []
- total_frames = 0
- next_recording_idx_to_merge = 0
+ first_run = runs[0]
+ first_url = build_vod_url(
+ self.internal_port,
+ camera_name,
+ float(first_run[0].start_time),
+ float(first_run[-1].end_time),
+ )
+ dims = probe_video_dimensions(ffprobe_path, first_url)
+ if dims is None:
+ raise ValueError(f"Could not probe VOD dimensions for camera {camera_name}")
+ rec_width, rec_height, _rec_fps = dims
+
+ self.crop, self.scaled = compute_roi_crop_and_scale(
+ self.job.polygon_points, rec_width, rec_height, SCALE_TARGET
+ )
+ self.scaled_mask = build_scaled_roi_mask(
+ self.job.polygon_points, rec_width, rec_height, self.crop, self.scaled
+ )
+ self.channels = 1 # always gray output
+
+ # Decide keyframe vs fixed-cadence sampling once from the first run's GOP
+ # (keyframe structure is a per-camera constant).
+ first_pts = probe_vod_keyframe_pts(ffprobe_path, first_url)
+ self.use_keyframe = keyframe_sampling_eligible(first_pts)
logger.debug(
- "Motion search job %s: starting motion search with %d workers "
- "across %d segments",
+ "Motion search job %s: %d runs, sampling=%s, hwaccel=%s, vod=%dx%d",
self.job.id,
- self.max_workers,
- len(recordings),
+ len(runs),
+ "keyframe" if self.use_keyframe else "cadence",
+ bool(self.decode_args),
+ rec_width,
+ rec_height,
)
- # Initialize partial results on the job so they stream to the frontend
+ return self._search_runs(runs)
+
+ def _emit_progress(self, abs_ts: float) -> None:
+ """Throttled intra-run progress broadcast (scanning cursor)."""
+ now = time.monotonic()
+ if now - self._last_progress_broadcast < PROGRESS_BROADCAST_INTERVAL:
+ return
+ self._last_progress_broadcast = now
+ self.job.scanning_timestamp = abs_ts
+ self._broadcast_status()
+
+ def _detect_with_progress(
+ self,
+ indexed_frames: list[tuple[int, np.ndarray]],
+ timestamp_fn: Callable[[int], float],
+ ) -> list[MotionSearchResult]:
+ """Run detection while firing throttled progress as frames are scanned."""
+
+ def _gen() -> Generator[tuple[int, np.ndarray], None, None]:
+ for i, frame in indexed_frames:
+ if not self._should_stop():
+ self._emit_progress(timestamp_fn(i))
+ yield i, frame
+
+ return detect_motion_scaled(
+ _gen(),
+ self.scaled_mask,
+ self.job.threshold,
+ self.job.min_area,
+ timestamp_fn,
+ )
+
+ def _process_run(
+ self, run: list[Recordings]
+ ) -> tuple[list[MotionSearchResult], int]:
+ """Decode one run's VOD stream and detect motion.
+
+ Keyframe mode compares every decoded keyframe (free recall, since they
+ are all decoded anyway) paired with its probed PTS; if the decoded and
+ probed counts disagree (the decoder ignored ``-skip_frame nokey`` or the
+ stream is corrupt) this run re-runs in the fixed-cadence fallback.
+ Returns ``(results, frame_count)``.
+ """
+ run_start: float = run[0].start_time # type: ignore[assignment]
+ run_end: float = run[-1].end_time # type: ignore[assignment]
+ vod_url = build_vod_url(self.internal_port, self.job.camera, run_start, run_end)
+ time_map = build_segment_time_map(run)
+
+ if self.use_keyframe:
+ kf_pts = probe_vod_keyframe_pts(self.ffprobe_path, vod_url)
+ frames = list(
+ iter_vod_frames(
+ self.ffmpeg_path,
+ vod_url,
+ self.scaled[0],
+ self.scaled[1],
+ self.channels,
+ self.decode_args,
+ self.crop,
+ self.scaled,
+ True,
+ self._should_stop,
+ skip_nonkey=True,
+ fps_rate=None,
+ )
+ )
+ if kf_pts and len(frames) == len(kf_pts):
+ abs_times = [stream_time_to_absolute(time_map, p) for p in kf_pts]
+ indexed = list(enumerate(frames))
+
+ def _ts_kf(i: int) -> float:
+ return abs_times[i]
+
+ results = self._detect_with_progress(indexed, _ts_kf)
+ return results, len(frames)
+
+ logger.debug(
+ "Keyframe count mismatch (%d decoded vs %d probed), using cadence",
+ len(frames),
+ len(kf_pts),
+ )
+
+ return self._process_run_cadence(vod_url, time_map)
+
+ def _process_run_cadence(
+ self, vod_url: str, time_map: list[tuple[float, float, float]]
+ ) -> tuple[list[MotionSearchResult], int]:
+ """Fixed-cadence fallback: fps-filtered VOD decode, evenly spaced times."""
+ frames = list(
+ iter_vod_frames(
+ self.ffmpeg_path,
+ vod_url,
+ self.scaled[0],
+ self.scaled[1],
+ self.channels,
+ self.decode_args,
+ self.crop,
+ self.scaled,
+ True,
+ self._should_stop,
+ skip_nonkey=False,
+ fps_rate=self.fps_rate,
+ )
+ )
+ indexed = list(enumerate(frames))
+
+ def _ts_fps(i: int) -> float:
+ return stream_time_to_absolute(time_map, i / self.fps_rate)
+
+ results = self._detect_with_progress(indexed, _ts_fps)
+ return results, len(frames)
+
+ def _merge_run(
+ self,
+ run: list[Recordings],
+ run_results: list[MotionSearchResult],
+ frames: int,
+ state: dict[str, Any],
+ ) -> bool:
+ """Fold one run's output into the running results; stream + dedup.
+
+ Returns True once ``max_results`` deduped hits have accumulated.
+ """
+ state["completed_runs"] += 1
+ state["all_results"].extend(run_results)
+ state["total_frames"] += frames
+ self.job.total_frames_processed = state["total_frames"]
+ self.metrics.frames_decoded = state["total_frames"]
+ self.metrics.segments_processed += len(run)
+ self.job.progress = state["completed_runs"] / state["total_runs"]
+
+ state["all_results"].sort(key=lambda r: r.timestamp)
+ deduped = self._deduplicate_results(state["all_results"])[
+ : self.job.max_results
+ ]
+ self.job.results = {
+ "results": [r.to_dict() for r in deduped],
+ "total_frames_processed": state["total_frames"],
+ }
+ self._broadcast_status()
+ return len(deduped) >= self.job.max_results
+
+ def _search_runs(self, runs: list[list[Recordings]]) -> list[MotionSearchResult]:
+ """Decode runs (parallel pool when enabled), merge in order, stream."""
+ state: dict[str, Any] = {
+ "all_results": [],
+ "total_frames": 0,
+ "completed_runs": 0,
+ "total_runs": len(runs),
+ }
self.job.results = {"results": [], "total_frames_processed": 0}
- with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
- futures: dict[Future, int] = {}
- completed_segments: dict[int, tuple[list[MotionSearchResult], int]] = {}
+ logger.debug(
+ "Motion search job %s: searching %d runs (parallel=%s, workers=%d)",
+ self.job.id,
+ len(runs),
+ self.job.parallel,
+ self.max_workers,
+ )
- for idx, recording in enumerate(recordings):
- if self._should_stop():
- break
+ if self.job.parallel and len(runs) > 1:
+ with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
+ futures: dict[Future, int] = {}
+ for idx, run in enumerate(runs):
+ if self._should_stop():
+ break
+ futures[executor.submit(self._process_run, run)] = idx
- rec_start: float = recording.start_time # type: ignore[assignment]
- rec_end: float = recording.end_time # type: ignore[assignment]
- future = executor.submit(
- self._process_recording_for_motion,
- str(recording.path),
- rec_start,
- rec_end,
- self.job.start_time_range,
- self.job.end_time_range,
- polygon_mask,
- self.job.threshold,
- self.job.min_area,
- self.job.frame_skip,
- )
- futures[future] = idx
+ completed: dict[int, tuple[list[MotionSearchResult], int]] = {}
+ next_idx = 0
+ for future in as_completed(futures):
+ if self._should_stop():
+ break
+ run_idx = futures[future]
+ try:
+ completed[run_idx] = future.result()
+ except Exception as e:
+ self.metrics.segments_with_errors += 1
+ logger.warning("Error processing run %d: %s", run_idx, e)
+ completed[run_idx] = ([], 0)
- for future in as_completed(futures):
- if self._should_stop():
- # Cancel remaining futures
- for f in futures:
- f.cancel()
- break
-
- recording_idx = futures[future]
- recording = recordings[recording_idx]
-
- try:
- results, frames = future.result()
- self.metrics.segments_processed += 1
- completed_segments[recording_idx] = (results, frames)
-
- while next_recording_idx_to_merge in completed_segments:
- segment_results, segment_frames = completed_segments.pop(
- next_recording_idx_to_merge
- )
-
- all_results.extend(segment_results)
- total_frames += segment_frames
- self.job.total_frames_processed = total_frames
- self.metrics.frames_decoded = total_frames
-
- if segment_results:
- deduped = self._deduplicate_results(all_results)
- self.job.results = {
- "results": [
- r.to_dict() for r in deduped[: self.job.max_results]
- ],
- "total_frames_processed": total_frames,
- }
-
- self._broadcast_status()
-
- if segment_results and len(deduped) >= self.job.max_results:
+ while next_idx in completed:
+ run_results, frames = completed.pop(next_idx)
+ if self._merge_run(runs[next_idx], run_results, frames, state):
self.internal_stop_event.set()
- for pending_future in futures:
- pending_future.cancel()
+ for pending in futures:
+ pending.cancel()
break
-
- next_recording_idx_to_merge += 1
+ next_idx += 1
if self.internal_stop_event.is_set():
break
-
+ else:
+ for run in runs:
+ if self._should_stop():
+ break
+ try:
+ run_results, frames = self._process_run(run)
except Exception as e:
- self.metrics.segments_processed += 1
self.metrics.segments_with_errors += 1
+ self.metrics.segments_processed += len(run)
self._broadcast_status()
- logger.warning(
- "Error processing segment %s: %s",
- recording.path,
- e,
- )
-
- self.job.total_frames_processed = total_frames
- self.metrics.frames_decoded = total_frames
-
- logger.debug(
- "Motion search job %s: motion search complete, "
- "found %d raw results, decoded %d frames, %d segment errors",
- self.job.id,
- len(all_results),
- total_frames,
- self.metrics.segments_with_errors,
- )
-
- # Sort and deduplicate results
- all_results.sort(key=lambda x: x.timestamp)
- return self._deduplicate_results(all_results)[: self.job.max_results]
-
- def _search_motion_sequential(
- self,
- recordings: list[Recordings],
- polygon_mask: np.ndarray,
- ) -> list[MotionSearchResult]:
- """Search for motion sequentially across segments, streaming results."""
- all_results: list[MotionSearchResult] = []
- total_frames = 0
-
- logger.debug(
- "Motion search job %s: starting sequential motion search across %d segments",
- self.job.id,
- len(recordings),
- )
-
- self.job.results = {"results": [], "total_frames_processed": 0}
-
- for recording in recordings:
- if self.cancel_event.is_set():
- break
-
- try:
- rec_start: float = recording.start_time # type: ignore[assignment]
- rec_end: float = recording.end_time # type: ignore[assignment]
- results, frames = self._process_recording_for_motion(
- str(recording.path),
- rec_start,
- rec_end,
- self.job.start_time_range,
- self.job.end_time_range,
- polygon_mask,
- self.job.threshold,
- self.job.min_area,
- self.job.frame_skip,
- )
- all_results.extend(results)
- total_frames += frames
-
- self.job.total_frames_processed = total_frames
- self.metrics.frames_decoded = total_frames
- self.metrics.segments_processed += 1
-
- if results:
- all_results.sort(key=lambda x: x.timestamp)
- deduped = self._deduplicate_results(all_results)[
- : self.job.max_results
- ]
- self.job.results = {
- "results": [r.to_dict() for r in deduped],
- "total_frames_processed": total_frames,
- }
-
- self._broadcast_status()
-
- if results and len(deduped) >= self.job.max_results:
+ logger.warning("Error processing run: %s", e)
+ continue
+ if self._merge_run(run, run_results, frames, state):
break
- except Exception as e:
- self.metrics.segments_processed += 1
- self.metrics.segments_with_errors += 1
- self._broadcast_status()
- logger.warning("Error processing segment %s: %s", recording.path, e)
-
- self.job.total_frames_processed = total_frames
- self.metrics.frames_decoded = total_frames
+ all_results: list[MotionSearchResult] = state["all_results"]
+ self.job.total_frames_processed = state["total_frames"]
+ self.metrics.frames_decoded = state["total_frames"]
+ self.job.progress = 1.0
logger.debug(
- "Motion search job %s: sequential motion search complete, "
- "found %d raw results, decoded %d frames, %d segment errors",
+ "Motion search job %s: complete, %d raw results, %d frames, %d errors",
self.job.id,
len(all_results),
- total_frames,
+ state["total_frames"],
self.metrics.segments_with_errors,
)
- all_results.sort(key=lambda x: x.timestamp)
+ all_results.sort(key=lambda r: r.timestamp)
return self._deduplicate_results(all_results)[: self.job.max_results]
def _deduplicate_results(
@@ -602,160 +843,6 @@ class MotionSearchRunner(threading.Thread):
return deduplicated
- def _process_recording_for_motion(
- self,
- recording_path: str,
- recording_start: float,
- recording_end: float,
- search_start: float,
- search_end: float,
- polygon_mask: np.ndarray,
- threshold: int,
- min_area: float,
- frame_skip: int,
- ) -> tuple[list[MotionSearchResult], int]:
- """Process a single recording file for motion detection.
-
- This method is designed to be called from a thread pool.
-
- Args:
- min_area: Minimum change area as a percentage of the ROI (0-100).
- """
- results: list[MotionSearchResult] = []
- frames_processed = 0
-
- if not os.path.exists(recording_path):
- logger.warning("Recording file not found: %s", recording_path)
- return results, frames_processed
-
- cap = cv2.VideoCapture(recording_path)
- if not cap.isOpened():
- logger.error("Could not open recording: %s", recording_path)
- return results, frames_processed
-
- try:
- fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
- total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
- recording_duration = recording_end - recording_start
-
- # Calculate frame range
- start_offset = max(0, search_start - recording_start)
- end_offset = min(recording_duration, search_end - recording_start)
- start_frame = int(start_offset * fps)
- end_frame = int(end_offset * fps)
- start_frame = max(0, min(start_frame, total_frames - 1))
- end_frame = max(0, min(end_frame, total_frames))
-
- if start_frame >= end_frame:
- return results, frames_processed
-
- cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
-
- # Get ROI bounding box
- roi_bbox = cv2.boundingRect(polygon_mask)
- roi_x, roi_y, roi_w, roi_h = roi_bbox
-
- prev_frame_gray = None
- frame_step = max(frame_skip, 1)
- frame_idx = start_frame
-
- while frame_idx < end_frame:
- if self._should_stop():
- break
-
- ret, frame = cap.read()
- if not ret:
- frame_idx += 1
- continue
-
- if (frame_idx - start_frame) % frame_step != 0:
- frame_idx += 1
- continue
-
- frames_processed += 1
-
- gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
-
- # Handle frame dimension changes
- if gray.shape != polygon_mask.shape:
- resized_mask = cv2.resize(
- polygon_mask,
- (gray.shape[1], gray.shape[0]),
- interpolation=cv2.INTER_NEAREST,
- )
- current_bbox = cv2.boundingRect(resized_mask)
- else:
- resized_mask = polygon_mask
- current_bbox = roi_bbox
-
- roi_x, roi_y, roi_w, roi_h = current_bbox
- cropped_gray = gray[roi_y : roi_y + roi_h, roi_x : roi_x + roi_w]
- cropped_mask = resized_mask[
- roi_y : roi_y + roi_h, roi_x : roi_x + roi_w
- ]
-
- cropped_mask_area = np.count_nonzero(cropped_mask)
- if cropped_mask_area == 0:
- frame_idx += 1
- continue
-
- # Convert percentage to pixel count for this ROI
- min_area_pixels = int((min_area / 100.0) * cropped_mask_area)
-
- masked_gray = cv2.bitwise_and(
- cropped_gray, cropped_gray, mask=cropped_mask
- )
-
- if prev_frame_gray is not None:
- diff = cv2.absdiff(prev_frame_gray, masked_gray) # type: ignore[unreachable]
- diff_blurred = cv2.GaussianBlur(diff, (3, 3), 0)
- _, thresh = cv2.threshold(
- diff_blurred, threshold, 255, cv2.THRESH_BINARY
- )
- thresh_dilated = cv2.dilate(thresh, None, iterations=1)
- thresh_masked = cv2.bitwise_and(
- thresh_dilated, thresh_dilated, mask=cropped_mask
- )
-
- change_pixels = cv2.countNonZero(thresh_masked)
- if change_pixels > min_area_pixels:
- contours, _ = cv2.findContours(
- thresh_masked, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
- )
- total_change_area = sum(
- cv2.contourArea(c)
- for c in contours
- if cv2.contourArea(c) >= min_area_pixels
- )
- if total_change_area > 0:
- frame_time_offset = (frame_idx - start_frame) / fps
- timestamp = (
- recording_start + start_offset + frame_time_offset
- )
- change_percentage = (
- total_change_area / cropped_mask_area
- ) * 100
- results.append(
- MotionSearchResult(
- timestamp=timestamp,
- change_percentage=round(change_percentage, 2),
- )
- )
-
- prev_frame_gray = masked_gray
- frame_idx += 1
-
- finally:
- cap.release()
-
- logger.debug(
- "Motion search segment complete: %s, %d frames processed, %d results found",
- recording_path,
- frames_processed,
- len(results),
- )
- return results, frames_processed
-
# Module-level state for managing per-camera jobs
_motion_search_jobs: dict[str, tuple[MotionSearchJob, threading.Event]] = {}
@@ -779,7 +866,6 @@ def start_motion_search_job(
polygon_points: list[list[float]],
threshold: int = 30,
min_area: float = 5.0,
- frame_skip: int = 5,
parallel: bool = False,
max_results: int = 25,
) -> str:
@@ -794,7 +880,6 @@ def start_motion_search_job(
polygon_points=polygon_points,
threshold=threshold,
min_area=min_area,
- frame_skip=frame_skip,
parallel=parallel,
max_results=max_results,
)
@@ -812,14 +897,13 @@ def start_motion_search_job(
logger.debug(
"Started motion search job %s for camera %s: "
"time_range=%.1f-%.1f, threshold=%d, min_area=%.1f%%, "
- "frame_skip=%d, parallel=%s, max_results=%d, polygon_points=%d vertices",
+ "parallel=%s, max_results=%d, polygon_points=%d vertices",
job.id,
camera_name,
start_time,
end_time,
threshold,
min_area,
- frame_skip,
parallel,
max_results,
len(polygon_points),
@@ -827,14 +911,14 @@ def start_motion_search_job(
return job.id
-def get_motion_search_job(job_id: str) -> Optional[MotionSearchJob]:
+def get_motion_search_job(job_id: str) -> MotionSearchJob | None:
"""Get a motion search job by ID."""
with _jobs_lock:
job_entry = _motion_search_jobs.get(job_id)
if job_entry:
return job_entry[0]
# Check completed jobs via manager
- return cast(Optional[MotionSearchJob], get_job_by_id("motion_search", job_id))
+ return cast(MotionSearchJob | None, get_job_by_id("motion_search", job_id))
def cancel_motion_search_job(job_id: str) -> bool:
@@ -858,7 +942,7 @@ def cancel_motion_search_job(job_id: str) -> bool:
job_payload = job.to_dict()
logger.info("Cancelled motion search job %s", job_id)
- requestor: Optional[InterProcessRequestor] = None
+ requestor: InterProcessRequestor | None = None
try:
requestor = InterProcessRequestor()
requestor.send_data(UPDATE_JOB_STATE, job_payload)
diff --git a/frigate/jobs/motion_search_batch.py b/frigate/jobs/motion_search_batch.py
new file mode 100644
index 0000000000..f916da5e0a
--- /dev/null
+++ b/frigate/jobs/motion_search_batch.py
@@ -0,0 +1,75 @@
+"""Pure helpers for VOD-batched motion search.
+
+Coalescing gate-passing segments into time-contiguous runs, mapping a frame's
+VOD stream time back to an absolute timestamp, and thinning sample times to a
+target interval. No I/O or ffmpeg here so the tricky math stays unit-testable.
+"""
+
+from bisect import bisect_right
+from typing import Any
+
+
+def coalesce_runs(
+ segments: list[Any], max_seconds: float, epsilon: float
+) -> list[list[Any]]:
+ """Group gate-passing segments into time-contiguous runs.
+
+ A run extends while each segment's ``start_time`` is within ``epsilon`` of
+ the previous segment's ``end_time`` (no recording gap) and the run's total
+ span stays at or below ``max_seconds``. A gap or the cap starts a new run.
+ Each segment must expose ``start_time`` / ``end_time``.
+ """
+ runs: list[list[Any]] = []
+ current: list[Any] = []
+ for seg in segments:
+ if not current:
+ current = [seg]
+ continue
+ prev_end = float(current[-1].end_time)
+ run_start = float(current[0].start_time)
+ contiguous = abs(float(seg.start_time) - prev_end) <= epsilon
+ within_cap = (float(seg.end_time) - run_start) <= max_seconds
+ if contiguous and within_cap:
+ current.append(seg)
+ else:
+ runs.append(current)
+ current = [seg]
+ if current:
+ runs.append(current)
+ return runs
+
+
+def build_segment_time_map(
+ run: list[Any],
+) -> list[tuple[float, float, float]]:
+ """Build a (stream_offset, abs_start, duration) row per segment in a run.
+
+ ``stream_offset`` is the segment's start in continuous VOD stream time (the
+ cumulative sum of preceding segment durations); ``abs_start`` is its absolute
+ ``start_time``. Built from each segment's own duration; for a gap-free run
+ this makes stream time equal ``run_start + offset``.
+ """
+ rows: list[tuple[float, float, float]] = []
+ offset = 0.0
+ for seg in run:
+ duration = float(seg.end_time) - float(seg.start_time)
+ rows.append((offset, float(seg.start_time), duration))
+ offset += duration
+ return rows
+
+
+def stream_time_to_absolute(
+ time_map: list[tuple[float, float, float]], stream_time: float
+) -> float:
+ """Map a VOD stream time to an absolute timestamp via the run's table.
+
+ Binary-searches the segment whose stream range contains ``stream_time`` and
+ returns ``abs_start + (stream_time - stream_offset)``. Times past the last
+ segment map into the last segment (clamped at the run edge).
+ """
+ offsets = [row[0] for row in time_map]
+ idx = bisect_right(offsets, stream_time) - 1
+ if idx < 0:
+ idx = 0
+ stream_offset, abs_start, _duration = time_map[idx]
+ return abs_start + (stream_time - stream_offset)
diff --git a/frigate/jobs/motion_search_decode.py b/frigate/jobs/motion_search_decode.py
new file mode 100644
index 0000000000..4b1d518013
--- /dev/null
+++ b/frigate/jobs/motion_search_decode.py
@@ -0,0 +1,382 @@
+"""Hardware-accelerated ffmpeg decode for motion search.
+
+Decodes a recording run's VOD/HLS stream with an ffmpeg subprocess, optionally
+selecting only keyframes, and streams raw frames over a pipe for the motion
+math. Output is the requested ``pix_fmt`` (gray or ``bgr24``) with optional
+crop/scale applied in the filter graph so downstream pixels are unchanged.
+"""
+
+import json
+import logging
+import subprocess as sp
+import tempfile
+from collections.abc import Callable, Generator
+from typing import IO
+
+import numpy as np
+
+from frigate.config import CameraConfig
+from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_decode
+from frigate.util.services import auto_detect_hwaccel
+
+logger = logging.getLogger(__name__)
+
+# Output-format surfaces that download cleanly to nv12 via the fixed
+# ``hwdownload,format=nv12`` step the decode path appends. Other surfaces
+# (drm_prime from rkmpp, vulkan, amf) need a different download step, so motion
+# search decodes them in software to keep results byte-identical rather than risk
+# a wrong-but-valid-sized frame the zero-frame fallback gate would not catch.
+_NV12_OUTPUT_FORMATS = frozenset({"vaapi", "cuda", "qsv"})
+
+
+def _hwaccel_output_format(decode_args: list[str]) -> str | None:
+ """Return the ``-hwaccel_output_format`` value in ffmpeg args, or None."""
+ try:
+ idx = decode_args.index("-hwaccel_output_format")
+ except ValueError:
+ return None
+ return decode_args[idx + 1] if idx + 1 < len(decode_args) else None
+
+
+def resolve_motion_decode_args(camera_config: CameraConfig) -> list[str]:
+ """Resolve the ffmpeg hwaccel decode args for a camera's recordings.
+
+ ``auto`` is resolved via ``auto_detect_hwaccel`` and the preset is expanded
+ by ``parse_preset_hardware_acceleration_decode`` (the same table the live
+ pipeline uses). Acceleration is kept only when the decoded surface downloads
+ cleanly to nv12 -- decided by reading ``-hwaccel_output_format`` back from the
+ resolved args rather than a separate preset allowlist that could drift from
+ ``PRESETS_HW_ACCEL_DECODE``. Anything else (custom args, a software-only
+ preset, or an nv12-incompatible surface) returns an empty list, meaning
+ software decode, so results stay byte-identical.
+ """
+ raw = camera_config.ffmpeg.hwaccel_args
+ preset = auto_detect_hwaccel() if raw == "auto" else raw
+
+ # Custom args (a list) decode in software so results stay byte-identical.
+ if not isinstance(preset, str):
+ return []
+
+ decode_args = parse_preset_hardware_acceleration_decode(
+ preset,
+ camera_config.detect.fps,
+ camera_config.detect.width or 0,
+ camera_config.detect.height or 0,
+ camera_config.ffmpeg.gpu,
+ )
+ if not decode_args:
+ return []
+
+ if _hwaccel_output_format(decode_args) not in _NV12_OUTPUT_FORMATS:
+ return []
+
+ return decode_args
+
+
+def _read_exact(stream: IO[bytes], size: int) -> bytes | None:
+ """Read exactly ``size`` bytes from a pipe, or None at clean EOF.
+
+ Pipe reads can return fewer bytes than requested, so loop until the frame
+ is complete. A short read at the start of a frame means end-of-stream.
+ """
+ buf = bytearray()
+ while len(buf) < size:
+ chunk = stream.read(size - len(buf))
+ if not chunk:
+ return None
+ buf.extend(chunk)
+ return bytes(buf)
+
+
+def _terminate(proc: sp.Popen[bytes]) -> None:
+ """Stop an ffmpeg decode process promptly."""
+ # Close the read end first so a blocked ffmpeg write unblocks (ffmpeg then
+ # sees a broken pipe), then signal it. The resulting ffmpeg write error is
+ # harmless and goes to the captured stderr.
+ if proc.stdout is not None:
+ try:
+ proc.stdout.close()
+ except OSError:
+ pass
+ if proc.poll() is None:
+ proc.terminate()
+ try:
+ proc.wait(timeout=5)
+ except sp.TimeoutExpired:
+ proc.kill()
+ proc.wait()
+
+
+KEYFRAME_MAX_GAP_SECONDS = 2.0
+
+
+def keyframe_sampling_eligible(
+ keyframe_pts: list[float], max_gap: float = KEYFRAME_MAX_GAP_SECONDS
+) -> bool:
+ """True if keyframes are dense and regular enough for keyframe-only sampling.
+
+ Requires at least two keyframes and no gap longer than ``max_gap`` seconds, so
+ a multi-second motion event necessarily spans a sampled keyframe.
+ """
+ if len(keyframe_pts) < 2:
+ return False
+ gaps = [b - a for a, b in zip(keyframe_pts, keyframe_pts[1:])]
+ return max(gaps) <= max_gap
+
+
+VOD_PROTOCOL_ARGS = ["-protocol_whitelist", "pipe,file,http,tcp"]
+
+
+def build_vod_decode_command(
+ ffmpeg_path: str,
+ vod_url: str,
+ decode_args: list[str],
+ crop: tuple[int, int, int, int] | None,
+ scale: tuple[int, int] | None,
+ gray: bool,
+ *,
+ skip_nonkey: bool,
+ fps_rate: float | None,
+) -> list[str]:
+ """Build the ffmpeg argv to decode a VOD HLS URL.
+
+ ``skip_nonkey`` adds ``-skip_frame nokey`` (keyframe-only). ``fps_rate`` adds
+ an ``fps`` filter for the fixed-cadence fallback. They are mutually
+ exclusive: keyframe mode passes ``skip_nonkey=True``/``fps_rate=None``; the
+ fallback passes ``skip_nonkey=False`` with a rate.
+ """
+ filters: list[str] = []
+ # With hwaccel the decoded frames are GPU surfaces; pull them back to system
+ # memory before the CPU fps/crop/scale filters and the rawvideo encoder.
+ if decode_args:
+ filters.append("hwdownload")
+ filters.append("format=nv12")
+ if fps_rate is not None:
+ filters.append(f"fps={fps_rate}")
+ if crop is not None:
+ cw, ch, cx, cy = crop
+ filters.append(f"crop={cw}:{ch}:{cx}:{cy}")
+ if scale is not None:
+ sw, sh = scale
+ filters.append(f"scale={sw}:{sh}")
+
+ pix_fmt = "gray" if gray else "bgr24"
+ cmd = [ffmpeg_path, "-hide_banner", "-loglevel", "error"]
+ if skip_nonkey:
+ cmd += ["-skip_frame", "nokey"]
+ cmd += [*decode_args, *VOD_PROTOCOL_ARGS, "-i", vod_url, "-an"]
+ if filters:
+ cmd += ["-vf", ",".join(filters)]
+ cmd += ["-vsync", "0", "-f", "rawvideo", "-pix_fmt", pix_fmt, "pipe:"]
+ return cmd
+
+
+def _run_vod_decode(
+ ffmpeg_path: str,
+ vod_url: str,
+ out_width: int,
+ out_height: int,
+ channels: int,
+ decode_args: list[str],
+ crop: tuple[int, int, int, int] | None,
+ scale: tuple[int, int] | None,
+ gray: bool,
+ should_stop: Callable[[], bool],
+ *,
+ skip_nonkey: bool,
+ fps_rate: float | None,
+ software_retry: bool,
+) -> Generator[np.ndarray, None, None]:
+ """Run one VOD decode, yielding raw frames; retry in software if empty."""
+ cmd = build_vod_decode_command(
+ ffmpeg_path,
+ vod_url,
+ decode_args,
+ crop,
+ scale,
+ gray,
+ skip_nonkey=skip_nonkey,
+ fps_rate=fps_rate,
+ )
+ frame_size = out_width * out_height * channels
+ stderr_file = tempfile.SpooledTemporaryFile(max_size=65536)
+ proc = sp.Popen(cmd, stdout=sp.PIPE, stderr=stderr_file)
+ assert proc.stdout is not None
+
+ count = 0
+ try:
+ while True:
+ if should_stop():
+ break
+ buf = _read_exact(proc.stdout, frame_size)
+ if buf is None:
+ break
+ if channels == 1:
+ frame = np.frombuffer(buf, dtype=np.uint8).reshape(
+ (out_height, out_width)
+ )
+ else:
+ frame = np.frombuffer(buf, dtype=np.uint8).reshape(
+ (out_height, out_width, channels)
+ )
+ count += 1
+ yield frame
+ finally:
+ _terminate(proc)
+ stderr_file.close()
+
+ if count == 0 and software_retry and not should_stop():
+ logger.warning("Hardware VOD decode produced no frames, retrying in software")
+ yield from _run_vod_decode(
+ ffmpeg_path,
+ vod_url,
+ out_width,
+ out_height,
+ channels,
+ [],
+ crop,
+ scale,
+ gray,
+ should_stop,
+ skip_nonkey=skip_nonkey,
+ fps_rate=fps_rate,
+ software_retry=False,
+ )
+
+
+def iter_vod_frames(
+ ffmpeg_path: str,
+ vod_url: str,
+ out_width: int,
+ out_height: int,
+ channels: int,
+ decode_args: list[str],
+ crop: tuple[int, int, int, int] | None,
+ scale: tuple[int, int] | None,
+ gray: bool,
+ should_stop: Callable[[], bool],
+ *,
+ skip_nonkey: bool,
+ fps_rate: float | None,
+) -> Generator[np.ndarray, None, None]:
+ """Decode a VOD HLS URL and yield raw frames in order.
+
+ Pair keyframe-mode output with probed keyframe PTS; pair fallback output with
+ a fixed cadence. Falls back once to software decode if a hwaccel decode yields
+ no frames.
+ """
+ yield from _run_vod_decode(
+ ffmpeg_path,
+ vod_url,
+ out_width,
+ out_height,
+ channels,
+ decode_args,
+ crop,
+ scale,
+ gray,
+ should_stop,
+ skip_nonkey=skip_nonkey,
+ fps_rate=fps_rate,
+ software_retry=bool(decode_args),
+ )
+
+
+def probe_vod_keyframe_pts(ffprobe_path: str, vod_url: str) -> list[float]:
+ """Return keyframe presentation timestamps (VOD stream time) in order.
+
+ Reads packet flags via ffprobe over the VOD URL (no decode). Returns [] on
+ any failure so the caller can fall back.
+ """
+ cmd = [
+ ffprobe_path,
+ "-v",
+ "error",
+ *VOD_PROTOCOL_ARGS,
+ "-i",
+ vod_url,
+ "-select_streams",
+ "v:0",
+ "-show_packets",
+ "-show_entries",
+ "packet=pts_time,flags",
+ "-of",
+ "json",
+ ]
+ try:
+ completed = sp.run(cmd, capture_output=True, text=True, timeout=120)
+ except (OSError, sp.SubprocessError):
+ logger.warning("ffprobe failed for VOD keyframe probe")
+ return []
+
+ if completed.returncode != 0 or not completed.stdout:
+ return []
+
+ try:
+ packets = json.loads(completed.stdout).get("packets", [])
+ except json.JSONDecodeError:
+ return []
+
+ pts: list[float] = []
+ for pkt in packets:
+ flags = pkt.get("flags", "")
+ pts_time = pkt.get("pts_time")
+ if flags.startswith("K") and pts_time is not None:
+ try:
+ pts.append(float(pts_time))
+ except ValueError:
+ continue
+ return sorted(pts)
+
+
+def probe_video_dimensions(
+ ffprobe_path: str, recording_path: str
+) -> tuple[int, int, float] | None:
+ """Return (width, height, fps) for a recording's video stream, or None.
+
+ Reads stream metadata via ffprobe (no decode). The record stream resolution
+ can differ from the camera's detect resolution, so this is probed once per
+ job against a real segment.
+ """
+ cmd = [
+ ffprobe_path,
+ "-v",
+ "error",
+ "-select_streams",
+ "v:0",
+ "-show_entries",
+ "stream=width,height,avg_frame_rate",
+ "-of",
+ "json",
+ recording_path,
+ ]
+ try:
+ completed = sp.run(cmd, capture_output=True, text=True, timeout=30)
+ except (OSError, sp.SubprocessError):
+ return None
+
+ if completed.returncode != 0 or not completed.stdout:
+ return None
+
+ try:
+ streams = json.loads(completed.stdout).get("streams", [])
+ except json.JSONDecodeError:
+ return None
+
+ if not streams:
+ return None
+
+ stream = streams[0]
+ width = int(stream.get("width", 0) or 0)
+ height = int(stream.get("height", 0) or 0)
+ rate = stream.get("avg_frame_rate", "0/0") or "0/0"
+ try:
+ num, _, den = rate.partition("/")
+ fps = float(num) / float(den) if float(den) != 0 else 0.0
+ except (ValueError, ZeroDivisionError):
+ fps = 0.0
+
+ if width <= 0 or height <= 0:
+ return None
+
+ return width, height, fps
diff --git a/frigate/jobs/vlm_watch.py b/frigate/jobs/vlm_watch.py
index cd64325d0d..1e6a542c21 100644
--- a/frigate/jobs/vlm_watch.py
+++ b/frigate/jobs/vlm_watch.py
@@ -8,7 +8,7 @@ import threading
import time
from dataclasses import asdict, dataclass, field
from datetime import datetime
-from typing import Any, Optional
+from typing import Any
import cv2
@@ -45,6 +45,7 @@ class VLMWatchJob(Job):
last_reasoning: str = ""
notification_message: str = ""
iteration_count: int = 0
+ username: str = ""
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@@ -359,8 +360,8 @@ class VLMWatchRunner(threading.Thread):
# Module-level singleton (only one watch job at a time)
-_current_job: Optional[VLMWatchJob] = None
-_cancel_event: Optional[threading.Event] = None
+_current_job: VLMWatchJob | None = None
+_cancel_event: threading.Event | None = None
_job_lock = threading.Lock()
@@ -374,6 +375,7 @@ def start_vlm_watch_job(
dispatcher: Any,
labels: list[str] | None = None,
zones: list[str] | None = None,
+ username: str = "",
) -> str:
"""Start a new VLM watch job. Returns the job ID.
@@ -397,6 +399,7 @@ def start_vlm_watch_job(
max_duration_minutes=max_duration_minutes,
labels=labels or [],
zones=zones or [],
+ username=username,
)
cancel_ev = threading.Event()
_current_job = job
@@ -441,6 +444,6 @@ def stop_vlm_watch_job() -> bool:
return True
-def get_vlm_watch_job() -> Optional[VLMWatchJob]:
+def get_vlm_watch_job() -> VLMWatchJob | None:
"""Return the current (or most recent) VLM watch job."""
return _current_job
diff --git a/frigate/log.py b/frigate/log.py
index cd475a4bdb..3c5f8ec8b8 100644
--- a/frigate/log.py
+++ b/frigate/log.py
@@ -6,13 +6,14 @@ import os
import sys
import threading
from collections import deque
+from collections.abc import Callable, Generator
from contextlib import contextmanager
from enum import Enum
from functools import wraps
from logging.handlers import QueueHandler, QueueListener
from multiprocessing.managers import SyncManager
from queue import Empty, Queue
-from typing import Any, Callable, Deque, Generator, Optional
+from typing import Any
from frigate.util.builtin import clean_camera_user_pass
@@ -47,8 +48,8 @@ class LogLevel(str, Enum):
critical = "critical"
-log_listener: Optional[QueueListener] = None
-log_queue: Optional[Queue] = None
+log_listener: QueueListener | None = None
+log_queue: Queue | None = None
def setup_logging(manager: SyncManager) -> None:
@@ -118,7 +119,7 @@ class LogPipe(threading.Thread):
super().__init__(daemon=False)
self.logger = logging.getLogger(log_name)
self.level = level
- self.deque: Deque[str] = deque(maxlen=100)
+ self.deque: deque[str] = deque(maxlen=100)
self.fdRead, self.fdWrite = os.pipe()
self.pipeReader = os.fdopen(self.fdRead)
self.start()
diff --git a/frigate/motion/__init__.py b/frigate/motion/__init__.py
index 58f781f465..e6ead35bb7 100644
--- a/frigate/motion/__init__.py
+++ b/frigate/motion/__init__.py
@@ -1,5 +1,4 @@
from abc import ABC, abstractmethod
-from typing import Tuple
from numpy import ndarray
@@ -10,7 +9,7 @@ class MotionDetector(ABC):
@abstractmethod
def __init__(
self,
- frame_shape: Tuple[int, int, int],
+ frame_shape: tuple[int, int, int],
config: MotionConfig,
fps: int,
improve_contrast: bool,
diff --git a/frigate/motion/improved_motion.py b/frigate/motion/improved_motion.py
index 6694dafff5..598eeacd1d 100644
--- a/frigate/motion/improved_motion.py
+++ b/frigate/motion/improved_motion.py
@@ -1,5 +1,4 @@
import logging
-from typing import Optional
import cv2
import numpy as np
@@ -19,7 +18,7 @@ class ImprovedMotionDetector(MotionDetector):
frame_shape: tuple[int, ...],
config: RuntimeMotionConfig,
fps: int,
- ptz_metrics: Optional[PTZMetrics] = None,
+ ptz_metrics: PTZMetrics | None = None,
name: str = "improved",
blur_radius: int = 1,
interpolation: int = cv2.INTER_NEAREST,
diff --git a/frigate/object_detection/base.py b/frigate/object_detection/base.py
index a62fe48431..bc7910e4d0 100644
--- a/frigate/object_detection/base.py
+++ b/frigate/object_detection/base.py
@@ -7,7 +7,7 @@ from abc import ABC, abstractmethod
from collections import deque
from multiprocessing import Queue, Value
from multiprocessing.synchronize import Event as MpEvent
-from typing import Any, Optional
+from typing import Any
import numpy as np
import zmq
@@ -42,9 +42,9 @@ class ObjectDetector(ABC):
class BaseLocalDetector(ObjectDetector):
def __init__(
self,
- detector_config: Optional[BaseDetectorConfig] = None,
- labels: Optional[str] = None,
- stop_event: Optional[MpEvent] = None,
+ detector_config: BaseDetectorConfig | None = None,
+ labels: str | None = None,
+ stop_event: MpEvent | None = None,
) -> None:
self.fps = EventsPerSecond()
if labels is None:
@@ -167,8 +167,9 @@ class DetectorRunner(FrigateProcess):
# detect and send the output
self.start_time.value = datetime.datetime.now().timestamp()
+ mono_start = time.monotonic()
detections = object_detector.detect_raw(input_frame)
- duration = datetime.datetime.now().timestamp() - self.start_time.value
+ duration = time.monotonic() - mono_start
frame_manager.close(connection_id)
if connection_id not in self.outputs:
diff --git a/frigate/output/birdseye.py b/frigate/output/birdseye.py
index 21e63df44e..fb26c7820c 100644
--- a/frigate/output/birdseye.py
+++ b/frigate/output/birdseye.py
@@ -8,10 +8,9 @@ import os
import queue
import subprocess as sp
import threading
-import time
import traceback
from multiprocessing.synchronize import Event as MpEvent
-from typing import Any, Optional
+from typing import Any
import cv2
import numpy as np
@@ -19,6 +18,7 @@ import numpy as np
from frigate.comms.inter_process import InterProcessRequestor
from frigate.config import BirdseyeModeEnum, FfmpegConfig, FrigateConfig
from frigate.const import BASE_DIR, BIRDSEYE_PIPE, INSTALL_DIR, UPDATE_BIRDSEYE_LAYOUT
+from frigate.output.ws_auth import ws_has_camera_access
from frigate.util.image import (
SharedMemoryFrameManager,
copy_yuv_to_position,
@@ -62,8 +62,10 @@ def get_canvas_shape(width: int, height: int) -> tuple[int, int]:
if round(a_w / a_h, 2) != round(width / height, 2):
canvas_width = int(width // 4 * 4)
canvas_height = int((canvas_width / a_w * a_h) // 4 * 4)
- logger.warning(
- f"The birdseye resolution is a non-standard aspect ratio, forcing birdseye resolution to {canvas_width} x {canvas_height}"
+ logger.error(
+ f"Birdseye resolution {width}x{height} is not a supported aspect ratio "
+ f"and may cause visual distortion; falling back to {canvas_width}x{canvas_height}. "
+ f"Set width and height to a supported aspect ratio (16:9, 20:10, 16:6, 32:9, 12:9, 22:15, 9:16, 9:12, 16:3, or 1:1)"
)
return (canvas_width, canvas_height)
@@ -236,12 +238,14 @@ class BroadcastThread(threading.Thread):
converter: FFMpegConverter,
websocket_server: Any,
stop_event: MpEvent,
+ config: FrigateConfig,
):
super().__init__()
self.camera = camera
self.converter = converter
self.websocket_server = websocket_server
self.stop_event = stop_event
+ self.config = config
def run(self) -> None:
while not self.stop_event.is_set():
@@ -256,6 +260,7 @@ class BroadcastThread(threading.Thread):
if (
not ws.terminated
and ws.environ["PATH_INFO"] == f"/{self.camera}"
+ and ws_has_camera_access(ws, self.camera, self.config)
):
try:
ws.send(buf, binary=True)
@@ -374,8 +379,8 @@ class BirdsEyeFrameManager:
def copy_to_position(
self,
position: Any,
- camera: Optional[str] = None,
- frame: Optional[np.ndarray] = None,
+ camera: str | None = None,
+ frame: np.ndarray | None = None,
) -> None:
if camera is None:
frame = None
@@ -423,7 +428,7 @@ class BirdsEyeFrameManager:
}
return coordinates
- def update_frame(self, frame: Optional[np.ndarray] = None) -> tuple[bool, bool]:
+ def update_frame(self, frame: np.ndarray | None = None) -> tuple[bool, bool]:
"""
Update birdseye, optionally with a new frame.
Returns (frame_changed, layout_changed) to indicate if the frame or layout changed.
@@ -587,7 +592,7 @@ class BirdsEyeFrameManager:
self,
cameras_to_add: list[str],
coefficient: float,
- ) -> Optional[list[list[Any]]]:
+ ) -> list[list[Any]] | None:
"""Calculate the optimal layout for 2+ cameras."""
def find_available_x(
@@ -595,7 +600,7 @@ class BirdsEyeFrameManager:
width: int,
reserved_ranges: list[tuple[int, int]],
max_width: int,
- ) -> Optional[int]:
+ ) -> int | None:
"""Find the first horizontal slot that does not collide with reservations."""
x = current_x
@@ -613,7 +618,7 @@ class BirdsEyeFrameManager:
return None
- def map_layout(row_height: int) -> tuple[int, int, Optional[list[list[Any]]]]:
+ def map_layout(row_height: int) -> tuple[int, int, list[list[Any]] | None]:
"""Lay out cameras row by row while reserving portrait spans for the next row."""
candidate_layout: list[list[Any]] = []
reserved_ranges: dict[int, list[tuple[int, int]]] = {}
@@ -773,27 +778,34 @@ class Birdseye:
websocket_server: Any,
) -> None:
self.config = config
+ canvas_width, canvas_height = get_canvas_shape(
+ config.birdseye.width, config.birdseye.height
+ )
self.input: queue.Queue[bytes] = queue.Queue(maxsize=10)
self.converter = FFMpegConverter(
config.ffmpeg,
self.input,
stop_event,
- config.birdseye.width,
- config.birdseye.height,
- config.birdseye.width,
- config.birdseye.height,
+ canvas_width,
+ canvas_height,
+ canvas_width,
+ canvas_height,
config.birdseye.quality,
config.birdseye.restream,
)
self.broadcaster = BroadcastThread(
- "birdseye", self.converter, websocket_server, stop_event
+ "birdseye",
+ self.converter,
+ websocket_server,
+ stop_event,
+ config,
)
self.birdseye_manager = BirdsEyeFrameManager(self.config, stop_event)
self.frame_manager = SharedMemoryFrameManager()
self.stop_event = stop_event
self.requestor = InterProcessRequestor()
self.idle_fps: float = self.config.birdseye.idle_heartbeat_fps
- self._idle_interval: Optional[float] = (
+ self._idle_interval: float | None = (
(1.0 / self.idle_fps) if self.idle_fps > 0 else None
)
@@ -854,7 +866,7 @@ class Birdseye:
coordinates = self.birdseye_manager.get_camera_coordinates()
self.requestor.send_data(UPDATE_BIRDSEYE_LAYOUT, coordinates)
if self._idle_interval:
- now = time.monotonic()
+ now = datetime.datetime.now().timestamp()
is_idle = len(self.birdseye_manager.camera_layout) == 0
if (
is_idle
diff --git a/frigate/output/camera.py b/frigate/output/camera.py
index 917e38dd1d..88d16ed4b4 100644
--- a/frigate/output/camera.py
+++ b/frigate/output/camera.py
@@ -7,7 +7,8 @@ import threading
from multiprocessing.synchronize import Event as MpEvent
from typing import Any
-from frigate.config import CameraConfig, FfmpegConfig
+from frigate.config import CameraConfig, FfmpegConfig, FrigateConfig
+from frigate.output.ws_auth import ws_has_camera_access
logger = logging.getLogger(__name__)
@@ -102,12 +103,14 @@ class BroadcastThread(threading.Thread):
converter: FFMpegConverter,
websocket_server: Any,
stop_event: MpEvent,
+ config: FrigateConfig,
):
super().__init__()
self.camera = camera
self.converter = converter
self.websocket_server = websocket_server
self.stop_event = stop_event
+ self.config = config
def run(self) -> None:
while not self.stop_event.is_set():
@@ -122,6 +125,7 @@ class BroadcastThread(threading.Thread):
if (
not ws.terminated
and ws.environ["PATH_INFO"] == f"/{self.camera}"
+ and ws_has_camera_access(ws, self.camera, self.config)
):
try:
ws.send(buf, binary=True)
@@ -135,7 +139,11 @@ class BroadcastThread(threading.Thread):
class JsmpegCamera:
def __init__(
- self, config: CameraConfig, stop_event: MpEvent, websocket_server: Any
+ self,
+ config: CameraConfig,
+ frigate_config: FrigateConfig,
+ stop_event: MpEvent,
+ websocket_server: Any,
) -> None:
self.config = config
self.input: queue.Queue[bytes] = queue.Queue(maxsize=config.detect.fps)
@@ -154,7 +162,11 @@ class JsmpegCamera:
config.live.quality,
)
self.broadcaster = BroadcastThread(
- config.name or "", self.converter, websocket_server, stop_event
+ config.name or "",
+ self.converter,
+ websocket_server,
+ stop_event,
+ frigate_config,
)
self.converter.start()
diff --git a/frigate/output/output.py b/frigate/output/output.py
index 22bcbb31ff..67dba5221f 100644
--- a/frigate/output/output.py
+++ b/frigate/output/output.py
@@ -32,6 +32,7 @@ from frigate.const import (
from frigate.output.birdseye import Birdseye
from frigate.output.camera import JsmpegCamera
from frigate.output.preview import PreviewRecorder
+from frigate.output.ws_auth import ws_has_camera_access
from frigate.util.image import SharedMemoryFrameManager, get_blank_yuv_frame
from frigate.util.process import FrigateProcess
@@ -102,7 +103,7 @@ class OutputProcess(FrigateProcess):
) -> None:
camera_config = self.config.cameras[camera]
jsmpeg_cameras[camera] = JsmpegCamera(
- camera_config, self.stop_event, websocket_server
+ camera_config, self.config, self.stop_event, websocket_server
)
preview_recorders[camera] = PreviewRecorder(camera_config)
preview_write_times[camera] = 0
@@ -262,6 +263,7 @@ class OutputProcess(FrigateProcess):
# send camera frame to ffmpeg process if websockets are connected
if any(
ws.environ["PATH_INFO"].endswith(camera)
+ and ws_has_camera_access(ws, camera, self.config)
for ws in websocket_server.manager
):
# write to the converter for the camera if clients are listening to the specific camera
@@ -275,6 +277,7 @@ class OutputProcess(FrigateProcess):
self.config.birdseye.restream
or any(
ws.environ["PATH_INFO"].endswith("birdseye")
+ and ws_has_camera_access(ws, "birdseye", self.config)
for ws in websocket_server.manager
)
)
@@ -339,13 +342,30 @@ def move_preview_frames(loc: str) -> None:
preview_holdover = os.path.join(CLIPS_DIR, "preview_restart_cache")
preview_cache = os.path.join(CACHE_DIR, "preview_frames")
- try:
- if loc == "clips":
- shutil.move(preview_cache, preview_holdover)
- elif loc == "cache":
- if not os.path.exists(preview_holdover):
- return
+ if loc == "clips":
+ src = preview_cache
+ dst = preview_holdover
+ elif loc == "cache":
+ src = preview_holdover
+ dst = preview_cache
+ else:
+ return
- shutil.move(preview_holdover, preview_cache)
+ try:
+ if not os.path.exists(src):
+ return
+
+ shutil.move(src, dst)
+
+ except PermissionError:
+ logger.error(
+ "Insufficient permissions while moving preview restart cache from %s to %s",
+ src,
+ dst,
+ )
except shutil.Error:
- logger.error("Failed to restore preview cache.")
+ logger.error(
+ "Failed to move preview restart cache from %s to %s",
+ src,
+ dst,
+ )
diff --git a/frigate/output/preview.py b/frigate/output/preview.py
index 389a3c2078..cce05565a0 100644
--- a/frigate/output/preview.py
+++ b/frigate/output/preview.py
@@ -251,7 +251,7 @@ class PreviewRecorder:
# end segment at end of hour (use UTC to avoid DST issues)
self.segment_end = (
- (datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=1))
+ (datetime.datetime.now(datetime.UTC) + datetime.timedelta(hours=1))
.replace(minute=0, second=0, microsecond=0)
.timestamp()
)
@@ -263,7 +263,7 @@ class PreviewRecorder:
# check for existing items in cache
start_ts = (
- datetime.datetime.now(datetime.timezone.utc)
+ datetime.datetime.now(datetime.UTC)
.replace(minute=0, second=0, microsecond=0)
.timestamp()
)
@@ -298,7 +298,7 @@ class PreviewRecorder:
def reset_frame_cache(self, frame_time: float) -> None:
self.segment_end = (
(
- datetime.datetime.fromtimestamp(frame_time, tz=datetime.timezone.utc)
+ datetime.datetime.fromtimestamp(frame_time, tz=datetime.UTC)
+ datetime.timedelta(hours=1)
)
.replace(minute=0, second=0, microsecond=0)
@@ -361,14 +361,17 @@ class PreviewRecorder:
small_frame,
cv2.COLOR_YUV2BGR_I420,
)
- cv2.imwrite(
- get_cache_image_name(self.camera_name, frame_time),
+ cache_path = get_cache_image_name(self.camera_name, frame_time)
+
+ if not cv2.imwrite(
+ cache_path,
small_frame,
[
int(cv2.IMWRITE_WEBP_QUALITY),
PREVIEW_QUALITY_WEBP[self.config.record.preview.quality],
],
- )
+ ):
+ logger.error("Failed to write preview frame to %s", cache_path)
def write_data(
self,
diff --git a/frigate/output/ws_auth.py b/frigate/output/ws_auth.py
new file mode 100644
index 0000000000..33ec4e4980
--- /dev/null
+++ b/frigate/output/ws_auth.py
@@ -0,0 +1,43 @@
+"""Authorization helpers for JSMPEG websocket clients."""
+
+from typing import Any
+
+from frigate.config import FrigateConfig
+from frigate.models import User
+
+
+def _get_valid_ws_roles(ws: Any, config: FrigateConfig) -> list[str]:
+ role_header = ws.environ.get("HTTP_REMOTE_ROLE", "")
+ roles = [
+ role.strip()
+ for role in role_header.split(config.proxy.separator)
+ if role.strip()
+ ]
+ return [role for role in roles if role in config.auth.roles]
+
+
+def ws_has_camera_access(ws: Any, camera_name: str, config: FrigateConfig) -> bool:
+ """Return True when a websocket client is authorized for the camera path."""
+ roles = _get_valid_ws_roles(ws, config)
+
+ if not roles:
+ return False
+
+ roles_dict = config.auth.roles
+
+ # Birdseye is a composite stream, so only users with unrestricted access
+ # should receive it.
+ if camera_name == "birdseye":
+ return any(role == "admin" or not roles_dict.get(role) for role in roles)
+
+ all_camera_names = set(config.cameras.keys())
+
+ for role in roles:
+ if role == "admin" or not roles_dict.get(role):
+ return True
+
+ allowed_cameras = User.get_allowed_cameras(role, roles_dict, all_camera_names)
+ if camera_name in allowed_cameras:
+ return True
+
+ return False
diff --git a/frigate/plus.py b/frigate/plus.py
index 2870d2ae5f..d528aa1757 100644
--- a/frigate/plus.py
+++ b/frigate/plus.py
@@ -4,7 +4,7 @@ import logging
import os
import re
from pathlib import Path
-from typing import Any, List
+from typing import Any
import cv2
import requests
@@ -140,8 +140,8 @@ class PlusApi:
def add_false_positive(
self,
plus_id: str,
- region: List[float],
- bbox: List[float],
+ region: list[float],
+ bbox: list[float],
score: float,
label: str,
model_hash: str,
@@ -184,7 +184,7 @@ class PlusApi:
def add_annotation(
self,
plus_id: str,
- bbox: List[float],
+ bbox: list[float],
label: str,
difficult: bool = False,
) -> None:
diff --git a/frigate/ptz/autotrack.py b/frigate/ptz/autotrack.py
index 1a45f619c2..9a114c9524 100644
--- a/frigate/ptz/autotrack.py
+++ b/frigate/ptz/autotrack.py
@@ -48,6 +48,22 @@ def ptz_moving_at_frame_time(frame_time, ptz_start_time, ptz_stop_time):
)
+def transform_is_finite(coord_transformations) -> bool:
+ """Return True if a norfair coordinate transform contains only finite values.
+
+ A near-singular homography (common when the motion estimator can't find
+ enough stable features during zoom on a low-texture scene) can produce
+ inf/nan matrix entries. norfair accumulates the homography across frames, so
+ a single bad transform poisons every subsequent one and propagates nan into
+ the tracker's distance function, crashing the camera process.
+ """
+ for attr in ("homography_matrix", "inverse_homography_matrix", "movement_vector"):
+ value = getattr(coord_transformations, attr, None)
+ if value is not None and not np.all(np.isfinite(value)):
+ return False
+ return True
+
+
class PtzMotionEstimator:
def __init__(self, config: CameraConfig, ptz_metrics: PTZMetrics) -> None:
self.frame_manager = SharedMemoryFrameManager()
@@ -135,6 +151,19 @@ class PtzMotionEstimator:
)
self.coord_transformations = None
+ # A degenerate homography can yield non-finite transform values that
+ # norfair would accumulate and feed to the tracker as nan estimates.
+ # Drop the bad transform and request a reset so the estimator rebuilds
+ # a fresh reference frame instead of poisoning every following frame.
+ if self.coord_transformations is not None and not transform_is_finite(
+ self.coord_transformations
+ ):
+ logger.warning(
+ f"Autotracker: motion estimator produced a non-finite transform for {camera} at frame time {frame_time}, resetting"
+ )
+ self.coord_transformations = None
+ self.ptz_metrics.reset.set()
+
try:
logger.debug(
f"{camera}: Motion estimator transformation: {self.coord_transformations.rel_to_abs([[0, 0]])}"
@@ -727,7 +756,7 @@ class PtzAutoTracker:
try:
# Asynchronously wait for move data with a timeout
move_data = await asyncio.wait_for(move_queue.get(), timeout=0.1)
- except asyncio.TimeoutError:
+ except TimeoutError:
continue
async with self.move_queue_locks[camera]:
@@ -919,8 +948,8 @@ class PtzAutoTracker:
if invalid:
logger.debug(
- f"{camera}: Invalid velocity: {tuple(np.round(velocities, 2).flatten().astype(int))}: Invalid because: "
- + ", ".join(
+ f"{camera}: Invalid velocity: {tuple(np.round(velocities, 2).flatten().astype(int))}: Invalid because: %s",
+ ", ".join(
[
var_name
for var_name, is_invalid in [
@@ -932,7 +961,7 @@ class PtzAutoTracker:
]
if is_invalid
]
- )
+ ),
)
# invalid velocity
return False, np.zeros((4,))
@@ -1331,6 +1360,8 @@ class PtzAutoTracker:
return self.tracked_object[camera]["region"]
def autotrack_object(self, camera: str, obj: TrackedObject):
+ if camera not in self.config.cameras:
+ return
camera_config = self.config.cameras[camera]
if camera_config.onvif.autotracking.enabled:
diff --git a/frigate/ptz/onvif.py b/frigate/ptz/onvif.py
index e48b3e7877..d29923e239 100644
--- a/frigate/ptz/onvif.py
+++ b/frigate/ptz/onvif.py
@@ -856,7 +856,7 @@ class OnvifController:
try:
# Wait with a timeout to prevent blocking indefinitely
future.result(timeout=10)
- except asyncio.TimeoutError:
+ except TimeoutError:
logger.error(f"Command {command} timed out for camera {camera_name}")
except Exception as e:
logger.error(
diff --git a/frigate/record/cleanup.py b/frigate/record/cleanup.py
index e41a5bf393..71097f1d95 100644
--- a/frigate/record/cleanup.py
+++ b/frigate/record/cleanup.py
@@ -351,9 +351,11 @@ class RecordingCleanup(threading.Thread):
)
.where(
ReviewSegment.camera == camera,
- # need to ensure segments for all reviews starting
- # before the expire date are included
- ReviewSegment.start_time < motion_expire_date,
+ # candidate recordings can extend up to continuous_expire_date
+ # (the no-motion no-audio branch of the recordings query),
+ # so reviews must cover that full range to avoid deleting
+ # segments that overlap recent alerts/detections.
+ ReviewSegment.start_time < continuous_expire_date,
)
.order_by(ReviewSegment.start_time)
.namedtuples()
diff --git a/frigate/record/export.py b/frigate/record/export.py
index 9d7a9eb0c9..5d307077c9 100644
--- a/frigate/record/export.py
+++ b/frigate/record/export.py
@@ -9,26 +9,28 @@ import shutil
import string
import subprocess as sp
import threading
+from collections.abc import Callable
from enum import Enum
from pathlib import Path
-from typing import Callable, Optional
+import pytz # type: ignore[import-untyped]
from peewee import DoesNotExist
from frigate.config import FfmpegConfig, FrigateConfig
+from frigate.config.camera.record import ChaptersEnum
from frigate.const import (
CACHE_DIR,
CLIPS_DIR,
EXPORT_DIR,
MAX_PLAYLIST_SECONDS,
PREVIEW_FRAME_TYPE,
- PROCESS_PRIORITY_LOW,
)
from frigate.ffmpeg_presets import (
EncodeTypeEnum,
parse_preset_hardware_acceleration_encode,
)
-from frigate.models import Export, Previews, Recordings
+from frigate.models import Export, Previews, Recordings, ReviewSegment
+from frigate.util.ffmpeg import run_ffmpeg_with_progress
from frigate.util.time import is_current_hour
logger = logging.getLogger(__name__)
@@ -41,33 +43,118 @@ TIMELAPSE_DATA_INPUT_ARGS = "-an -skip_frame nokey"
# Captures the floating-point factor so we can scale expected duration.
SETPTS_FACTOR_RE = re.compile(r"setpts=([0-9]*\.?[0-9]+)\*PTS")
-# ffmpeg flags that can read from or write to arbitrary files
-BLOCKED_FFMPEG_ARGS = frozenset(
+# Allowlisted flags that take no value.
+_VALUELESS_FLAGS = frozenset({"-an", "-sn", "-dn"})
+
+# Allowlisted filter flags. Their value is validated as a filtergraph and may
+# only reference filters in _SAFE_FILTERS.
+_FILTER_FLAGS = frozenset({"-vf", "-af", "-filter"})
+
+# Allowlisted flags that take exactly one value (encoder / muxer-safe options).
+_VALUE_FLAGS = frozenset(
{
- "-i",
- "-filter_script",
- "-filter_complex",
- "-lavfi",
- "-vf",
- "-af",
- "-filter",
- "-vstats_file",
- "-passlogfile",
- "-sdp_file",
- "-dump_attachment",
- "-attach",
+ "-c",
+ "-codec",
+ "-b",
+ "-crf",
+ "-qp",
+ "-q",
+ "-qscale",
+ "-preset",
+ "-tune",
+ "-profile",
+ "-level",
+ "-pix_fmt",
+ "-r",
+ "-g",
+ "-keyint_min",
+ "-sc_threshold",
+ "-bf",
+ "-refs",
+ "-qmin",
+ "-qmax",
+ "-maxrate",
+ "-minrate",
+ "-bufsize",
+ "-movflags",
+ "-threads",
+ "-aspect",
+ "-fps_mode",
+ "-vsync",
+ "-skip_frame",
}
)
+_ALLOWED_FLAGS = _VALUELESS_FLAGS | _FILTER_FLAGS | _VALUE_FLAGS
+
+# Filters that cannot read files, load plugins, or open network sources.
+_SAFE_FILTERS = frozenset(
+ {
+ "setpts",
+ "fps",
+ "scale",
+ "format",
+ "transpose",
+ "hflip",
+ "vflip",
+ "crop",
+ "pad",
+ "setsar",
+ "setdar",
+ }
+)
+
+# Conservative shape for a non-filter flag value. Excludes "/" (paths /
+# filtergraph division), whitespace, brackets, and a leading "-" so a value
+# can never be a path or swallow a following flag. ":" is permitted for values
+# like "16:9".
+_SAFE_VALUE_RE = re.compile(r"^[A-Za-z0-9_.:+][A-Za-z0-9_.:+-]*$")
+
+# Substrings inside a filtergraph that indicate a file-reading filter option.
+# "movie=" also matches "amovie=" as a substring.
+_BLOCKED_FILTER_VALUE_MARKERS = ("movie=", "textfile=", "filename=", "fontfile=")
+
+
+def _base_flag(token: str) -> str:
+ """Return a flag's base name, lowercased and without its stream specifier.
+
+ e.g. "-c:v" -> "-c", "-filter:a:0" -> "-filter".
+ """
+ return token.lower().split(":", 1)[0]
+
+
+def _validate_filtergraph(value: str) -> tuple[bool, str]:
+ """Validate a filtergraph value, allowing only filters in _SAFE_FILTERS."""
+ # None of the safe filters need any of these
+ if any(token in value for token in ("://", "..", "[", "]")):
+ return False, "Invalid filter graph in custom ffmpeg arguments"
+
+ lowered = value.lower()
+ if any(marker in lowered for marker in _BLOCKED_FILTER_VALUE_MARKERS):
+ return False, "File-reading filters are not allowed in custom ffmpeg arguments"
+
+ # Filters are separated by "," within a chain and ";" between chains. Safe
+ # filters never use unescaped "," or ";" in their arguments, so splitting on
+ # them to recover filter names cannot hide a disallowed filter.
+ for spec in re.split(r"[;,]", value):
+ spec = spec.strip()
+ if not spec:
+ continue
+
+ name = spec.split("=", 1)[0].strip().lower()
+ if name not in _SAFE_FILTERS:
+ return False, f"Filter not allowed in custom ffmpeg arguments: {name}"
+
+ return True, ""
+
def validate_ffmpeg_args(args: str) -> tuple[bool, str]:
- """Validate that user-provided ffmpeg args don't allow input/output injection.
+ """Validate user-provided custom export ffmpeg args with an allowlist.
- Blocks:
- - The -i flag and other flags that read/write arbitrary files
- - Filter flags (can read files via movie=/amovie= source filters)
- - Absolute/relative file paths (potential extra outputs)
- - URLs and ffmpeg protocol references (data exfiltration)
+ Every token must be an allowlisted flag or the value of one; filter values
+ may only reference safe filters; and no token may become a bare input or
+ output URL. This structurally prevents arbitrary file read/write, network
+ exfiltration/SSRF, and resource-exhaustion via the export endpoint.
Admin users skip this validation entirely since they are trusted.
"""
@@ -75,26 +162,36 @@ def validate_ffmpeg_args(args: str) -> tuple[bool, str]:
return True, ""
tokens = args.split()
- for token in tokens:
- # Block flags that could inject inputs or write to arbitrary files
- if token.lower() in BLOCKED_FFMPEG_ARGS:
+ i = 0
+ while i < len(tokens):
+ token = tokens[i]
+
+ # A bare (non-flag) token here would be parsed by ffmpeg as an input or
+ # output URL. Only the server sets inputs/outputs, never the user.
+ if not token.startswith("-"):
+ return False, f"Unexpected argument in custom ffmpeg arguments: {token}"
+
+ base = _base_flag(token)
+ if base not in _ALLOWED_FLAGS:
return False, f"Forbidden ffmpeg argument: {token}"
- # Block tokens that look like file paths (potential output injection)
- if (
- token.startswith("/")
- or token.startswith("./")
- or token.startswith("../")
- or token.startswith("~")
- ):
- return False, "File paths are not allowed in custom ffmpeg arguments"
+ if base in _VALUELESS_FLAGS:
+ i += 1
+ continue
- # Block URLs and ffmpeg protocol references (e.g. http://, tcp://, pipe:, file:)
- if "://" in token or token.startswith("pipe:") or token.startswith("file:"):
- return (
- False,
- "Protocol references are not allowed in custom ffmpeg arguments",
- )
+ # Remaining flags consume exactly one value.
+ if i + 1 >= len(tokens):
+ return False, f"Missing value for ffmpeg argument: {token}"
+
+ value = tokens[i + 1]
+ if base in _FILTER_FLAGS:
+ valid, message = _validate_filtergraph(value)
+ if not valid:
+ return False, message
+ elif not _SAFE_VALUE_RE.match(value):
+ return False, f"Invalid value for {token}: {value}"
+
+ i += 2
return True, ""
@@ -112,16 +209,17 @@ class RecordingExporter(threading.Thread):
config: FrigateConfig,
id: str,
camera: str,
- name: Optional[str],
- image: Optional[str],
+ name: str | None,
+ image: str | None,
start_time: int,
end_time: int,
playback_source: PlaybackSourceEnum,
- export_case_id: Optional[str] = None,
- ffmpeg_input_args: Optional[str] = None,
- ffmpeg_output_args: Optional[str] = None,
+ export_case_id: str | None = None,
+ ffmpeg_input_args: str | None = None,
+ ffmpeg_output_args: str | None = None,
cpu_fallback: bool = False,
- on_progress: Optional[Callable[[str, float], None]] = None,
+ chapters: ChaptersEnum | None = None,
+ on_progress: Callable[[str, float], None] | None = None,
) -> None:
super().__init__()
self.config = config
@@ -136,6 +234,7 @@ class RecordingExporter(threading.Thread):
self.ffmpeg_input_args = ffmpeg_input_args
self.ffmpeg_output_args = ffmpeg_output_args
self.cpu_fallback = cpu_fallback
+ self.chapters = chapters
self.on_progress = on_progress
# ensure export thumb dir
@@ -184,7 +283,7 @@ class RecordingExporter(threading.Thread):
return input_duration * factor
- def _sum_source_duration_seconds(self) -> Optional[float]:
+ def _sum_source_duration_seconds(self) -> float | None:
"""Sum saved-video seconds inside [start_time, end_time].
Queries Recordings or Previews depending on the playback source,
@@ -242,111 +341,245 @@ class RecordingExporter(threading.Thread):
return total
- def _inject_progress_flags(self, ffmpeg_cmd: list[str]) -> list[str]:
- """Insert FFmpeg progress reporting flags before the output path.
-
- ``-progress pipe:2`` writes structured key=value lines to stderr,
- ``-nostats`` suppresses the noisy default stats output.
- """
- if not ffmpeg_cmd:
- return ffmpeg_cmd
- return ffmpeg_cmd[:-1] + ["-progress", "pipe:2", "-nostats", ffmpeg_cmd[-1]]
-
def _run_ffmpeg_with_progress(
self,
ffmpeg_cmd: list[str],
playlist_lines: str | list[str],
step: str = "encoding",
) -> tuple[int, str]:
- """Run an FFmpeg export command, parsing progress events from stderr.
+ """Delegate to the shared helper, mapping percent → (step, percent).
- Returns ``(returncode, captured_stderr)``. Stdout is left attached to
- the parent process so we don't have to drain it (and risk a deadlock
- if the buffer fills). Progress percent is computed against the
- expected output duration; values are clamped to [0, 100] inside
- :py:meth:`_emit_progress`.
+ Returns ``(returncode, captured_stderr)``.
"""
- cmd = ["nice", "-n", str(PROCESS_PRIORITY_LOW)] + self._inject_progress_flags(
- ffmpeg_cmd
- )
-
if isinstance(playlist_lines, list):
stdin_payload = "\n".join(playlist_lines)
else:
stdin_payload = playlist_lines
- expected_duration = self._expected_output_duration_seconds()
-
- self._emit_progress(step, 0.0)
-
- proc = sp.Popen(
- cmd,
- stdin=sp.PIPE,
- stderr=sp.PIPE,
- text=True,
- encoding="ascii",
- errors="replace",
+ return run_ffmpeg_with_progress(
+ ffmpeg_cmd,
+ expected_duration_seconds=self._expected_output_duration_seconds(),
+ on_progress=lambda percent: self._emit_progress(step, percent),
+ stdin_payload=stdin_payload,
+ use_low_priority=True,
)
- assert proc.stdin is not None
- assert proc.stderr is not None
-
- try:
- proc.stdin.write(stdin_payload)
- except (BrokenPipeError, OSError):
- # FFmpeg may have rejected the input early; still wait for it
- # to terminate so the returncode is meaningful.
- pass
- finally:
- try:
- proc.stdin.close()
- except (BrokenPipeError, OSError):
- pass
-
- captured: list[str] = []
-
- try:
- for raw_line in proc.stderr:
- captured.append(raw_line)
- line = raw_line.strip()
-
- if not line:
- continue
-
- if line.startswith("out_time_us="):
- if expected_duration <= 0:
- continue
- try:
- out_time_us = int(line.split("=", 1)[1])
- except (ValueError, IndexError):
- continue
- if out_time_us < 0:
- continue
- out_seconds = out_time_us / 1_000_000.0
- percent = (out_seconds / expected_duration) * 100.0
- self._emit_progress(step, percent)
- elif line == "progress=end":
- self._emit_progress(step, 100.0)
- break
- except Exception:
- logger.exception("Failed reading FFmpeg progress for %s", self.export_id)
-
- proc.wait()
-
- # Drain any remaining stderr so callers can log it on failure.
- try:
- remaining = proc.stderr.read()
- if remaining:
- captured.append(remaining)
- except Exception:
- pass
-
- return proc.returncode, "".join(captured)
-
def get_datetime_from_timestamp(self, timestamp: int) -> str:
- # return in iso format
+ # return in iso format using the configured ui.timezone when set,
+ # so the auto-generated export name reflects local time rather
+ # than the container's UTC clock
+ tz_name = self.config.ui.timezone
+ if tz_name:
+ try:
+ tz = pytz.timezone(tz_name)
+ except pytz.UnknownTimeZoneError:
+ tz = None
+ if tz is not None:
+ return datetime.datetime.fromtimestamp(timestamp, tz=tz).strftime(
+ "%Y-%m-%d %H:%M:%S"
+ )
return datetime.datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
+ def _chapter_metadata_path(self) -> str:
+ return os.path.join(CACHE_DIR, f"export_chapters_{self.export_id}.txt")
+
+ def _build_chapter_metadata_file(self, recordings: list) -> str | None:
+ """Write an FFmpeg metadata file with chapters for review items in range.
+
+ Chapter offsets are computed in *output time*: the VOD endpoint
+ concatenates recording clips back-to-back, so wall-clock gaps
+ between recordings collapse in the produced video. We walk the
+ same recording rows that feed the playlist and convert each
+ review item's wall-clock boundaries into output-time offsets.
+ Returns ``None`` when there are no recordings, no review items,
+ or any chapter would have zero output duration.
+ """
+ if not recordings:
+ return None
+
+ windows: list[tuple[float, float, float]] = []
+ output_offset = 0.0
+ for rec in recordings:
+ clipped_start = max(float(rec.start_time), float(self.start_time))
+ clipped_end = min(float(rec.end_time), float(self.end_time))
+ if clipped_end <= clipped_start:
+ continue
+ windows.append((clipped_start, clipped_end, output_offset))
+ output_offset += clipped_end - clipped_start
+
+ if not windows:
+ return None
+
+ try:
+ review_rows = list(
+ ReviewSegment.select(
+ ReviewSegment.start_time,
+ ReviewSegment.end_time,
+ ReviewSegment.severity,
+ ReviewSegment.data,
+ )
+ .where(
+ ReviewSegment.start_time.between(self.start_time, self.end_time)
+ | ReviewSegment.end_time.between(self.start_time, self.end_time)
+ | (
+ (self.start_time > ReviewSegment.start_time)
+ & (self.end_time < ReviewSegment.end_time)
+ )
+ )
+ .where(ReviewSegment.camera == self.camera)
+ .order_by(ReviewSegment.start_time.asc())
+ .iterator()
+ )
+ except Exception:
+ logger.exception(
+ "Failed to query review segments for export %s", self.export_id
+ )
+ return None
+
+ if not review_rows:
+ return None
+
+ total_output = windows[-1][2] + (windows[-1][1] - windows[-1][0])
+ last_recorded_end = windows[-1][1]
+
+ def wall_to_output(t: float) -> float:
+ t = max(float(self.start_time), min(float(self.end_time), t))
+ for w_start, w_end, w_offset in windows:
+ if t < w_start:
+ return w_offset
+ if t <= w_end:
+ return w_offset + (t - w_start)
+ return total_output
+
+ chapter_blocks: list[str] = []
+ for review in review_rows:
+ if review.start_time is None:
+ continue
+ # In-progress segments have a NULL end_time until the activity
+ # closes; clamp to the last recorded second so the chapter never
+ # extends past the actual video.
+ review_end = (
+ float(review.end_time)
+ if review.end_time is not None
+ else last_recorded_end
+ )
+ start_out = wall_to_output(float(review.start_time))
+ end_out = wall_to_output(review_end)
+
+ # Drop chapters that fall entirely in a recording gap, or are
+ # too short to be navigable in a player.
+ if end_out - start_out < 1.0:
+ continue
+
+ data = review.data or {}
+ labels: list[str] = []
+ for obj in data.get("objects") or []:
+ label = str(obj).split("-")[0]
+ if label and label not in labels:
+ labels.append(label)
+
+ metadata = data.get("metadata") or {}
+ title = metadata.get("title")
+
+ if not title:
+ title = str(review.severity).capitalize()
+
+ if labels:
+ title = f"{title}: {', '.join(labels)}"
+
+ chapter_blocks.append(
+ "[CHAPTER]\n"
+ "TIMEBASE=1/1000\n"
+ f"START={int(start_out * 1000)}\n"
+ f"END={int(end_out * 1000)}\n"
+ f"title={title}"
+ )
+
+ if not chapter_blocks:
+ return None
+
+ meta_path = self._chapter_metadata_path()
+ try:
+ with open(meta_path, "w", encoding="utf-8") as f:
+ f.write(";FFMETADATA1\n")
+ f.write("\n".join(chapter_blocks))
+ f.write("\n")
+ except OSError:
+ logger.exception(
+ "Failed to write chapter metadata file for export %s", self.export_id
+ )
+ return None
+
+ return meta_path
+
+ def _build_recording_segment_chapter_metadata_file(
+ self, recordings: list
+ ) -> str | None:
+ """Write an FFmpeg metadata file with one chapter per recording segment.
+
+ Each chapter's title is the segment's wallclock start time in
+ strict ISO 8601 form so a viewer can map any point in the
+ export's playback timeline back to real-world time without
+ OCR-ing a burnt-in timestamp. Chapter offsets are computed in
+ *output time*: the VOD endpoint concatenates recording clips
+ back-to-back, so wall-clock gaps between recordings collapse in
+ the produced video. Returns ``None`` when there are no
+ recordings or every segment is empty after clipping.
+ """
+ if not recordings:
+ return None
+
+ tz_name = self.config.ui.timezone
+ tz: datetime.tzinfo | None = None
+ if tz_name:
+ try:
+ tz = pytz.timezone(tz_name)
+ except pytz.UnknownTimeZoneError:
+ tz = None
+ if tz is None:
+ tz = datetime.UTC
+
+ chapter_blocks: list[str] = []
+ output_offset_ms = 0
+ for rec in recordings:
+ clipped_start = max(float(rec.start_time), float(self.start_time))
+ clipped_end = min(float(rec.end_time), float(self.end_time))
+ if clipped_end <= clipped_start:
+ continue
+
+ duration_ms = int(round((clipped_end - clipped_start) * 1000))
+ if duration_ms <= 0:
+ continue
+
+ title = datetime.datetime.fromtimestamp(clipped_start, tz=tz).isoformat(
+ timespec="seconds"
+ )
+ chapter_blocks.append(
+ "[CHAPTER]\n"
+ "TIMEBASE=1/1000\n"
+ f"START={output_offset_ms}\n"
+ f"END={output_offset_ms + duration_ms}\n"
+ f"title={title}"
+ )
+ output_offset_ms += duration_ms
+
+ if not chapter_blocks:
+ return None
+
+ meta_path = self._chapter_metadata_path()
+ try:
+ with open(meta_path, "w", encoding="utf-8") as f:
+ f.write(";FFMETADATA1\n")
+ f.write("\n".join(chapter_blocks))
+ f.write("\n")
+ except OSError:
+ logger.exception(
+ "Failed to write chapter metadata file for export %s", self.export_id
+ )
+ return None
+
+ return meta_path
+
def save_thumbnail(self, id: str) -> str:
thumb_path = os.path.join(CLIPS_DIR, f"export/{id}.webp")
@@ -358,7 +591,7 @@ class RecordingExporter(threading.Thread):
if (
self.start_time
- < datetime.datetime.now(datetime.timezone.utc)
+ < datetime.datetime.now(datetime.UTC)
.replace(minute=0, second=0, microsecond=0)
.timestamp()
):
@@ -387,16 +620,14 @@ class RecordingExporter(threading.Thread):
except DoesNotExist:
return ""
- diff = self.start_time - preview.start_time
- minutes = int(diff / 60)
- seconds = int(diff % 60)
+ diff = max(0.0, float(self.start_time) - float(preview.start_time))
ffmpeg_cmd = [
- "/usr/lib/ffmpeg/7.0/bin/ffmpeg", # hardcode path for exports thumbnail due to missing libwebp support
+ "/usr/lib/ffmpeg/8.0/bin/ffmpeg", # hardcode path for exports thumbnail due to missing libwebp support
"-hide_banner",
"-loglevel",
"warning",
"-ss",
- f"00:{minutes}:{seconds}",
+ f"{diff:.3f}",
"-i",
preview.path,
"-frames",
@@ -422,12 +653,18 @@ class RecordingExporter(threading.Thread):
start_file = f"{file_start}{self.start_time}.{PREVIEW_FRAME_TYPE}"
end_file = f"{file_start}{self.end_time}.{PREVIEW_FRAME_TYPE}"
selected_preview = None
+ # Preview frames are written at most 1-2 fps during activity
+ # and as little as one every 30s during quiet periods, so a
+ # short export window can contain zero frames. Track the most
+ # recent frame before the window as a fallback.
+ fallback_preview = None
for file in sorted(os.listdir(preview_dir)):
if not file.startswith(file_start):
continue
if file < start_file:
+ fallback_preview = os.path.join(preview_dir, file)
continue
if file > end_file:
@@ -436,6 +673,9 @@ class RecordingExporter(threading.Thread):
selected_preview = os.path.join(preview_dir, file)
break
+ if not selected_preview:
+ selected_preview = fallback_preview
+
if not selected_preview:
return ""
@@ -451,6 +691,24 @@ class RecordingExporter(threading.Thread):
if type(internal_port) is str:
internal_port = int(internal_port.split(":")[-1])
+ recordings = list(
+ Recordings.select(
+ Recordings.start_time,
+ Recordings.end_time,
+ )
+ .where(
+ Recordings.start_time.between(self.start_time, self.end_time)
+ | Recordings.end_time.between(self.start_time, self.end_time)
+ | (
+ (self.start_time > Recordings.start_time)
+ & (self.end_time < Recordings.end_time)
+ )
+ )
+ .where(Recordings.camera == self.camera)
+ .order_by(Recordings.start_time.asc())
+ .iterator()
+ )
+
playlist_lines: list[str] = []
if (self.end_time - self.start_time) <= MAX_PLAYLIST_SECONDS:
playlist_url = f"http://127.0.0.1:{internal_port}/vod/{self.camera}/start/{self.start_time}/end/{self.end_time}/index.m3u8"
@@ -458,32 +716,13 @@ class RecordingExporter(threading.Thread):
f"-y -protocol_whitelist pipe,file,http,tcp -i {playlist_url}"
)
else:
- # get full set of recordings
- export_recordings = (
- Recordings.select(
- Recordings.start_time,
- Recordings.end_time,
- )
- .where(
- Recordings.start_time.between(self.start_time, self.end_time)
- | Recordings.end_time.between(self.start_time, self.end_time)
- | (
- (self.start_time > Recordings.start_time)
- & (self.end_time < Recordings.end_time)
- )
- )
- .where(Recordings.camera == self.camera)
- .order_by(Recordings.start_time.asc())
- )
-
- # Use pagination to process records in chunks
+ # Chunk the recording rows into pages so each playlist line
+ # references a bounded sub-range rather than the full export.
page_size = 1000
- num_pages = (export_recordings.count() + page_size - 1) // page_size
-
- for page in range(1, num_pages + 1):
- playlist = export_recordings.paginate(page, page_size)
+ for i in range(0, len(recordings), page_size):
+ chunk = recordings[i : i + page_size]
playlist_lines.append(
- f"file 'http://127.0.0.1:{internal_port}/vod/{self.camera}/start/{float(playlist[0].start_time)}/end/{float(playlist[-1].end_time)}/index.m3u8'"
+ f"file 'http://127.0.0.1:{internal_port}/vod/{self.camera}/start/{float(chunk[0].start_time)}/end/{float(chunk[-1].end_time)}/index.m3u8'"
)
ffmpeg_input = "-y -protocol_whitelist pipe,file,http,tcp -f concat -safe 0 -i /dev/stdin"
@@ -504,13 +743,42 @@ class RecordingExporter(threading.Thread):
)
).split(" ")
else:
+ # Realtime/stream-copy export. Embed chapter metadata according to
+ # the camera's configured chapter mode: per-recording-segment
+ # timestamps or per-review-item titles.
+ if self.chapters == ChaptersEnum.recording_segments:
+ chapters_path = self._build_recording_segment_chapter_metadata_file(
+ recordings
+ )
+ elif self.chapters == ChaptersEnum.review_items:
+ chapters_path = self._build_chapter_metadata_file(recordings)
+ else:
+ chapters_path = None
+
+ chapter_args = (
+ f" -i {chapters_path} -map 0 -dn -map_metadata 1"
+ if chapters_path
+ else ""
+ )
ffmpeg_cmd = (
- f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} -c copy -movflags +faststart"
+ f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input}{chapter_args} -c copy -movflags +faststart"
).split(" ")
# add metadata
title = f"Frigate Recording for {self.camera}, {self.get_datetime_from_timestamp(self.start_time)} - {self.get_datetime_from_timestamp(self.end_time)}"
- ffmpeg_cmd.extend(["-metadata", f"title={title}"])
+ creation_time = datetime.datetime.fromtimestamp(
+ self.start_time, tz=datetime.UTC
+ ).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
+ ffmpeg_cmd.extend(
+ [
+ "-metadata",
+ f"title={title}",
+ "-metadata",
+ f"creation_time={creation_time}",
+ "-metadata",
+ f"comment=Camera: {self.camera}",
+ ]
+ )
ffmpeg_cmd.append(video_path)
@@ -596,18 +864,32 @@ class RecordingExporter(threading.Thread):
self.config.ffmpeg.ffmpeg_path,
hwaccel_args,
f"{self.ffmpeg_input_args} {TIMELAPSE_DATA_INPUT_ARGS} {ffmpeg_input}".strip(),
- f"{self.ffmpeg_output_args} -movflags +faststart {video_path}".strip(),
+ f"{self.ffmpeg_output_args} -movflags +faststart".strip(),
EncodeTypeEnum.timelapse,
)
).split(" ")
else:
ffmpeg_cmd = (
- f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} {codec} -movflags +faststart {video_path}"
+ f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} {codec} -movflags +faststart"
).split(" ")
# add metadata
title = f"Frigate Preview for {self.camera}, {self.get_datetime_from_timestamp(self.start_time)} - {self.get_datetime_from_timestamp(self.end_time)}"
- ffmpeg_cmd.extend(["-metadata", f"title={title}"])
+ creation_time = datetime.datetime.fromtimestamp(
+ self.start_time, tz=datetime.UTC
+ ).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
+ ffmpeg_cmd.extend(
+ [
+ "-metadata",
+ f"title={title}",
+ "-metadata",
+ f"creation_time={creation_time}",
+ "-metadata",
+ f"comment=Camera: {self.camera}",
+ ]
+ )
+
+ ffmpeg_cmd.append(video_path)
return ffmpeg_cmd, playlist_lines
@@ -691,6 +973,8 @@ class RecordingExporter(threading.Thread):
ffmpeg_cmd, playlist_lines, step="encoding_retry"
)
+ Path(self._chapter_metadata_path()).unlink(missing_ok=True)
+
if returncode != 0:
logger.error(
f"Failed to export {self.playback_source.value} for command {' '.join(ffmpeg_cmd)}"
diff --git a/frigate/record/maintainer.py b/frigate/record/maintainer.py
index 73868ea245..7f9dbc19da 100644
--- a/frigate/record/maintainer.py
+++ b/frigate/record/maintainer.py
@@ -11,7 +11,7 @@ import time
from collections import defaultdict
from multiprocessing.synchronize import Event as MpEvent
from pathlib import Path
-from typing import Any, Optional, Tuple
+from typing import Any
import numpy as np
import psutil
@@ -42,6 +42,8 @@ from frigate.util.services import get_video_properties
logger = logging.getLogger(__name__)
+STALE_RECORDINGS_INFO_TTL = MAX_SEGMENTS_IN_CACHE * MAX_SEGMENT_DURATION * 2
+
class SegmentInfo:
def __init__(
@@ -98,7 +100,7 @@ class RecordingMaintainer(threading.Thread):
self.stop_event = stop_event
self.object_recordings_info: dict[str, list] = defaultdict(list)
self.audio_recordings_info: dict[str, list] = defaultdict(list)
- self.end_time_cache: dict[str, Tuple[datetime.datetime, float]] = {}
+ self.end_time_cache: dict[str, tuple[datetime.datetime, float]] = {}
self.unexpected_cache_files_logged: bool = False
async def move_files(self) -> None:
@@ -125,7 +127,7 @@ class RecordingMaintainer(threading.Thread):
start_time = datetime.datetime.strptime(
date, CACHE_SEGMENT_FORMAT
- ).astimezone(datetime.timezone.utc)
+ ).astimezone(datetime.UTC)
if (
camera not in newest_cache_segments
or start_time > newest_cache_segments[camera]["start_time"]
@@ -185,7 +187,7 @@ class RecordingMaintainer(threading.Thread):
# important that start_time is utc because recordings are stored and compared in utc
start_time = datetime.datetime.strptime(
date, CACHE_SEGMENT_FORMAT
- ).astimezone(datetime.timezone.utc)
+ ).astimezone(datetime.UTC)
grouped_recordings[camera].append(
{
@@ -301,9 +303,9 @@ class RecordingMaintainer(threading.Thread):
RecordingsDataTypeEnum.saved.value,
)
- recordings_to_insert: list[Optional[dict[str, Any]]] = await asyncio.gather(
- *tasks
- )
+ self._expire_stale_recordings_info(grouped_recordings)
+
+ recordings_to_insert: list[dict[str, Any] | None] = await asyncio.gather(*tasks)
# fire and forget recordings entries
self.requestor.send_data(
@@ -311,13 +313,28 @@ class RecordingMaintainer(threading.Thread):
[r for r in recordings_to_insert if r is not None],
)
+ def _expire_stale_recordings_info(
+ self, grouped_recordings: defaultdict[str, list[dict[str, Any]]]
+ ) -> None:
+ expire_before = datetime.datetime.now().timestamp() - STALE_RECORDINGS_INFO_TTL
+ for recordings_info in (
+ self.object_recordings_info,
+ self.audio_recordings_info,
+ ):
+ for camera in list(recordings_info.keys()):
+ if camera in grouped_recordings:
+ continue
+ info = recordings_info[camera]
+ while info and info[0][0] < expire_before:
+ info.pop(0)
+
def drop_segment(self, cache_path: str) -> None:
Path(cache_path).unlink(missing_ok=True)
self.end_time_cache.pop(cache_path, None)
async def validate_and_move_segment(
self, camera: str, reviews: Any, recording: dict[str, Any]
- ) -> Optional[dict[str, Any]]:
+ ) -> dict[str, Any] | None:
cache_path: str = recording["cache_path"]
start_time: datetime.datetime = recording["start_time"]
@@ -394,7 +411,7 @@ class RecordingMaintainer(threading.Thread):
if (
datetime.datetime.fromtimestamp(
most_recently_processed_frame_time
- ).astimezone(datetime.timezone.utc)
+ ).astimezone(datetime.UTC)
>= end_time
):
record_mode = (
@@ -464,17 +481,19 @@ class RecordingMaintainer(threading.Thread):
self.drop_segment(cache_path)
return None
- # if it doesn't overlap with an review item, go ahead and drop the segment
- # if it ends more than the configured pre_capture for the camera
- # BUT only if continuous/motion is NOT enabled (otherwise wait for processing)
- elif highest is None:
+ # if it doesn't overlap with a review item, drop the segment once it
+ # ends more than event_pre_capture before the most recently processed
+ # frame. at this point we've already decided not to keep it for
+ # continuous/motion retention (either disabled or segment_stats said
+ # discard), so waiting longer just fills the cache.
+ else:
camera_info = self.object_recordings_info[camera]
most_recently_processed_frame_time = (
camera_info[-1][0] if len(camera_info) > 0 else 0
)
retain_cutoff = datetime.datetime.fromtimestamp(
most_recently_processed_frame_time - record_config.event_pre_capture
- ).astimezone(datetime.timezone.utc)
+ ).astimezone(datetime.UTC)
if end_time < retain_cutoff:
self.drop_segment(cache_path)
@@ -600,7 +619,7 @@ class RecordingMaintainer(threading.Thread):
duration: float,
cache_path: str,
segment_info: SegmentInfo,
- ) -> Optional[dict[str, Any]]:
+ ) -> dict[str, Any] | None:
# directory will be in utc due to start_time being in utc
directory = os.path.join(
RECORD_DIR,
@@ -608,8 +627,7 @@ class RecordingMaintainer(threading.Thread):
camera,
)
- if not os.path.exists(directory):
- os.makedirs(directory)
+ os.makedirs(directory, exist_ok=True)
# file will be in utc due to start_time being in utc
file_name = f"{start_time.strftime('%M.%S.mp4')}"
@@ -630,6 +648,8 @@ class RecordingMaintainer(threading.Thread):
"copy",
"-movflags",
"+faststart",
+ "-metadata",
+ f"creation_time={start_time.strftime('%Y-%m-%dT%H:%M:%S.%fZ')}",
file_path,
stderr=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.DEVNULL,
diff --git a/frigate/review/maintainer.py b/frigate/review/maintainer.py
index cfc59744c3..f3abc1bba3 100644
--- a/frigate/review/maintainer.py
+++ b/frigate/review/maintainer.py
@@ -11,7 +11,7 @@ import sys
import threading
from multiprocessing.synchronize import Event as MpEvent
from pathlib import Path
-from typing import Any, Optional
+from typing import Any
import cv2
import numpy as np
@@ -267,7 +267,7 @@ class ReviewSegmentMaintainer(threading.Thread):
def __init__(self, config: FrigateConfig, stop_event: MpEvent):
super().__init__(name="review_segment_maintainer")
self.config = config
- self.active_review_segments: dict[str, Optional[PendingReviewSegment]] = {}
+ self.active_review_segments: dict[str, PendingReviewSegment | None] = {}
self.frame_manager = SharedMemoryFrameManager()
# create communication for review segments
@@ -323,7 +323,7 @@ class ReviewSegmentMaintainer(threading.Thread):
self,
segment: PendingReviewSegment,
camera_config: CameraConfig,
- frame: Optional[np.ndarray],
+ frame: np.ndarray | None,
objects: list[dict[str, Any]],
prev_data: dict[str, Any],
) -> None:
diff --git a/frigate/service_manager/multiprocessing.py b/frigate/service_manager/multiprocessing.py
index 87bb4ffeea..88d497dd66 100644
--- a/frigate/service_manager/multiprocessing.py
+++ b/frigate/service_manager/multiprocessing.py
@@ -9,7 +9,6 @@ from abc import ABC, abstractmethod
from asyncio.exceptions import TimeoutError
from logging.handlers import QueueHandler
from types import FrameType
-from typing import Optional
import frigate.log
@@ -22,13 +21,13 @@ DEFAULT_STOP_TIMEOUT = 10 # seconds
class BaseServiceProcess(Service, ABC):
"""A Service the manages a multiprocessing.Process."""
- _process: Optional[mp.Process]
+ _process: mp.Process | None
def __init__(
self,
*,
- name: Optional[str] = None,
- manager: Optional[ServiceManager] = None,
+ name: str | None = None,
+ manager: ServiceManager | None = None,
) -> None:
super().__init__(name=name, manager=manager)
@@ -55,7 +54,7 @@ class BaseServiceProcess(Service, ABC):
self,
*,
force: bool = False,
- timeout: Optional[float] = None,
+ timeout: float | None = None,
) -> None:
if timeout is None:
timeout = DEFAULT_STOP_TIMEOUT
@@ -85,7 +84,7 @@ class BaseServiceProcess(Service, ABC):
self.manager.logger.info(f"{self.name} stopped")
@property
- def pid(self) -> Optional[int]:
+ def pid(self) -> int | None:
return self._process.pid if self._process else None
def _run(self) -> None:
@@ -143,7 +142,7 @@ class ServiceProcess(BaseServiceProcess):
faulthandler.enable()
- def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None:
+ def receiveSignal(signalNumber: int, frame: FrameType | None) -> None:
# Get the stop_event through the dict to bypass lazy initialization.
stop_event = self.__dict__.get("stop_event")
if stop_event is not None:
diff --git a/frigate/service_manager/multiprocessing_waiter.py b/frigate/service_manager/multiprocessing_waiter.py
index 8acdf583c7..e356fac1d6 100644
--- a/frigate/service_manager/multiprocessing_waiter.py
+++ b/frigate/service_manager/multiprocessing_waiter.py
@@ -7,7 +7,7 @@ import threading
from multiprocessing.connection import Connection
from multiprocessing.connection import wait as mp_wait
from socket import socket
-from typing import Any, Optional, Union
+from typing import Any
logger = logging.getLogger(__name__)
@@ -118,10 +118,10 @@ class MultiprocessingWaiter(threading.Thread):
waiter_lock = threading.Lock()
-waiter_thread: Optional[MultiprocessingWaiter] = None
+waiter_thread: MultiprocessingWaiter | None = None
-async def wait(object: Union[mp.Process, Connection, socket]) -> None:
+async def wait(object: mp.Process | Connection | socket) -> None:
"""Wait for the supplied object to be ready.
Under the hood, this uses multiprocessing.connection.wait() and a background thread manage the
@@ -129,7 +129,7 @@ async def wait(object: Union[mp.Process, Connection, socket]) -> None:
"""
global waiter_thread, waiter_lock
- sentinel: Union[Connection, socket, int]
+ sentinel: Connection | socket | int
if isinstance(object, mp.Process):
sentinel = object.sentinel
elif isinstance(object, Connection) or isinstance(object, socket):
diff --git a/frigate/service_manager/service.py b/frigate/service_manager/service.py
index 89d766e9d6..34631b6210 100644
--- a/frigate/service_manager/service.py
+++ b/frigate/service_manager/service.py
@@ -5,12 +5,11 @@ import atexit
import logging
import threading
from abc import ABC, abstractmethod
+from collections.abc import Coroutine
from contextvars import ContextVar
from dataclasses import dataclass
from functools import partial
-from typing import Coroutine, Optional, Union, cast
-
-from typing_extensions import Self
+from typing import Self, cast
class Service(ABC):
@@ -19,8 +18,8 @@ class Service(ABC):
def __init__(
self,
*,
- name: Optional[str] = None,
- manager: Optional[ServiceManager] = None,
+ name: str | None = None,
+ manager: ServiceManager | None = None,
):
if name:
self.__dict__["name"] = name
@@ -42,13 +41,13 @@ class Service(ABC):
try:
return self.__manager
except AttributeError:
- raise RuntimeError("Cannot access associated service manager")
+ raise RuntimeError("Cannot access associated service manager") from None
def start(
self,
*,
wait: bool = False,
- wait_timeout: Optional[float] = None,
+ wait_timeout: float | None = None,
) -> Self:
"""Start this service.
@@ -70,9 +69,9 @@ class Service(ABC):
self,
*,
force: bool = False,
- timeout: Optional[float] = None,
+ timeout: float | None = None,
wait: bool = False,
- wait_timeout: Optional[float] = None,
+ wait_timeout: float | None = None,
) -> Self:
"""Stop this service.
@@ -97,9 +96,9 @@ class Service(ABC):
self,
*,
force: bool = False,
- stop_timeout: Optional[float] = None,
+ stop_timeout: float | None = None,
wait: bool = False,
- wait_timeout: Optional[float] = None,
+ wait_timeout: float | None = None,
) -> Self:
"""Restart this service.
@@ -129,7 +128,7 @@ class Service(ABC):
self,
*,
force: bool = False,
- timeout: Optional[float] = None,
+ timeout: float | None = None,
) -> None:
pass
@@ -137,14 +136,14 @@ class Service(ABC):
self,
*,
force: bool = False,
- stop_timeout: Optional[float] = None,
+ stop_timeout: float | None = None,
) -> None:
await self.on_stop(force=force, timeout=stop_timeout)
await self.on_start()
default_service_manager_lock = threading.Lock()
-default_service_manager: Optional[ServiceManager] = None
+default_service_manager: ServiceManager | None = None
current_service_manager: ContextVar[ServiceManager] = ContextVar(
"current_service_manager"
@@ -162,8 +161,8 @@ class Command:
"""
coro: Coroutine
- lock: Optional[asyncio.Lock] = None
- done: Optional[threading.Event] = None
+ lock: asyncio.Lock | None = None
+ done: threading.Event | None = None
class ServiceManager:
@@ -189,7 +188,7 @@ class ServiceManager:
_services_lock: threading.Lock
# Commands will be queued with associated event loop. Queueing `None` signals shutdown.
- _command_queue: asyncio.Queue[Union[Command, None]]
+ _command_queue: asyncio.Queue[Command | None]
_event_loop: asyncio.AbstractEventLoop
# The pending command counter is used to ensure all commands have been queued before shutdown.
@@ -204,7 +203,7 @@ class ServiceManager:
# Will be acquired to ensure the shutdown sentinel is sent only once. Never released.
_shutdown_lock: threading.Lock
- def __init__(self, *, name: Optional[str] = None):
+ def __init__(self, *, name: str | None = None):
self._name = name if name is not None else (__package__ or __name__)
self._logger = logging.getLogger(self.name)
@@ -276,8 +275,8 @@ class ServiceManager:
coro: Coroutine,
*,
wait: bool = False,
- wait_timeout: Optional[float] = None,
- lock: Optional[asyncio.Lock] = None,
+ wait_timeout: float | None = None,
+ lock: asyncio.Lock | None = None,
) -> None:
"""Run an async task in the service manager thread.
@@ -299,7 +298,7 @@ class ServiceManager:
cmd.done.wait(timeout=wait_timeout)
def shutdown(
- self, *, wait: bool = False, wait_timeout: Optional[float] = None
+ self, *, wait: bool = False, wait_timeout: float | None = None
) -> None:
"""Shutdown the service manager thread.
@@ -321,7 +320,7 @@ class ServiceManager:
if not self._manager_thread.is_alive():
raise RuntimeError(f"ServiceManager {self.name} is not running")
- def _send_command(self, command: Union[Command, None]) -> None:
+ def _send_command(self, command: Command | None) -> None:
self._ensure_running()
async def queue_command() -> None:
@@ -336,7 +335,7 @@ class ServiceManager:
self._ensure_running()
with self._services_lock:
- name_conflict: Optional[Service] = next(
+ name_conflict: Service | None = next(
(
existing
for name, existing in self._services.items()
diff --git a/frigate/stats/emitter.py b/frigate/stats/emitter.py
index 2b34c7c4ed..7c2e328c73 100644
--- a/frigate/stats/emitter.py
+++ b/frigate/stats/emitter.py
@@ -6,7 +6,7 @@ import logging
import threading
import time
from multiprocessing.synchronize import Event as MpEvent
-from typing import Any, Optional
+from typing import Any
from frigate.comms.inter_process import InterProcessRequestor
from frigate.config import FrigateConfig
@@ -32,7 +32,7 @@ class StatsEmitter(threading.Thread):
self.config = config
self.stats_tracking = stats_tracking
self.stop_event = stop_event
- self.hwaccel_errors: list[str] = []
+ self.hwaccel_errors: dict[str, float] = {}
self.stats_history: list[dict[str, Any]] = []
# create communication for stats
@@ -49,9 +49,7 @@ class StatsEmitter(threading.Thread):
self.stats_history.append(stats)
return stats
- def get_stats_history(
- self, keys: Optional[list[str]] = None
- ) -> list[dict[str, Any]]:
+ def get_stats_history(self, keys: list[str] | None = None) -> list[dict[str, Any]]:
"""Get stats history.
Supports dot-notation for nested keys to avoid returning large objects
diff --git a/frigate/stats/intel_gpu_info.py b/frigate/stats/intel_gpu_info.py
new file mode 100644
index 0000000000..5bbb5c335b
--- /dev/null
+++ b/frigate/stats/intel_gpu_info.py
@@ -0,0 +1,108 @@
+"""Resolve human-readable names for Intel GPUs via OpenVINO."""
+
+import logging
+import re
+
+logger = logging.getLogger(__name__)
+
+
+class IntelGpuNameResolver:
+ """Build a pdev -> normalized device name map by enumerating OpenVINO GPUs.
+
+ The lookup is performed once on first access and cached for the process
+ lifetime. OpenVINO exposes DEVICE_PCI_INFO (domain/bus/device/function) and
+ FULL_DEVICE_NAME for each GPU it can see, which is enough to associate the
+ name with the pdev string used by DRM fdinfo.
+ """
+
+ _names: dict[str, str] | None = None
+
+ def get_names(self) -> dict[str, str]:
+ if self._names is not None:
+ return self._names
+
+ names: dict[str, str] = {}
+
+ try:
+ from openvino import Core
+ except ImportError:
+ logger.debug("OpenVINO unavailable; cannot resolve Intel GPU names")
+ self._names = names
+ return names
+
+ try:
+ core = Core()
+ devices = core.available_devices
+ except Exception as exc:
+ logger.debug(f"OpenVINO Core initialization failed: {exc}")
+ self._names = names
+ return names
+
+ cpu_name: str | None = None
+ if "CPU" in devices:
+ try:
+ cpu_name = self._strip_trademarks(
+ core.get_property("CPU", "FULL_DEVICE_NAME")
+ )
+ except Exception as exc:
+ logger.debug(f"Failed to read CPU FULL_DEVICE_NAME: {exc}")
+
+ for device in devices:
+ if not device.startswith("GPU"):
+ continue
+
+ try:
+ pci = core.get_property(device, "DEVICE_PCI_INFO")
+ raw_name = core.get_property(device, "FULL_DEVICE_NAME")
+ device_type = core.get_property(device, "DEVICE_TYPE")
+ except Exception as exc:
+ logger.debug(f"Failed to read properties for {device}: {exc}")
+ continue
+
+ pdev = self._format_pdev(pci)
+ if not pdev:
+ continue
+
+ names[pdev] = self._resolve_name(raw_name, device_type, cpu_name)
+
+ self._names = names
+ return names
+
+ @staticmethod
+ def _format_pdev(pci) -> str | None:
+ try:
+ return f"{pci.domain:04x}:{pci.bus:02x}:{pci.device:02x}.{pci.function:x}"
+ except AttributeError:
+ return None
+
+ @classmethod
+ def _resolve_name(cls, raw_name: str, device_type, cpu_name: str | None) -> str:
+ """Build a display name for a GPU.
+
+ Modern integrated Intel GPUs are reported by OpenVINO with a generic
+ FULL_DEVICE_NAME like "Intel(R) Graphics (iGPU)" that gives no model
+ information. Since the iGPU is part of the CPU on these platforms, fall
+ back to the CPU name (which OpenVINO does report specifically) and
+ suffix it with "iGPU" so it's clear what the entry is.
+ """
+ is_integrated = "INTEGRATED" in str(device_type).upper()
+
+ if is_integrated and cpu_name:
+ short_cpu = re.sub(r"^Intel\s+", "", cpu_name)
+ return f"{short_cpu} iGPU"
+
+ return cls._normalize_name(raw_name)
+
+ @classmethod
+ def _normalize_name(cls, name: str) -> str:
+ cleaned = cls._strip_trademarks(name)
+ cleaned = re.sub(r"\s*\((?:i|d)GPU\)\s*$", "", cleaned, flags=re.IGNORECASE)
+ return " ".join(cleaned.split())
+
+ @staticmethod
+ def _strip_trademarks(name: str) -> str:
+ cleaned = re.sub(r"\(R\)|\(TM\)", "", name)
+ return " ".join(cleaned.split())
+
+
+intel_gpu_name_resolver = IntelGpuNameResolver()
diff --git a/frigate/stats/prometheus.py b/frigate/stats/prometheus.py
index d2e2295686..9797d569e1 100644
--- a/frigate/stats/prometheus.py
+++ b/frigate/stats/prometheus.py
@@ -1,6 +1,6 @@
import logging
import re
-from typing import Any, Dict, List
+from typing import Any
from prometheus_client import CONTENT_TYPE_LATEST, generate_latest
from prometheus_client.core import (
@@ -11,7 +11,7 @@ from prometheus_client.core import (
)
-class CustomCollector(object):
+class CustomCollector:
def __init__(self, _url):
self.complete_stats = {} # Store complete stats data
self.process_stats = {} # Keep for CPU processing
@@ -491,7 +491,7 @@ collector = CustomCollector(None)
REGISTRY.register(collector)
-def update_metrics(stats: Dict[str, Any], event_counts: List[Dict[str, Any]]):
+def update_metrics(stats: dict[str, Any], event_counts: list[dict[str, Any]]):
"""Updates the Prometheus metrics with the given stats data."""
try:
# Store the complete stats for later use by collect()
diff --git a/frigate/stats/util.py b/frigate/stats/util.py
index 07b410ad21..d3cb81986a 100644
--- a/frigate/stats/util.py
+++ b/frigate/stats/util.py
@@ -1,12 +1,13 @@
"""Utilities for stats."""
import asyncio
+import logging
import os
import shutil
import time
from json import JSONDecodeError
from multiprocessing.managers import DictProxy
-from typing import Any, Optional
+from typing import Any
import requests
from requests.exceptions import RequestException
@@ -34,6 +35,10 @@ from frigate.util.services import (
)
from frigate.version import VERSION
+logger = logging.getLogger(__name__)
+
+HWACCEL_ERROR_COOLDOWN_SECONDS = 3600
+
def get_latest_version(config: FrigateConfig) -> str:
if not config.telemetry.version_check:
@@ -73,7 +78,7 @@ def stats_init(
return stats_tracking
-def read_temperature(path: str) -> Optional[float]:
+def read_temperature(path: str) -> float | None:
if os.path.isfile(path):
with open(path) as f:
line = f.readline().strip()
@@ -101,7 +106,7 @@ def get_temperatures() -> dict[str, float]:
def get_detector_temperature(
detector_type: str,
detector_index_by_type: dict[str, int],
-) -> Optional[float]:
+) -> float | None:
"""Get temperature for a specific detector based on its type."""
if detector_type == "edgetpu":
# Get temperatures for all attached Corals
@@ -167,7 +172,9 @@ def get_detector_stats(
def get_processing_stats(
- config: FrigateConfig, stats: dict[str, str], hwaccel_errors: list[str]
+ config: FrigateConfig,
+ stats: dict[str, str],
+ hwaccel_errors: dict[str, float],
) -> None:
"""Get stats for cpu / gpu."""
@@ -206,7 +213,9 @@ async def set_bandwidth_stats(config: FrigateConfig, all_stats: dict[str, Any])
async def set_gpu_stats(
- config: FrigateConfig, all_stats: dict[str, Any], hwaccel_errors: list[str]
+ config: FrigateConfig,
+ all_stats: dict[str, Any],
+ hwaccel_errors: dict[str, float],
) -> None:
"""Parse GPUs from hwaccel args and use for stats."""
hwaccel_args = []
@@ -230,18 +239,24 @@ async def set_gpu_stats(
hwaccel_args.append(args)
stats: dict[str, dict] = {}
+ intel_gpu_collected = False
+ now = time.monotonic()
for args in hwaccel_args:
- if args in hwaccel_errors:
- # known erroring args should automatically return as error
- stats["error-gpu"] = {"gpu": "", "mem": ""}
- elif "cuvid" in args or "nvidia" in args:
+ last_error = hwaccel_errors.get(args)
+ if last_error is not None:
+ if now - last_error < HWACCEL_ERROR_COOLDOWN_SECONDS:
+ continue
+ hwaccel_errors.pop(args, None)
+
+ if "cuvid" in args or "nvidia" in args:
# nvidia GPU
nvidia_usage = get_nvidia_gpu_stats()
if nvidia_usage:
for i in range(len(nvidia_usage)):
stats[nvidia_usage[i]["name"]] = {
+ "vendor": "nvidia",
"gpu": str(round(float(nvidia_usage[i]["gpu"]), 2)) + "%",
"mem": str(round(float(nvidia_usage[i]["mem"]), 2)) + "%",
"enc": str(round(float(nvidia_usage[i]["enc"]), 2)) + "%",
@@ -250,32 +265,35 @@ async def set_gpu_stats(
}
else:
- stats["nvidia-gpu"] = {"gpu": "", "mem": ""}
- hwaccel_errors.append(args)
+ stats["nvidia-gpu"] = {"vendor": "nvidia", "gpu": "", "mem": ""}
+ hwaccel_errors[args] = time.monotonic()
elif "nvmpi" in args or "jetson" in args:
# nvidia Jetson
jetson_usage = get_jetson_stats()
if jetson_usage:
- stats["jetson-gpu"] = jetson_usage
+ stats["jetson-gpu"] = {"vendor": "nvidia", **jetson_usage}
else:
- stats["jetson-gpu"] = {"gpu": "", "mem": ""}
- hwaccel_errors.append(args)
+ stats["jetson-gpu"] = {"vendor": "nvidia", "gpu": "", "mem": ""}
+ hwaccel_errors[args] = time.monotonic()
elif "qsv" in args or ("vaapi" in args and not is_vaapi_amd_driver()):
if not config.telemetry.stats.intel_gpu_stats:
continue
- if "intel-gpu" not in stats:
+ if not intel_gpu_collected:
# intel GPU (QSV or VAAPI both use the same physical GPU)
+ intel_gpu_collected = True
intel_usage = get_intel_gpu_stats(
config.telemetry.stats.intel_gpu_device
)
- if intel_usage is not None:
- stats["intel-gpu"] = intel_usage or {"gpu": "", "mem": ""}
+ if intel_usage:
+ for entry in intel_usage.values():
+ name = entry.pop("name")
+ stats[name] = entry
else:
- stats["intel-gpu"] = {"gpu": "", "mem": ""}
- hwaccel_errors.append(args)
+ stats["intel-gpu"] = {"vendor": "intel", "gpu": "", "mem": ""}
+ hwaccel_errors[args] = time.monotonic()
elif "vaapi" in args:
if not config.telemetry.stats.amd_gpu_stats:
continue
@@ -284,18 +302,18 @@ async def set_gpu_stats(
amd_usage = get_amd_gpu_stats()
if amd_usage:
- stats["amd-vaapi"] = amd_usage
+ stats["amd-vaapi"] = {"vendor": "amd", **amd_usage}
else:
- stats["amd-vaapi"] = {"gpu": "", "mem": ""}
- hwaccel_errors.append(args)
+ stats["amd-vaapi"] = {"vendor": "amd", "gpu": "", "mem": ""}
+ hwaccel_errors[args] = time.monotonic()
elif "preset-rk" in args:
rga_usage = get_rockchip_gpu_stats()
if rga_usage:
- stats["rockchip"] = rga_usage
+ stats["rockchip"] = {"vendor": "rockchip", **rga_usage}
elif "v4l2m2m" in args or "rpi" in args:
# RPi v4l2m2m is currently not able to get usage stats
- stats["rpi-v4l2m2m"] = {"gpu": "", "mem": ""}
+ stats["rpi-v4l2m2m"] = {"vendor": "rpi", "gpu": "", "mem": ""}
if stats:
all_stats["gpu_usages"] = stats
@@ -323,7 +341,9 @@ async def set_npu_usages(config: FrigateConfig, all_stats: dict[str, Any]) -> No
def stats_snapshot(
- config: FrigateConfig, stats_tracking: StatsTrackingTypes, hwaccel_errors: list[str]
+ config: FrigateConfig,
+ stats_tracking: StatsTrackingTypes,
+ hwaccel_errors: dict[str, float],
) -> dict[str, Any]:
"""Get a snapshot of the current stats that are being tracked."""
camera_metrics = stats_tracking["camera_metrics"]
diff --git a/frigate/storage.py b/frigate/storage.py
index 8cc199a1b3..585a5d87f1 100644
--- a/frigate/storage.py
+++ b/frigate/storage.py
@@ -197,7 +197,7 @@ class StorageMaintainer(threading.Thread):
# check if need to delete retained segments
if deleted_segments_size < hourly_bandwidth:
logger.error(
- f"Could not clear {hourly_bandwidth} MB, currently {deleted_segments_size} MB have been cleared. Retained recordings must be deleted."
+ f"Could not clear {hourly_bandwidth} MB, currently {deleted_segments_size:.2f} MB have been cleared. Retained recordings must be deleted."
)
recordings = (
Recordings.select(
@@ -225,7 +225,7 @@ class StorageMaintainer(threading.Thread):
# this file was not found so we must assume no space was cleaned up
pass
else:
- logger.info(f"Cleaned up {deleted_segments_size} MB of recordings")
+ logger.info(f"Cleaned up {deleted_segments_size:.2f} MB of recordings")
logger.debug(f"Expiring {len(deleted_recordings)} recordings")
# delete up to 100,000 at a time
diff --git a/frigate/test/http_api/test_debug_replay_api.py b/frigate/test/http_api/test_debug_replay_api.py
new file mode 100644
index 0000000000..be4e7f496f
--- /dev/null
+++ b/frigate/test/http_api/test_debug_replay_api.py
@@ -0,0 +1,124 @@
+"""Tests for /debug_replay API endpoints."""
+
+from unittest.mock import patch
+
+from frigate.models import Event, Recordings, ReviewSegment
+from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp
+
+
+class TestDebugReplayAPI(BaseTestHttp):
+ def setUp(self):
+ super().setUp([Event, Recordings, ReviewSegment])
+ self.app = self.create_app()
+
+ def test_start_returns_202_with_job_id(self):
+ # Stub the factory to skip validation/threading and just record the
+ # name on the manager the way the real factory's mark_starting would.
+ def fake_start(**kwargs):
+ source = kwargs["source"]
+ kwargs["replay_manager"].mark_starting(
+ source_camera=source.source_camera,
+ replay_camera_name="_replay_front",
+ start_ts=source.start_ts,
+ end_ts=source.end_ts,
+ )
+ return "job-1234"
+
+ with patch(
+ "frigate.api.debug_replay.start_debug_replay_job",
+ side_effect=fake_start,
+ ):
+ with AuthTestClient(self.app) as client:
+ resp = client.post(
+ "/debug_replay/start",
+ json={
+ "camera": "front",
+ "start_time": 100,
+ "end_time": 200,
+ },
+ )
+
+ self.assertEqual(resp.status_code, 202)
+ body = resp.json()
+ self.assertTrue(body["success"])
+ self.assertEqual(body["job_id"], "job-1234")
+ self.assertEqual(body["replay_camera"], "_replay_front")
+
+ def test_start_returns_400_on_validation_error(self):
+ with patch(
+ "frigate.api.debug_replay.start_debug_replay_job",
+ side_effect=ValueError("Camera 'missing' not found"),
+ ):
+ with AuthTestClient(self.app) as client:
+ resp = client.post(
+ "/debug_replay/start",
+ json={
+ "camera": "missing",
+ "start_time": 100,
+ "end_time": 200,
+ },
+ )
+
+ self.assertEqual(resp.status_code, 400)
+ body = resp.json()
+ self.assertFalse(body["success"])
+ # Message is hard-coded so we don't echo exception text back to clients
+ # (CodeQL: information exposure through an exception).
+ self.assertEqual(body["message"], "Invalid debug replay parameters")
+
+ def test_start_returns_409_when_session_already_active(self):
+ with patch(
+ "frigate.api.debug_replay.start_debug_replay_job",
+ side_effect=RuntimeError("A replay session is already active"),
+ ):
+ with AuthTestClient(self.app) as client:
+ resp = client.post(
+ "/debug_replay/start",
+ json={
+ "camera": "front",
+ "start_time": 100,
+ "end_time": 200,
+ },
+ )
+
+ self.assertEqual(resp.status_code, 409)
+ body = resp.json()
+ self.assertFalse(body["success"])
+
+ def test_status_inactive_when_no_session(self):
+ with AuthTestClient(self.app) as client:
+ resp = client.get("/debug_replay/status")
+
+ self.assertEqual(resp.status_code, 200)
+ body = resp.json()
+ self.assertFalse(body["active"])
+ self.assertIsNone(body["replay_camera"])
+ self.assertIsNone(body["source_camera"])
+ self.assertIsNone(body["start_time"])
+ self.assertIsNone(body["end_time"])
+ self.assertFalse(body["live_ready"])
+ # Make sure deprecated fields are gone
+ self.assertNotIn("state", body)
+ self.assertNotIn("progress_percent", body)
+ self.assertNotIn("error_message", body)
+
+ def test_status_active_after_mark_starting(self):
+ manager = self.app.replay_manager
+ manager.mark_starting(
+ source_camera="front",
+ replay_camera_name="_replay_front",
+ start_ts=100.0,
+ end_ts=200.0,
+ )
+
+ with AuthTestClient(self.app) as client:
+ resp = client.get("/debug_replay/status")
+
+ self.assertEqual(resp.status_code, 200)
+ body = resp.json()
+ self.assertTrue(body["active"])
+ self.assertEqual(body["replay_camera"], "_replay_front")
+ self.assertEqual(body["source_camera"], "front")
+ self.assertEqual(body["start_time"], 100.0)
+ self.assertEqual(body["end_time"], 200.0)
+ self.assertFalse(body["live_ready"])
diff --git a/frigate/test/http_api/test_http_app.py b/frigate/test/http_api/test_http_app.py
index bf8e9c72a9..4c581dd426 100644
--- a/frigate/test/http_api/test_http_app.py
+++ b/frigate/test/http_api/test_http_app.py
@@ -1,5 +1,9 @@
-from unittest.mock import Mock
+from unittest.mock import Mock, patch
+import frigate.genai
+from frigate.config import GenAIProviderEnum
+from frigate.const import REDACTED_CREDENTIAL_SENTINEL
+from frigate.genai import GenAIClient
from frigate.models import Event, Recordings, ReviewSegment
from frigate.stats.emitter import StatsEmitter
from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp
@@ -23,6 +27,26 @@ class TestHttpApp(BaseTestHttp):
response_json = response.json()
assert response_json == self.test_stats
+ def test_recordings_storage_requires_admin(self):
+ stats = Mock(spec=StatsEmitter)
+ stats.get_latest_stats.return_value = self.test_stats
+ app = super().create_app(stats)
+ app.storage_maintainer = Mock()
+ app.storage_maintainer.calculate_camera_usages.return_value = {
+ "front_door": {"usage": 2.0},
+ }
+
+ with AuthTestClient(app) as client:
+ response = client.get(
+ "/recordings/storage",
+ headers={"remote-user": "viewer", "remote-role": "viewer"},
+ )
+ assert response.status_code == 403
+
+ response = client.get("/recordings/storage")
+ assert response.status_code == 200
+ assert response.json()["front_door"]["usage_percent"] == 25.0
+
def test_config_set_in_memory_replaces_objects_track_list(self):
self.minimal_config["cameras"]["front_door"]["objects"] = {
"track": ["person", "car"],
@@ -51,3 +75,108 @@ class TestHttpApp(BaseTestHttp):
assert response.status_code == 200
assert app.frigate_config.cameras["front_door"].objects.track == ["person"]
+
+ ####################################################################################################################
+ ################################### Credential redaction sentinel ################################################
+ ####################################################################################################################
+ def test_config_response_redacts_mqtt_password_with_sentinel(self):
+ self.minimal_config["mqtt"]["user"] = "mqttuser"
+ self.minimal_config["mqtt"]["password"] = "supersecret"
+ app = super().create_app()
+
+ with AuthTestClient(app) as client:
+ response = client.get("/config")
+ assert response.status_code == 200
+ mqtt = response.json()["mqtt"]
+ assert mqtt["password"] == REDACTED_CREDENTIAL_SENTINEL
+
+ ####################################################################################################################
+ ################################### POST /genai/probe Endpoint ##################################################
+ ####################################################################################################################
+ def test_genai_probe_requires_admin(self):
+ app = super().create_app()
+
+ with AuthTestClient(app) as client:
+ response = client.post(
+ "/genai/probe",
+ json={"provider": "openai"},
+ headers={"remote-user": "viewer", "remote-role": "viewer"},
+ )
+ assert response.status_code == 403
+
+ def test_genai_probe_returns_models_from_transient_client(self):
+ class FakeClient(GenAIClient):
+ def list_models(self):
+ return ["fake-model-a", "fake-model-b"]
+
+ app = super().create_app()
+
+ with (
+ AuthTestClient(app) as client,
+ patch.dict(
+ frigate.genai.PROVIDERS,
+ {GenAIProviderEnum.openai: FakeClient},
+ ),
+ ):
+ response = client.post(
+ "/genai/probe",
+ json={
+ "provider": "openai",
+ "api_key": "sk-test",
+ "base_url": "https://example.invalid",
+ },
+ )
+ assert response.status_code == 200
+ assert response.json() == {
+ "success": True,
+ "models": ["fake-model-a", "fake-model-b"],
+ }
+
+ def test_genai_probe_empty_list_is_treated_as_failure(self):
+ # The plugin's list_models() returns [] on connection failure rather
+ # than raising. The endpoint should surface that as success=false so
+ # the UI can show a meaningful error.
+ class EmptyClient(GenAIClient):
+ def list_models(self):
+ return []
+
+ app = super().create_app()
+
+ with (
+ AuthTestClient(app) as client,
+ patch.dict(
+ frigate.genai.PROVIDERS,
+ {GenAIProviderEnum.openai: EmptyClient},
+ ),
+ ):
+ response = client.post(
+ "/genai/probe",
+ json={"provider": "openai"},
+ )
+ assert response.status_code == 200
+ payload = response.json()
+ assert payload["success"] is False
+ assert "message" in payload
+
+ def test_genai_probe_handles_provider_failure(self):
+ class FailingClient(GenAIClient):
+ def list_models(self):
+ raise RuntimeError("provider unreachable")
+
+ app = super().create_app()
+
+ with (
+ AuthTestClient(app) as client,
+ patch.dict(
+ frigate.genai.PROVIDERS,
+ {GenAIProviderEnum.openai: FailingClient},
+ ),
+ ):
+ response = client.post(
+ "/genai/probe",
+ json={"provider": "openai"},
+ )
+ assert response.status_code == 200
+ payload = response.json()
+ assert payload["success"] is False
+ assert "message" in payload
diff --git a/frigate/test/http_api/test_http_camera_access.py b/frigate/test/http_api/test_http_camera_access.py
index 211c84bb4f..44520d79f5 100644
--- a/frigate/test/http_api/test_http_camera_access.py
+++ b/frigate/test/http_api/test_http_camera_access.py
@@ -1,3 +1,4 @@
+import os
from unittest.mock import patch
from fastapi import HTTPException, Request
@@ -357,6 +358,51 @@ class TestGo2rtcStreamAccess(BaseTestHttp):
f"got {resp.status_code}"
)
+ def test_add_stream_rejects_restricted_source(self):
+ """PUT /go2rtc/streams must reject exec:/echo:/expr: sources even for
+ admins"""
+ app = self._make_app(_MULTI_CAMERA_CONFIG)
+ with AuthTestClient(app) as client:
+ for src in (
+ "exec:/tmp/rev.sh",
+ "echo:foo",
+ "expr:bar",
+ " exec:/tmp/rev.sh",
+ ):
+ resp = client.put(f"/go2rtc/streams/revshell?src={src}")
+ assert resp.status_code == 400, (
+ f"Expected 400 for restricted src {src!r}; got {resp.status_code}"
+ )
+ assert resp.json().get("success") is False
+
+ def test_add_stream_allows_non_restricted_source(self):
+ """A normal stream URL should pass the restricted-source check and reach
+ the (unavailable in tests) go2rtc proxy — so we expect 500, not 400."""
+ app = self._make_app(_MULTI_CAMERA_CONFIG)
+ with AuthTestClient(app) as client:
+ resp = client.put("/go2rtc/streams/legit?src=rtsp://10.0.0.1:554/video")
+ assert resp.status_code != 400, (
+ f"Non-restricted source should not be rejected with 400; got {resp.status_code}"
+ )
+
+ def test_add_stream_allows_restricted_source_when_override_set(self):
+ """When GO2RTC_ALLOW_ARBITRARY_EXEC is set, the API must defer to operator
+ intent and forward the request to go2rtc instead of short-circuiting with 400."""
+ app = self._make_app(_MULTI_CAMERA_CONFIG)
+ mock_response = type("R", (), {"ok": True, "status_code": 200, "text": "ok"})()
+ with patch.dict(os.environ, {"GO2RTC_ALLOW_ARBITRARY_EXEC": "true"}):
+ with patch(
+ "frigate.api.camera.requests.put", return_value=mock_response
+ ) as mock_put:
+ with AuthTestClient(app) as client:
+ resp = client.put("/go2rtc/streams/legit?src=exec:/tmp/something")
+ assert resp.status_code == 200, (
+ f"Restricted src should be forwarded when override set; got {resp.status_code}"
+ )
+ mock_put.assert_called_once()
+ forwarded_src = mock_put.call_args.kwargs["params"]["src"]
+ assert forwarded_src == "exec:/tmp/something"
+
def test_stream_alias_blocked_when_owning_camera_disallowed(self):
"""limited_user cannot access a stream alias that belongs to a camera they
are not allowed to see."""
diff --git a/frigate/test/http_api/test_http_event.py b/frigate/test/http_api/test_http_event.py
index bc7f388e15..8aca6577d9 100644
--- a/frigate/test/http_api/test_http_event.py
+++ b/frigate/test/http_api/test_http_event.py
@@ -219,6 +219,25 @@ class TestHttpApp(BaseTestHttp):
assert len(events) == 1
assert events[0]["id"] == event_id
+ def test_similarity_search_hides_unauthorized_anchor_event(self):
+ mock_embeddings = Mock()
+ self.app.frigate_config.semantic_search.enabled = True
+ self.app.embeddings = mock_embeddings
+
+ with AuthTestClient(self.app) as client:
+ super().insert_mock_event("hidden.anchor", camera="back_door")
+ response = client.get(
+ "/events/search",
+ params={
+ "search_type": "similarity",
+ "event_id": "hidden.anchor",
+ },
+ )
+
+ assert response.status_code == 404
+ assert response.json()["message"] == "Event not found"
+ mock_embeddings.search_thumbnail.assert_not_called()
+
def test_get_good_event(self):
id = "123456.random"
diff --git a/frigate/test/http_api/test_http_keyframe_analysis.py b/frigate/test/http_api/test_http_keyframe_analysis.py
new file mode 100644
index 0000000000..6a49e105ec
--- /dev/null
+++ b/frigate/test/http_api/test_http_keyframe_analysis.py
@@ -0,0 +1,58 @@
+from unittest.mock import AsyncMock, patch
+
+from frigate.models import Event, Recordings, ReviewSegment
+from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp
+
+
+class TestHttpKeyframeAnalysis(BaseTestHttp):
+ def setUp(self):
+ super().setUp([Event, Recordings, ReviewSegment])
+
+ def test_invalid_camera_returns_404(self):
+ app = super().create_app()
+ with AuthTestClient(app) as client:
+ response = client.get("/keyframe_analysis?camera=does_not_exist")
+ assert response.status_code == 404
+
+ def test_record_disabled_returns_neutral(self):
+ # default minimal_config has recording disabled
+ app = super().create_app()
+ with AuthTestClient(app) as client:
+ response = client.get("/keyframe_analysis?camera=front_door")
+ assert response.status_code == 200
+ assert response.json()["severity"] == "record_disabled"
+
+ def test_probes_record_input_and_returns_severity(self):
+ self.minimal_config["cameras"]["front_door"]["ffmpeg"]["inputs"] = [
+ {
+ "path": "rtsp://10.0.0.1:554/record",
+ "roles": ["detect", "record"],
+ }
+ ]
+ self.minimal_config["cameras"]["front_door"]["record"] = {"enabled": True}
+ app = super().create_app()
+
+ canned = {
+ "severity": "ok",
+ "keyframe_count": 5,
+ "max_gap": 1.0,
+ "mean_gap": 1.0,
+ "min_gap": 1.0,
+ "segment_time": 10,
+ "duration_observed": 4.0,
+ "thresholds": {"warning": 4.0, "error": 10},
+ }
+
+ with patch(
+ "frigate.api.camera.analyze_record_keyframes",
+ AsyncMock(return_value=canned),
+ ) as mock_probe:
+ with AuthTestClient(app) as client:
+ response = client.get("/keyframe_analysis?camera=front_door")
+
+ assert response.status_code == 200
+ assert response.json()["severity"] == "ok"
+ # index matches the input carrying the record role ("Stream 1")
+ assert response.json()["stream_index"] == 0
+ # the record-role input path was probed
+ assert mock_probe.await_args.args[1] == "rtsp://10.0.0.1:554/record"
diff --git a/frigate/test/http_api/test_http_media.py b/frigate/test/http_api/test_http_media.py
index 6af3dd9724..b2d83cbc8a 100644
--- a/frigate/test/http_api/test_http_media.py
+++ b/frigate/test/http_api/test_http_media.py
@@ -1,6 +1,6 @@
"""Unit tests for recordings/media API endpoints."""
-from datetime import datetime, timezone
+from datetime import UTC, datetime
import pytz
from fastapi import Request
@@ -306,8 +306,8 @@ class TestHttpMedia(BaseTestHttp):
Test recordings summary with UTC timezone (no DST).
"""
# Use UTC timestamps directly
- march_9_utc = datetime(2024, 3, 9, 17, 0, 0, tzinfo=timezone.utc).timestamp()
- march_10_utc = datetime(2024, 3, 10, 17, 0, 0, tzinfo=timezone.utc).timestamp()
+ march_9_utc = datetime(2024, 3, 9, 17, 0, 0, tzinfo=UTC).timestamp()
+ march_10_utc = datetime(2024, 3, 10, 17, 0, 0, tzinfo=UTC).timestamp()
with AuthTestClient(self.app) as client:
Recordings.insert(
@@ -403,3 +403,127 @@ class TestHttpMedia(BaseTestHttp):
assert len(summary) == 1
assert "2024-03-10" in summary
assert summary["2024-03-10"] is True
+
+ def test_recordings_unavailable_reports_gap_between_recordings(self):
+ """A gap between two recordings is reported as an unavailable segment."""
+ with AuthTestClient(self.app) as client:
+ # Two recordings with a 20s gap (1010-1030) between them.
+ Recordings.insert(
+ id="rec_a",
+ path="/media/recordings/a.mp4",
+ camera="front_door",
+ start_time=1000,
+ end_time=1010,
+ duration=10,
+ motion=0,
+ ).execute()
+ Recordings.insert(
+ id="rec_b",
+ path="/media/recordings/b.mp4",
+ camera="front_door",
+ start_time=1030,
+ end_time=1040,
+ duration=10,
+ motion=0,
+ ).execute()
+
+ response = client.get(
+ "/recordings/unavailable",
+ params={
+ "after": 1000,
+ "before": 1040,
+ "scale": 5,
+ "cameras": "front_door",
+ },
+ )
+
+ assert response.status_code == 200
+ assert response.json() == [{"start_time": 1010, "end_time": 1030}]
+
+ def test_recordings_unavailable_merges_overlapping_recordings(self):
+ """Overlapping recordings are merged so no false gap is reported."""
+ with AuthTestClient(self.app) as client:
+ # Overlapping recordings spanning the whole requested range.
+ Recordings.insert(
+ id="rec_a",
+ path="/media/recordings/a.mp4",
+ camera="front_door",
+ start_time=1000,
+ end_time=1020,
+ duration=20,
+ motion=0,
+ ).execute()
+ Recordings.insert(
+ id="rec_b",
+ path="/media/recordings/b.mp4",
+ camera="front_door",
+ start_time=1010,
+ end_time=1030,
+ duration=20,
+ motion=0,
+ ).execute()
+
+ response = client.get(
+ "/recordings/unavailable",
+ params={
+ "after": 1000,
+ "before": 1030,
+ "scale": 5,
+ "cameras": "front_door",
+ },
+ )
+
+ assert response.status_code == 200
+ assert response.json() == []
+
+ def test_recordings_unavailable_cameras_all_scopes_to_allowed_cameras(self):
+ """cameras=all must not error and must only consider allowed cameras.
+
+ allowed_cameras is mocked to ["front_door"]. A back_door recording that
+ would otherwise fill the gap must be ignored, and the request must not
+ 500 the way it did when cameras was reassigned to a list.
+ """
+ with AuthTestClient(self.app) as client:
+ # front_door has a 20s gap (1010-1030).
+ Recordings.insert(
+ id="front_a",
+ path="/media/recordings/front_a.mp4",
+ camera="front_door",
+ start_time=1000,
+ end_time=1010,
+ duration=10,
+ motion=0,
+ ).execute()
+ Recordings.insert(
+ id="front_b",
+ path="/media/recordings/front_b.mp4",
+ camera="front_door",
+ start_time=1030,
+ end_time=1040,
+ duration=10,
+ motion=0,
+ ).execute()
+ # back_door is not in allowed_cameras; its full-window coverage must
+ # not mask the front_door gap.
+ Recordings.insert(
+ id="back_a",
+ path="/media/recordings/back_a.mp4",
+ camera="back_door",
+ start_time=1000,
+ end_time=1040,
+ duration=40,
+ motion=0,
+ ).execute()
+
+ response = client.get(
+ "/recordings/unavailable",
+ params={
+ "after": 1000,
+ "before": 1040,
+ "scale": 5,
+ "cameras": "all",
+ },
+ )
+
+ assert response.status_code == 200
+ assert response.json() == [{"start_time": 1010, "end_time": 1030}]
diff --git a/frigate/test/http_api/test_http_review.py b/frigate/test/http_api/test_http_review.py
index ca73c87064..d13a7bd273 100644
--- a/frigate/test/http_api/test_http_review.py
+++ b/frigate/test/http_api/test_http_review.py
@@ -610,19 +610,16 @@ class TestHttpReview(BaseTestHttp):
response = client.get("/review/activity/motion", params=params)
assert response.status_code == 200
response_json = response.json()
- assert len(response_json) == 61
+ # Only buckets with an actual recording are returned. Empty
+ # gap-fill buckets between the two recordings are dropped.
+ assert len(response_json) == 2
self.assertDictEqual(
{"motion": 50.5, "camera": "front_door", "start_time": now + 1},
response_json[0],
)
- for item in response_json[1:-1]:
- self.assertDictEqual(
- {"motion": 0.0, "camera": "", "start_time": item["start_time"]},
- item,
- )
self.assertDictEqual(
{"motion": 100.0, "camera": "front_door", "start_time": one_m + 1},
- response_json[len(response_json) - 1],
+ response_json[1],
)
####################################################################################################################
diff --git a/frigate/test/test_builtin.py b/frigate/test/test_builtin.py
new file mode 100644
index 0000000000..7cf47de5c6
--- /dev/null
+++ b/frigate/test/test_builtin.py
@@ -0,0 +1,41 @@
+"""Tests for frigate.util.builtin helpers."""
+
+import unittest
+from unittest.mock import patch
+
+from frigate.util.builtin import EventsPerSecond
+
+
+class TestEventsPerSecond(unittest.TestCase):
+ def test_eps_is_zero_before_any_events(self) -> None:
+ eps = EventsPerSecond()
+ with patch("frigate.util.builtin.time.monotonic", return_value=100.0):
+ self.assertEqual(eps.eps(), 0.0)
+
+ def test_eps_counts_events_in_window(self) -> None:
+ eps = EventsPerSecond(last_n_seconds=10)
+ clock = [1000.0]
+ with patch("frigate.util.builtin.time.monotonic", side_effect=lambda: clock[0]):
+ eps.start()
+ # one event per second for five seconds
+ for _ in range(5):
+ clock[0] += 1.0
+ eps.update()
+ # five events over the five seconds since start
+ self.assertAlmostEqual(eps.eps(), 1.0)
+
+ def test_old_timestamps_expire_from_window(self) -> None:
+ eps = EventsPerSecond(last_n_seconds=10)
+ clock = [0.0]
+ with patch("frigate.util.builtin.time.monotonic", side_effect=lambda: clock[0]):
+ eps.start()
+ for _ in range(10):
+ clock[0] += 1.0
+ eps.update()
+ # jump well past the window so every timestamp ages out
+ clock[0] += 100.0
+ self.assertEqual(eps.eps(), 0.0)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/frigate/test/test_camera_maintainer.py b/frigate/test/test_camera_maintainer.py
new file mode 100644
index 0000000000..c03d965784
--- /dev/null
+++ b/frigate/test/test_camera_maintainer.py
@@ -0,0 +1,79 @@
+"""Tests for CameraMaintainer SHM cleanup on camera remove.
+
+Regression coverage for the case where a camera is removed and then a
+new camera is added with the same name. Without unlinking the per-frame
+YUV SHM slots, the maintainer's frame_manager.create call hits
+FileExistsError and falls back to reopening the existing segment at the
+*old* size, which the new ffmpeg process then writes mismatched-size
+frames into.
+"""
+
+import unittest
+from unittest.mock import MagicMock, patch
+
+from frigate.camera.maintainer import CameraMaintainer
+
+
+class TestMaintainerUnlinkFrameSlotsOnRemove(unittest.TestCase):
+ def _make_maintainer(self) -> CameraMaintainer:
+ """Build a maintainer without invoking __init__ (avoids needing real
+ FrigateConfig, queues, multiprocessing manager, etc.). We're only
+ exercising the SHM-cleanup helper, so the surrounding init is
+ irrelevant."""
+ maintainer = CameraMaintainer.__new__(CameraMaintainer)
+ maintainer.frame_manager = MagicMock()
+ return maintainer
+
+ def test_unlinks_only_segments_with_matching_prefix(self) -> None:
+ maintainer = self._make_maintainer()
+ maintainer.frame_manager.shm_store = {
+ "front_frame0": object(),
+ "front_frame1": object(),
+ "front_frame2": object(),
+ # Different camera; must not be touched.
+ "side_frame0": object(),
+ # Detector input/output buffers are sized by the model and
+ # cached by the long-lived DetectorRunner — must not be
+ # touched even when their owning camera is removed.
+ "front": object(),
+ "out-front": object(),
+ }
+
+ # __name-mangled access from outside the class.
+ maintainer._CameraMaintainer__unlink_camera_frame_slots("front")
+
+ deleted = [c.args[0] for c in maintainer.frame_manager.delete.call_args_list]
+ self.assertEqual(
+ sorted(deleted),
+ ["front_frame0", "front_frame1", "front_frame2"],
+ )
+
+ def test_handles_camera_with_no_slots(self) -> None:
+ """Cameras that were removed before any frame slot was ever
+ created (e.g. cancelled during preparing_clip) should be a no-op."""
+ maintainer = self._make_maintainer()
+ maintainer.frame_manager.shm_store = {"other_frame0": object()}
+
+ maintainer._CameraMaintainer__unlink_camera_frame_slots("front")
+
+ maintainer.frame_manager.delete.assert_not_called()
+
+ def test_swallows_delete_errors(self) -> None:
+ """Unlink failures shouldn't abort the remove loop — best-effort."""
+ maintainer = self._make_maintainer()
+ maintainer.frame_manager.shm_store = {
+ "front_frame0": object(),
+ "front_frame1": object(),
+ }
+ maintainer.frame_manager.delete.side_effect = OSError("simulated")
+
+ # Both slots are attempted; the OSError on the first doesn't
+ # prevent the second from being tried.
+ with patch("frigate.camera.maintainer.logger"):
+ maintainer._CameraMaintainer__unlink_camera_frame_slots("front")
+
+ self.assertEqual(maintainer.frame_manager.delete.call_count, 2)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/frigate/test/test_chat_find_similar_objects.py b/frigate/test/test_chat_find_similar_objects.py
index 38055658e1..73fd3b27db 100644
--- a/frigate/test/test_chat_find_similar_objects.py
+++ b/frigate/test/test_chat_find_similar_objects.py
@@ -145,9 +145,12 @@ class TestExecuteFindSimilarObjects(unittest.TestCase):
embeddings=embeddings,
frigate_config=SimpleNamespace(
semantic_search=SimpleNamespace(enabled=semantic_enabled),
+ cameras={"driveway": object()},
+ auth=SimpleNamespace(roles={"admin": [], "viewer": ["driveway"]}),
+ proxy=SimpleNamespace(separator=","),
),
)
- return SimpleNamespace(app=app)
+ return SimpleNamespace(app=app, headers={})
def test_semantic_search_disabled_returns_error(self):
req = self._make_request(semantic_enabled=False)
@@ -180,7 +183,7 @@ class TestExecuteFindSimilarObjects(unittest.TestCase):
_execute_find_similar_objects(
req,
{"event_id": "anchor", "cameras": ["nonexistent_cam"]},
- allowed_cameras=["nonexistent_cam"],
+ allowed_cameras=["driveway"],
)
)
self.assertEqual(result["results"], [])
diff --git a/frigate/test/test_classification_enabled.py b/frigate/test/test_classification_enabled.py
new file mode 100644
index 0000000000..abb139cd08
--- /dev/null
+++ b/frigate/test/test_classification_enabled.py
@@ -0,0 +1,106 @@
+"""Tests that disabled custom classification models are not registered or run."""
+
+import sys
+import unittest
+from unittest.mock import MagicMock
+
+# Mock TFLite before importing the maintainer / classification modules
+_MOCK_MODULES = [
+ "tflite_runtime",
+ "tflite_runtime.interpreter",
+ "ai_edge_litert",
+ "ai_edge_litert.interpreter",
+]
+for mod in _MOCK_MODULES:
+ if mod not in sys.modules:
+ sys.modules[mod] = MagicMock()
+
+from frigate.data_processing.real_time.custom_classification import ( # noqa: E402
+ CustomObjectClassificationProcessor,
+)
+from frigate.embeddings.maintainer import EmbeddingMaintainer # noqa: E402
+
+
+class TestCustomClassificationEnabledGating(unittest.TestCase):
+ """A model with enabled: false must not keep a processor registered."""
+
+ def _make_maintainer(self) -> EmbeddingMaintainer:
+ # Bypass the heavy __init__; only the attributes touched by the
+ # config update path are needed for these tests.
+ maintainer = EmbeddingMaintainer.__new__(EmbeddingMaintainer)
+ maintainer.realtime_processors = []
+ maintainer.config = MagicMock()
+ maintainer.config.classification.custom = {}
+ maintainer.requestor = MagicMock()
+ maintainer.metrics = MagicMock()
+ maintainer.event_metadata_publisher = MagicMock()
+ return maintainer
+
+ def _make_model_config(self, name: str, enabled: bool) -> MagicMock:
+ model_config = MagicMock()
+ model_config.name = name
+ model_config.enabled = enabled
+ model_config.state_config = None
+ return model_config
+
+ def _make_processor(self, name: str) -> MagicMock:
+ processor = MagicMock(spec=CustomObjectClassificationProcessor)
+ processor.model_config = MagicMock()
+ processor.model_config.name = name
+ return processor
+
+ def test_disabled_update_tears_down_existing_processor(self):
+ """Toggling a running model to disabled shuts down and drops its processor."""
+ maintainer = self._make_maintainer()
+ processor = self._make_processor("atli")
+ maintainer.realtime_processors = [processor]
+
+ maintainer._handle_custom_classification_update(
+ "config/classification/custom/atli",
+ self._make_model_config("atli", enabled=False),
+ )
+
+ processor.shutdown.assert_called_once()
+ self.assertEqual(maintainer.realtime_processors, [])
+
+ def test_disabled_update_does_not_register_processor(self):
+ """A disabled model that has no processor is never registered."""
+ maintainer = self._make_maintainer()
+
+ maintainer._handle_custom_classification_update(
+ "config/classification/custom/atli",
+ self._make_model_config("atli", enabled=False),
+ )
+
+ self.assertEqual(maintainer.realtime_processors, [])
+
+ def test_disabled_update_leaves_other_processors_untouched(self):
+ """Disabling one model must not affect other running processors."""
+ maintainer = self._make_maintainer()
+ other = self._make_processor("simbi")
+ maintainer.realtime_processors = [other]
+
+ maintainer._handle_custom_classification_update(
+ "config/classification/custom/atli",
+ self._make_model_config("atli", enabled=False),
+ )
+
+ other.shutdown.assert_not_called()
+ self.assertEqual(maintainer.realtime_processors, [other])
+
+ def test_removed_model_tears_down_processor(self):
+ """A None payload (model deleted) still shuts down its processor."""
+ maintainer = self._make_maintainer()
+ processor = self._make_processor("atli")
+ maintainer.realtime_processors = [processor]
+
+ maintainer._handle_custom_classification_update(
+ "config/classification/custom/atli", None
+ )
+
+ processor.shutdown.assert_called_once()
+ self.assertEqual(maintainer.realtime_processors, [])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/frigate/test/test_config.py b/frigate/test/test_config.py
index e82b688c62..95884342be 100644
--- a/frigate/test/test_config.py
+++ b/frigate/test/test_config.py
@@ -10,7 +10,7 @@ from ruamel.yaml.constructor import DuplicateKeyError
from frigate.config import BirdseyeModeEnum, FrigateConfig
from frigate.const import MODEL_CACHE_DIR
from frigate.detectors import DetectorTypeEnum
-from frigate.util.builtin import deep_merge, load_labels
+from frigate.util.builtin import deep_merge
class TestConfig(unittest.TestCase):
@@ -309,16 +309,11 @@ class TestConfig(unittest.TestCase):
}
frigate_config = FrigateConfig(**config)
- all_audio_labels = {
- label
- for label in load_labels("/audio-labelmap.txt", prefill=521).values()
- if label
+ assert set(frigate_config.cameras["back"].audio.filters.keys()) == {
+ "speech",
+ "yell",
}
- assert all_audio_labels.issubset(
- set(frigate_config.cameras["back"].audio.filters.keys())
- )
-
def test_override_audio_filters(self):
config = {
"mqtt": {"host": "mqtt"},
@@ -345,7 +340,8 @@ class TestConfig(unittest.TestCase):
frigate_config = FrigateConfig(**config)
assert "speech" in frigate_config.cameras["back"].audio.filters
assert frigate_config.cameras["back"].audio.filters["speech"].threshold == 0.9
- assert "babbling" in frigate_config.cameras["back"].audio.filters
+ assert "yell" in frigate_config.cameras["back"].audio.filters
+ assert "babbling" not in frigate_config.cameras["back"].audio.filters
def test_inherit_object_filters(self):
config = {
@@ -401,6 +397,43 @@ class TestConfig(unittest.TestCase):
assert "dog" in frigate_config.cameras["back"].objects.filters
assert frigate_config.cameras["back"].objects.filters["dog"].threshold == 0.7
+ def test_unsupported_tracked_object_pruned_from_track_and_filters(self):
+ # "unicorn" is not in the model labelmap, so it must be removed from the
+ # tracked objects AND from the object filters, otherwise a stale filter
+ # entry lingers in the parsed config.
+ config = {
+ "mqtt": {"host": "mqtt"},
+ "cameras": {
+ "back": {
+ "ffmpeg": {
+ "inputs": [
+ {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
+ ]
+ },
+ "detect": {
+ "height": 1080,
+ "width": 1920,
+ "fps": 5,
+ },
+ "objects": {
+ "track": ["person", "unicorn"],
+ "filters": {
+ "person": {"threshold": 0.7},
+ "unicorn": {"threshold": 0.7},
+ },
+ },
+ }
+ },
+ }
+
+ frigate_config = FrigateConfig(**config)
+ objects = frigate_config.cameras["back"].objects
+ assert "unicorn" not in objects.track
+ assert "unicorn" not in objects.filters
+ # supported entries are left untouched
+ assert "person" in objects.track
+ assert "person" in objects.filters
+
def test_global_object_mask(self):
config = {
"mqtt": {"host": "mqtt"},
@@ -1005,6 +1038,7 @@ class TestConfig(unittest.TestCase):
config = {
"mqtt": {"host": "mqtt"},
+ "detectors": {"cpu": {"type": "cpu"}},
"model": {"path": "plus://test"},
"cameras": {
"back": {
@@ -1676,5 +1710,60 @@ class TestConfig(unittest.TestCase):
self.assertRaises(ValueError, lambda: FrigateConfig(**config))
+class TestAttributeFilterDefaults(unittest.TestCase):
+ """Verify attribute filter min_score handling at config load."""
+
+ def setUp(self):
+ self.minimal = {
+ "mqtt": {"host": "mqtt"},
+ "cameras": {
+ "back": {
+ "ffmpeg": {
+ "inputs": [
+ {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
+ ]
+ },
+ "detect": {
+ "height": 1080,
+ "width": 1920,
+ "fps": 5,
+ },
+ }
+ },
+ }
+
+ def _build_config(self, object_filters: dict | None = None) -> FrigateConfig:
+ config = deep_merge({}, self.minimal)
+ if object_filters is not None:
+ config.setdefault("objects", {})["filters"] = object_filters
+ return FrigateConfig(**config)
+
+ def test_attribute_with_no_filter_gets_default_min_score(self):
+ """Attribute with no user-provided filter gets created with min_score=0.7."""
+ config = self._build_config()
+ face_filter = config.objects.filters.get("face")
+ self.assertIsNotNone(face_filter)
+ self.assertEqual(face_filter.min_score, 0.7)
+
+ def test_attribute_filter_without_min_score_gets_bumped(self):
+ """If user sets some FilterConfig field but not min_score, min_score is bumped to 0.7."""
+ config = self._build_config({"face": {"min_area": 500}})
+ face_filter = config.objects.filters["face"]
+ self.assertEqual(face_filter.min_area, 500)
+ self.assertEqual(face_filter.min_score, 0.7)
+
+ def test_attribute_filter_explicit_min_score_half_is_preserved(self):
+ """User-provided min_score=0.5 must NOT be silently rewritten to 0.7."""
+ config = self._build_config({"face": {"min_score": 0.5}})
+ face_filter = config.objects.filters["face"]
+ self.assertEqual(face_filter.min_score, 0.5)
+
+ def test_attribute_filter_explicit_min_score_other_value_is_preserved(self):
+ """Sanity: explicit non-0.5 values pass through unchanged."""
+ config = self._build_config({"face": {"min_score": 0.3}})
+ face_filter = config.objects.filters["face"]
+ self.assertEqual(face_filter.min_score, 0.3)
+
+
if __name__ == "__main__":
unittest.main(verbosity=2)
diff --git a/frigate/test/test_debug_replay.py b/frigate/test/test_debug_replay.py
new file mode 100644
index 0000000000..a91f759c44
--- /dev/null
+++ b/frigate/test/test_debug_replay.py
@@ -0,0 +1,250 @@
+"""Tests for the simplified DebugReplayManager.
+
+Startup orchestration lives in ``frigate.jobs.debug_replay`` (covered by
+``test_debug_replay_job``). The manager owns only session presence and
+cleanup.
+"""
+
+import unittest
+import unittest.mock
+from unittest.mock import MagicMock, patch
+
+
+class TestDebugReplayManagerSession(unittest.TestCase):
+ def test_inactive_by_default(self) -> None:
+ from frigate.debug_replay import DebugReplayManager
+
+ manager = DebugReplayManager()
+
+ self.assertFalse(manager.active)
+ self.assertIsNone(manager.replay_camera_name)
+ self.assertIsNone(manager.source_camera)
+ self.assertIsNone(manager.clip_path)
+ self.assertIsNone(manager.start_ts)
+ self.assertIsNone(manager.end_ts)
+
+ def test_mark_starting_sets_session_pointers_and_active(self) -> None:
+ from frigate.debug_replay import DebugReplayManager
+
+ manager = DebugReplayManager()
+
+ manager.mark_starting(
+ source_camera="front",
+ replay_camera_name="_replay_front",
+ start_ts=100.0,
+ end_ts=200.0,
+ )
+
+ self.assertTrue(manager.active)
+ self.assertEqual(manager.replay_camera_name, "_replay_front")
+ self.assertEqual(manager.source_camera, "front")
+ self.assertEqual(manager.start_ts, 100.0)
+ self.assertEqual(manager.end_ts, 200.0)
+ self.assertIsNone(manager.clip_path)
+
+ def test_mark_session_ready_sets_clip_path(self) -> None:
+ from frigate.debug_replay import DebugReplayManager
+
+ manager = DebugReplayManager()
+ manager.mark_starting("front", "_replay_front", 100.0, 200.0)
+
+ manager.mark_session_ready(clip_path="/tmp/replay/_replay_front.mp4")
+
+ self.assertEqual(manager.clip_path, "/tmp/replay/_replay_front.mp4")
+ self.assertTrue(manager.active)
+
+ def test_clear_session_resets_all_pointers(self) -> None:
+ from frigate.debug_replay import DebugReplayManager
+
+ manager = DebugReplayManager()
+ manager.mark_starting("front", "_replay_front", 100.0, 200.0)
+ manager.mark_session_ready("/tmp/replay/clip.mp4")
+
+ manager.clear_session()
+
+ self.assertFalse(manager.active)
+ self.assertIsNone(manager.replay_camera_name)
+ self.assertIsNone(manager.source_camera)
+ self.assertIsNone(manager.clip_path)
+ self.assertIsNone(manager.start_ts)
+ self.assertIsNone(manager.end_ts)
+
+
+class TestDebugReplayManagerStop(unittest.TestCase):
+ def setUp(self) -> None:
+ # stop() publishes a terminal job_state via a real JobStatePublisher,
+ # which opens a ZMQ REQ socket and blocks on REP. No dispatcher runs
+ # in unit tests, so substitute a no-op publisher.
+ patcher = patch("frigate.debug_replay.JobStatePublisher")
+ patcher.start()
+ self.addCleanup(patcher.stop)
+
+ def test_stop_when_inactive_is_a_noop(self) -> None:
+ from frigate.debug_replay import DebugReplayManager
+
+ manager = DebugReplayManager()
+ frigate_config = MagicMock()
+ frigate_config.cameras = {}
+ publisher = MagicMock()
+
+ # Should not raise; should not publish any events.
+ manager.stop(frigate_config=frigate_config, config_publisher=publisher)
+
+ publisher.publish_update.assert_not_called()
+
+ def test_stop_publishes_remove_when_camera_was_published(self) -> None:
+ from frigate.config.camera.updater import CameraConfigUpdateEnum
+ from frigate.debug_replay import DebugReplayManager
+
+ manager = DebugReplayManager()
+ manager.mark_starting("front", "_replay_front", 100.0, 200.0)
+ manager.mark_session_ready("/tmp/replay/_replay_front.mp4")
+
+ camera_config = MagicMock()
+ frigate_config = MagicMock()
+ frigate_config.cameras = {"_replay_front": camera_config}
+ publisher = MagicMock()
+
+ with (
+ patch.object(manager, "_cleanup_db"),
+ patch.object(manager, "_cleanup_files"),
+ patch("frigate.debug_replay.cancel_debug_replay_job", return_value=False),
+ ):
+ manager.stop(frigate_config=frigate_config, config_publisher=publisher)
+
+ # One publish_update call with a remove topic.
+ self.assertEqual(publisher.publish_update.call_count, 1)
+ topic_arg = publisher.publish_update.call_args.args[0]
+ self.assertEqual(topic_arg.update_type, CameraConfigUpdateEnum.remove)
+ self.assertFalse(manager.active)
+
+ def test_stop_skips_remove_publish_when_camera_not_in_config(self) -> None:
+ """Cancellation during preparing_clip: no camera was published yet."""
+ from frigate.debug_replay import DebugReplayManager
+
+ manager = DebugReplayManager()
+ manager.mark_starting("front", "_replay_front", 100.0, 200.0)
+ # clip_path stays None because we cancelled before camera publish.
+
+ frigate_config = MagicMock()
+ frigate_config.cameras = {} # _replay_front not present
+ publisher = MagicMock()
+
+ with (
+ patch.object(manager, "_cleanup_db"),
+ patch.object(manager, "_cleanup_files"),
+ patch("frigate.debug_replay.cancel_debug_replay_job", return_value=True),
+ ):
+ manager.stop(frigate_config=frigate_config, config_publisher=publisher)
+
+ publisher.publish_update.assert_not_called()
+ self.assertFalse(manager.active)
+
+ def test_stop_calls_cancel_debug_replay_job(self) -> None:
+ from frigate.debug_replay import DebugReplayManager
+
+ manager = DebugReplayManager()
+ manager.mark_starting("front", "_replay_front", 100.0, 200.0)
+
+ frigate_config = MagicMock()
+ frigate_config.cameras = {}
+ publisher = MagicMock()
+
+ with (
+ patch.object(manager, "_cleanup_db"),
+ patch.object(manager, "_cleanup_files"),
+ patch(
+ "frigate.debug_replay.cancel_debug_replay_job",
+ return_value=True,
+ ) as mock_cancel,
+ ):
+ manager.stop(frigate_config=frigate_config, config_publisher=publisher)
+
+ mock_cancel.assert_called_once()
+
+
+class TestDebugReplayManagerPublishCamera(unittest.TestCase):
+ def test_publish_camera_invokes_publisher_with_add_topic(self) -> None:
+ from frigate.config.camera.updater import CameraConfigUpdateEnum
+ from frigate.debug_replay import DebugReplayManager
+
+ manager = DebugReplayManager()
+
+ source_config = MagicMock()
+ new_camera_config = MagicMock()
+ frigate_config = MagicMock()
+ frigate_config.cameras = {"front": source_config}
+ publisher = MagicMock()
+
+ with (
+ patch.object(
+ manager,
+ "_build_camera_config_dict",
+ return_value={"enabled": True},
+ ),
+ patch("frigate.debug_replay.find_config_file", return_value="/cfg.yml"),
+ patch("frigate.debug_replay.YAML") as yaml_cls,
+ patch("frigate.debug_replay.FrigateConfig.parse_object") as parse_object,
+ patch("builtins.open", unittest.mock.mock_open(read_data="cameras:\n")),
+ ):
+ yaml_instance = yaml_cls.return_value
+ yaml_instance.load.return_value = {"cameras": {}}
+ parsed = MagicMock()
+ parsed.cameras = {"_replay_front": new_camera_config}
+ parse_object.return_value = parsed
+
+ manager.publish_camera(
+ source_camera="front",
+ replay_name="_replay_front",
+ clip_path="/tmp/clip.mp4",
+ frigate_config=frigate_config,
+ config_publisher=publisher,
+ )
+
+ # Camera registered into the live config dict
+ self.assertIn("_replay_front", frigate_config.cameras)
+ # Publisher invoked with an add topic
+ self.assertEqual(publisher.publish_update.call_count, 1)
+ topic_arg = publisher.publish_update.call_args.args[0]
+ self.assertEqual(topic_arg.update_type, CameraConfigUpdateEnum.add)
+
+ def test_publish_camera_wraps_parse_failure_in_runtime_error(self) -> None:
+ from frigate.debug_replay import DebugReplayManager
+
+ manager = DebugReplayManager()
+ frigate_config = MagicMock()
+ frigate_config.cameras = {"front": MagicMock()}
+ publisher = MagicMock()
+
+ with (
+ patch.object(
+ manager,
+ "_build_camera_config_dict",
+ return_value={"enabled": True},
+ ),
+ patch("frigate.debug_replay.find_config_file", return_value="/cfg.yml"),
+ patch("frigate.debug_replay.YAML") as yaml_cls,
+ patch(
+ "frigate.debug_replay.FrigateConfig.parse_object",
+ side_effect=ValueError("zone foo has invalid coordinates"),
+ ),
+ patch("builtins.open", unittest.mock.mock_open(read_data="cameras:\n")),
+ ):
+ yaml_cls.return_value.load.return_value = {"cameras": {}}
+
+ with self.assertRaises(RuntimeError) as ctx:
+ manager.publish_camera(
+ source_camera="front",
+ replay_name="_replay_front",
+ clip_path="/tmp/clip.mp4",
+ frigate_config=frigate_config,
+ config_publisher=publisher,
+ )
+
+ self.assertIn("replay camera config", str(ctx.exception))
+ self.assertIn("invalid coordinates", str(ctx.exception))
+ publisher.publish_update.assert_not_called()
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/frigate/test/test_debug_replay_job.py b/frigate/test/test_debug_replay_job.py
new file mode 100644
index 0000000000..12c4be82b8
--- /dev/null
+++ b/frigate/test/test_debug_replay_job.py
@@ -0,0 +1,488 @@
+"""Tests for the debug replay job runner and factory."""
+
+import threading
+import time
+import unittest
+import unittest.mock
+from unittest.mock import MagicMock, patch
+
+from frigate.debug_replay import DebugReplayManager
+from frigate.jobs.debug_replay import (
+ DebugReplayJob,
+ RecordingDebugReplaySource,
+ cancel_debug_replay_job,
+ get_active_runner,
+ start_debug_replay_job,
+)
+from frigate.jobs.export import JobStatePublisher
+from frigate.jobs.manager import _completed_jobs, _current_jobs
+from frigate.types import JobStatusTypesEnum
+
+
+def _reset_job_manager() -> None:
+ """Clear the global job manager state between tests."""
+ _current_jobs.clear()
+ _completed_jobs.clear()
+
+
+def _patch_publisher(test_case: unittest.TestCase) -> None:
+ """Replace JobStatePublisher.publish with a no-op to avoid hanging on IPC."""
+ publisher_patch = patch.object(
+ JobStatePublisher, "publish", lambda self, payload: None
+ )
+ publisher_patch.start()
+ test_case.addCleanup(publisher_patch.stop)
+
+
+class TestDebugReplayJob(unittest.TestCase):
+ def test_default_fields(self) -> None:
+ job = DebugReplayJob()
+
+ self.assertEqual(job.job_type, "debug_replay")
+ self.assertEqual(job.status, JobStatusTypesEnum.queued)
+ self.assertIsNone(job.current_step)
+ self.assertEqual(job.progress_percent, 0.0)
+
+ def test_to_dict_whitelist(self) -> None:
+ job = DebugReplayJob(
+ source_camera="front",
+ replay_camera_name="_replay_front",
+ start_ts=100.0,
+ end_ts=200.0,
+ )
+ job.current_step = "preparing_clip"
+ job.progress_percent = 42.5
+
+ payload = job.to_dict()
+
+ # Top-level matches the standard Job shape.
+ for key in (
+ "id",
+ "job_type",
+ "status",
+ "start_time",
+ "end_time",
+ "error_message",
+ "results",
+ ):
+ self.assertIn(key, payload, f"missing top-level field: {key}")
+
+ results = payload["results"]
+ self.assertEqual(results["source_camera"], "front")
+ self.assertEqual(results["replay_camera_name"], "_replay_front")
+ self.assertEqual(results["current_step"], "preparing_clip")
+ self.assertEqual(results["progress_percent"], 42.5)
+ self.assertEqual(results["start_ts"], 100.0)
+ self.assertEqual(results["end_ts"], 200.0)
+
+
+class TestStartDebugReplayJob(unittest.TestCase):
+ def setUp(self) -> None:
+ _reset_job_manager()
+ _patch_publisher(self)
+ self.manager = DebugReplayManager()
+ self.frigate_config = MagicMock()
+ self.frigate_config.cameras = {"front": MagicMock()}
+ self.frigate_config.ffmpeg.ffmpeg_path = "/bin/true"
+ self.publisher = MagicMock()
+
+ self.recordings_qs = MagicMock()
+ self.recordings_qs.count.return_value = 1
+ self.recordings_qs.__iter__.return_value = iter([MagicMock(path="/tmp/r1.mp4")])
+
+ def tearDown(self) -> None:
+ runner = get_active_runner()
+ if runner is not None:
+ runner.cancel()
+ runner.join(timeout=2.0)
+ _reset_job_manager()
+
+ def test_rejects_unknown_camera(self) -> None:
+ with self.assertRaises(ValueError):
+ start_debug_replay_job(
+ source=RecordingDebugReplaySource(
+ source_camera="missing",
+ start_ts=100.0,
+ end_ts=200.0,
+ internal_port=5000,
+ ),
+ frigate_config=self.frigate_config,
+ config_publisher=self.publisher,
+ replay_manager=self.manager,
+ )
+
+ def test_rejects_invalid_time_range(self) -> None:
+ with self.assertRaises(ValueError):
+ start_debug_replay_job(
+ source=RecordingDebugReplaySource(
+ source_camera="front",
+ start_ts=200.0,
+ end_ts=100.0,
+ internal_port=5000,
+ ),
+ frigate_config=self.frigate_config,
+ config_publisher=self.publisher,
+ replay_manager=self.manager,
+ )
+
+ def test_rejects_when_no_recordings(self) -> None:
+ empty_qs = MagicMock()
+ empty_qs.count.return_value = 0
+ with patch("frigate.jobs.debug_replay.query_recordings", return_value=empty_qs):
+ with self.assertRaises(ValueError):
+ start_debug_replay_job(
+ source=RecordingDebugReplaySource(
+ source_camera="front",
+ start_ts=100.0,
+ end_ts=200.0,
+ internal_port=5000,
+ ),
+ frigate_config=self.frigate_config,
+ config_publisher=self.publisher,
+ replay_manager=self.manager,
+ )
+
+ def test_returns_job_id_and_marks_session_starting(self) -> None:
+ block = threading.Event()
+
+ def slow_helper(cmd, **kwargs):
+ block.wait(timeout=5)
+ return 0, ""
+
+ with (
+ patch(
+ "frigate.jobs.debug_replay.query_recordings",
+ return_value=self.recordings_qs,
+ ),
+ patch(
+ "frigate.jobs.debug_replay.run_ffmpeg_with_progress",
+ side_effect=slow_helper,
+ ),
+ patch.object(self.manager, "publish_camera"),
+ patch("os.path.exists", return_value=True),
+ patch("os.makedirs"),
+ patch("builtins.open", unittest.mock.mock_open()),
+ ):
+ job_id = start_debug_replay_job(
+ source=RecordingDebugReplaySource(
+ source_camera="front",
+ start_ts=100.0,
+ end_ts=200.0,
+ internal_port=5000,
+ ),
+ frigate_config=self.frigate_config,
+ config_publisher=self.publisher,
+ replay_manager=self.manager,
+ )
+
+ self.assertIsInstance(job_id, str)
+ self.assertTrue(self.manager.active)
+ self.assertEqual(self.manager.replay_camera_name, "_replay_front")
+ self.assertEqual(self.manager.source_camera, "front")
+
+ block.set()
+
+ def test_rejects_concurrent_calls(self) -> None:
+ block = threading.Event()
+
+ def slow_helper(cmd, **kwargs):
+ block.wait(timeout=5)
+ return 0, ""
+
+ with (
+ patch(
+ "frigate.jobs.debug_replay.query_recordings",
+ return_value=self.recordings_qs,
+ ),
+ patch(
+ "frigate.jobs.debug_replay.run_ffmpeg_with_progress",
+ side_effect=slow_helper,
+ ),
+ patch.object(self.manager, "publish_camera"),
+ patch("os.path.exists", return_value=True),
+ patch("os.makedirs"),
+ patch("builtins.open", unittest.mock.mock_open()),
+ ):
+ start_debug_replay_job(
+ source=RecordingDebugReplaySource(
+ source_camera="front",
+ start_ts=100.0,
+ end_ts=200.0,
+ internal_port=5000,
+ ),
+ frigate_config=self.frigate_config,
+ config_publisher=self.publisher,
+ replay_manager=self.manager,
+ )
+
+ with self.assertRaises(RuntimeError):
+ start_debug_replay_job(
+ source=RecordingDebugReplaySource(
+ source_camera="front",
+ start_ts=100.0,
+ end_ts=200.0,
+ internal_port=5000,
+ ),
+ frigate_config=self.frigate_config,
+ config_publisher=self.publisher,
+ replay_manager=self.manager,
+ )
+
+ block.set()
+
+
+class TestRunnerHappyPath(unittest.TestCase):
+ def setUp(self) -> None:
+ _reset_job_manager()
+ _patch_publisher(self)
+ self.manager = DebugReplayManager()
+ self.frigate_config = MagicMock()
+ self.frigate_config.cameras = {"front": MagicMock()}
+ self.frigate_config.ffmpeg.ffmpeg_path = "/bin/true"
+ self.publisher = MagicMock()
+
+ self.recordings_qs = MagicMock()
+ self.recordings_qs.count.return_value = 1
+ self.recordings_qs.__iter__.return_value = iter([MagicMock(path="/tmp/r1.mp4")])
+
+ def tearDown(self) -> None:
+ runner = get_active_runner()
+ if runner is not None:
+ runner.cancel()
+ runner.join(timeout=2.0)
+ _reset_job_manager()
+
+ def _wait_for(self, predicate, timeout: float = 5.0) -> bool:
+ deadline = time.time() + timeout
+ while time.time() < deadline:
+ if predicate():
+ return True
+ time.sleep(0.02)
+ return False
+
+ def test_progress_callback_updates_job_percent(self) -> None:
+ captured: list[float] = []
+
+ def fake_helper(cmd, *, on_progress=None, **kwargs):
+ on_progress(0.0)
+ on_progress(50.0)
+ on_progress(100.0)
+ return 0, ""
+
+ with (
+ patch(
+ "frigate.jobs.debug_replay.query_recordings",
+ return_value=self.recordings_qs,
+ ),
+ patch(
+ "frigate.jobs.debug_replay.run_ffmpeg_with_progress",
+ side_effect=fake_helper,
+ ),
+ patch.object(
+ self.manager,
+ "publish_camera",
+ side_effect=lambda *a, **kw: captured.append("published"),
+ ),
+ patch("os.path.exists", return_value=True),
+ patch("os.makedirs"),
+ patch("builtins.open", unittest.mock.mock_open()),
+ ):
+ start_debug_replay_job(
+ source=RecordingDebugReplaySource(
+ source_camera="front",
+ start_ts=100.0,
+ end_ts=200.0,
+ internal_port=5000,
+ ),
+ frigate_config=self.frigate_config,
+ config_publisher=self.publisher,
+ replay_manager=self.manager,
+ )
+
+ self.assertTrue(
+ self._wait_for(lambda: get_active_runner() is None),
+ "runner did not finish",
+ )
+
+ from frigate.jobs.manager import get_current_job
+
+ job = get_current_job("debug_replay")
+ self.assertIsNotNone(job)
+ self.assertEqual(job.status, JobStatusTypesEnum.success)
+ self.assertEqual(job.progress_percent, 100.0)
+ self.assertEqual(captured, ["published"])
+ # Manager should have been told the session is ready with the clip path.
+ self.assertIsNotNone(self.manager.clip_path)
+
+
+class TestRunnerFailurePath(unittest.TestCase):
+ def setUp(self) -> None:
+ _reset_job_manager()
+ _patch_publisher(self)
+ self.manager = DebugReplayManager()
+ self.frigate_config = MagicMock()
+ self.frigate_config.cameras = {"front": MagicMock()}
+ self.frigate_config.ffmpeg.ffmpeg_path = "/bin/true"
+ self.publisher = MagicMock()
+ self.recordings_qs = MagicMock()
+ self.recordings_qs.count.return_value = 1
+ self.recordings_qs.__iter__.return_value = iter([MagicMock(path="/tmp/r1.mp4")])
+
+ def tearDown(self) -> None:
+ runner = get_active_runner()
+ if runner is not None:
+ runner.cancel()
+ runner.join(timeout=2.0)
+ _reset_job_manager()
+
+ def _wait_for(self, predicate, timeout: float = 5.0) -> bool:
+ deadline = time.time() + timeout
+ while time.time() < deadline:
+ if predicate():
+ return True
+ time.sleep(0.02)
+ return False
+
+ def test_ffmpeg_failure_marks_job_failed_and_clears_session(self) -> None:
+ def failing_helper(cmd, **kwargs):
+ return 1, "ffmpeg exploded"
+
+ with (
+ patch(
+ "frigate.jobs.debug_replay.query_recordings",
+ return_value=self.recordings_qs,
+ ),
+ patch(
+ "frigate.jobs.debug_replay.run_ffmpeg_with_progress",
+ side_effect=failing_helper,
+ ),
+ patch("os.path.exists", return_value=True),
+ patch("os.makedirs"),
+ patch("os.remove"),
+ patch("builtins.open", unittest.mock.mock_open()),
+ ):
+ start_debug_replay_job(
+ source=RecordingDebugReplaySource(
+ source_camera="front",
+ start_ts=100.0,
+ end_ts=200.0,
+ internal_port=5000,
+ ),
+ frigate_config=self.frigate_config,
+ config_publisher=self.publisher,
+ replay_manager=self.manager,
+ )
+
+ self.assertTrue(
+ self._wait_for(lambda: get_active_runner() is None),
+ "runner did not finish",
+ )
+
+ from frigate.jobs.manager import get_current_job
+
+ job = get_current_job("debug_replay")
+ self.assertIsNotNone(job)
+ self.assertEqual(job.status, JobStatusTypesEnum.failed)
+ self.assertIsNotNone(job.error_message)
+ self.assertIn("ffmpeg", job.error_message.lower())
+ # Session cleared so a new /start is allowed
+ self.assertFalse(self.manager.active)
+
+
+class TestRunnerCancellation(unittest.TestCase):
+ def setUp(self) -> None:
+ _reset_job_manager()
+ _patch_publisher(self)
+ self.manager = DebugReplayManager()
+ self.frigate_config = MagicMock()
+ self.frigate_config.cameras = {"front": MagicMock()}
+ self.frigate_config.ffmpeg.ffmpeg_path = "/bin/true"
+ self.publisher = MagicMock()
+ self.recordings_qs = MagicMock()
+ self.recordings_qs.count.return_value = 1
+ self.recordings_qs.__iter__.return_value = iter([MagicMock(path="/tmp/r1.mp4")])
+
+ def tearDown(self) -> None:
+ runner = get_active_runner()
+ if runner is not None:
+ runner.cancel()
+ runner.join(timeout=2.0)
+ _reset_job_manager()
+
+ def _wait_for(self, predicate, timeout: float = 5.0) -> bool:
+ deadline = time.time() + timeout
+ while time.time() < deadline:
+ if predicate():
+ return True
+ time.sleep(0.02)
+ return False
+
+ def test_cancel_terminates_ffmpeg_and_marks_cancelled(self) -> None:
+ terminated = threading.Event()
+ fake_proc = MagicMock()
+ fake_proc.terminate = MagicMock(side_effect=lambda: terminated.set())
+
+ def fake_helper(cmd, *, process_started=None, **kwargs):
+ if process_started is not None:
+ process_started(fake_proc)
+ terminated.wait(timeout=5)
+ return -15, "killed"
+
+ with (
+ patch(
+ "frigate.jobs.debug_replay.query_recordings",
+ return_value=self.recordings_qs,
+ ),
+ patch(
+ "frigate.jobs.debug_replay.run_ffmpeg_with_progress",
+ side_effect=fake_helper,
+ ),
+ patch("os.path.exists", return_value=True),
+ patch("os.makedirs"),
+ patch("os.remove"),
+ patch("builtins.open", unittest.mock.mock_open()),
+ ):
+ start_debug_replay_job(
+ source=RecordingDebugReplaySource(
+ source_camera="front",
+ start_ts=100.0,
+ end_ts=200.0,
+ internal_port=5000,
+ ),
+ frigate_config=self.frigate_config,
+ config_publisher=self.publisher,
+ replay_manager=self.manager,
+ )
+
+ # Wait for the runner to register the active process.
+ self.assertTrue(
+ self._wait_for(
+ lambda: (
+ get_active_runner() is not None
+ and get_active_runner()._active_process is fake_proc
+ )
+ )
+ )
+
+ cancelled = cancel_debug_replay_job()
+ self.assertTrue(cancelled)
+ self.assertTrue(fake_proc.terminate.called)
+
+ self.assertTrue(
+ self._wait_for(lambda: get_active_runner() is None),
+ "runner did not finish",
+ )
+
+ from frigate.jobs.manager import get_current_job
+
+ job = get_current_job("debug_replay")
+ self.assertEqual(job.status, JobStatusTypesEnum.cancelled)
+ # Runner must not clear the manager session on cancellation —
+ # that belongs to the caller of cancel_debug_replay_job (stop()).
+ # If the runner cleared it, stop() would log "no active session"
+ # and skip its cleanup_db / cleanup_files calls.
+ self.assertTrue(self.manager.active)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/frigate/test/test_dispatcher_runtime_state.py b/frigate/test/test_dispatcher_runtime_state.py
new file mode 100644
index 0000000000..dae0518d80
--- /dev/null
+++ b/frigate/test/test_dispatcher_runtime_state.py
@@ -0,0 +1,217 @@
+"""Tests for Dispatcher runtime state persistence wiring."""
+
+import os
+import tempfile
+import unittest
+from unittest.mock import MagicMock, patch
+
+from frigate.comms.dispatcher import Dispatcher
+from frigate.comms.runtime_state import RuntimeStatePersistence
+
+
+def _make_camera_mock(
+ *,
+ enabled: bool = True,
+ enabled_in_config: bool = True,
+ detect_enabled: bool = True,
+ record_enabled: bool = True,
+ record_enabled_in_config: bool = True,
+ snapshots_enabled: bool = True,
+ audio_enabled: bool = True,
+ audio_enabled_in_config: bool = True,
+) -> MagicMock:
+ """Build a camera config mock with the fields the in-scope handlers read."""
+ camera = MagicMock()
+ camera.enabled = enabled
+ camera.enabled_in_config = enabled_in_config
+ camera.detect.enabled = detect_enabled
+ camera.motion.enabled = True # avoid the detect→motion side-effect path
+ camera.record.enabled = record_enabled
+ camera.record.enabled_in_config = record_enabled_in_config
+ camera.snapshots.enabled = snapshots_enabled
+ camera.audio.enabled = audio_enabled
+ camera.audio.enabled_in_config = audio_enabled_in_config
+ return camera
+
+
+def _build_dispatcher(cameras: dict[str, MagicMock]) -> Dispatcher:
+ """Construct a Dispatcher with the bare-minimum mocks the tests need."""
+ config = MagicMock()
+ config.cameras = cameras
+ config_updater = MagicMock()
+ onvif = MagicMock()
+ ptz_metrics: dict = {}
+ communicators: list = []
+
+ with (
+ patch("frigate.comms.dispatcher.CameraActivityManager"),
+ patch("frigate.comms.dispatcher.AudioActivityManager"),
+ ):
+ return Dispatcher(config, config_updater, onvif, ptz_metrics, communicators)
+
+
+class TestRestoreRuntimeState(unittest.TestCase):
+ """Verify replay routes through handlers and tolerates missing entries."""
+
+ def setUp(self) -> None:
+ self.dispatcher = _build_dispatcher(
+ {
+ "front_door": _make_camera_mock(),
+ "back_yard": _make_camera_mock(),
+ }
+ )
+ # Swap each in-scope handler for a MagicMock so we can assert calls
+ # without exercising the handler's own logic.
+ self.handler_mocks: dict[str, MagicMock] = {}
+ for topic in ("enabled", "detect", "snapshots", "recordings", "audio"):
+ mock = MagicMock()
+ self.dispatcher._camera_settings_handlers[topic] = mock
+ self.handler_mocks[topic] = mock
+
+ def test_replays_each_stored_entry_through_its_handler(self) -> None:
+ self.dispatcher._runtime_state = MagicMock(
+ spec=RuntimeStatePersistence,
+ load=MagicMock(
+ return_value={
+ "front_door": {"detect": False, "recordings": False},
+ "back_yard": {"audio": False},
+ }
+ ),
+ )
+ self.dispatcher.restore_runtime_state()
+
+ self.handler_mocks["detect"].assert_called_once_with("front_door", "OFF")
+ self.handler_mocks["recordings"].assert_called_once_with("front_door", "OFF")
+ self.handler_mocks["audio"].assert_called_once_with("back_yard", "OFF")
+ self.handler_mocks["enabled"].assert_not_called()
+ self.handler_mocks["snapshots"].assert_not_called()
+
+ def test_skips_unknown_cameras(self) -> None:
+ self.dispatcher._runtime_state = MagicMock(
+ spec=RuntimeStatePersistence,
+ load=MagicMock(return_value={"removed_cam": {"detect": False}}),
+ )
+ self.dispatcher.restore_runtime_state()
+ for mock in self.handler_mocks.values():
+ mock.assert_not_called()
+
+ def test_skips_unknown_topics(self) -> None:
+ self.dispatcher._runtime_state = MagicMock(
+ spec=RuntimeStatePersistence,
+ load=MagicMock(return_value={"front_door": {"some_old_topic": True}}),
+ )
+ self.dispatcher.restore_runtime_state()
+ for mock in self.handler_mocks.values():
+ mock.assert_not_called()
+
+ def test_continues_after_handler_exception(self) -> None:
+ self.handler_mocks["detect"].side_effect = RuntimeError("boom")
+ self.dispatcher._runtime_state = MagicMock(
+ spec=RuntimeStatePersistence,
+ load=MagicMock(
+ return_value={
+ "front_door": {"detect": False, "recordings": False},
+ }
+ ),
+ )
+ # Must not raise; the recordings handler must still run.
+ self.dispatcher.restore_runtime_state()
+ self.handler_mocks["recordings"].assert_called_once_with("front_door", "OFF")
+
+ def test_true_value_routes_as_on_payload(self) -> None:
+ self.dispatcher._runtime_state = MagicMock(
+ spec=RuntimeStatePersistence,
+ load=MagicMock(return_value={"front_door": {"detect": True}}),
+ )
+ self.dispatcher.restore_runtime_state()
+ self.handler_mocks["detect"].assert_called_once_with("front_door", "ON")
+
+
+class TestHandlersPersistViaSet(unittest.TestCase):
+ """Verify each in-scope handler writes to the runtime state on success."""
+
+ def setUp(self) -> None:
+ self.tmp_dir = tempfile.mkdtemp()
+ self.config_path = os.path.join(self.tmp_dir, "config.yml")
+ with open(self.config_path, "w") as f:
+ f.write("")
+ self._patcher = patch(
+ "frigate.comms.runtime_state.find_config_file",
+ return_value=self.config_path,
+ )
+ self._patcher.start()
+
+ # Start with everything OFF so each ON payload triggers a real change
+ self.cameras = {
+ "front_door": _make_camera_mock(
+ enabled=False,
+ detect_enabled=False,
+ record_enabled=False,
+ snapshots_enabled=False,
+ audio_enabled=False,
+ )
+ }
+ self.dispatcher = _build_dispatcher(self.cameras)
+
+ def tearDown(self) -> None:
+ self._patcher.stop()
+ for name in os.listdir(self.tmp_dir):
+ os.remove(os.path.join(self.tmp_dir, name))
+ os.rmdir(self.tmp_dir)
+
+ def _stored_state(self) -> dict:
+ return RuntimeStatePersistence().load()
+
+ def test_enabled_handler_persists(self) -> None:
+ self.dispatcher._on_enabled_command("front_door", "ON")
+ self.assertEqual(self._stored_state(), {"front_door": {"enabled": True}})
+
+ def test_detect_handler_persists(self) -> None:
+ self.dispatcher._on_detect_command("front_door", "ON")
+ self.assertEqual(self._stored_state(), {"front_door": {"detect": True}})
+
+ def test_recordings_handler_persists(self) -> None:
+ self.dispatcher._on_recordings_command("front_door", "ON")
+ self.assertEqual(self._stored_state(), {"front_door": {"recordings": True}})
+
+ def test_snapshots_handler_persists(self) -> None:
+ self.dispatcher._on_snapshots_command("front_door", "ON")
+ self.assertEqual(self._stored_state(), {"front_door": {"snapshots": True}})
+
+ def test_audio_handler_persists(self) -> None:
+ self.dispatcher._on_audio_command("front_door", "ON")
+ self.assertEqual(self._stored_state(), {"front_door": {"audio": True}})
+
+ def test_enabled_in_config_gate_blocks_persistence(self) -> None:
+ """An ON payload rejected by the gate must not be persisted."""
+ cam = self.cameras["front_door"]
+ cam.enabled_in_config = False
+ cam.record.enabled_in_config = False
+ cam.audio.enabled_in_config = False
+
+ self.dispatcher._on_enabled_command("front_door", "ON")
+ self.dispatcher._on_recordings_command("front_door", "ON")
+ self.dispatcher._on_audio_command("front_door", "ON")
+
+ self.assertEqual(self._stored_state(), {})
+
+
+class TestClearPassthrough(unittest.TestCase):
+ """The dispatcher's public clear methods delegate to the store."""
+
+ def test_clear_runtime_state_for_yaml_keys_passthrough(self) -> None:
+ dispatcher = _build_dispatcher({})
+ dispatcher._runtime_state = MagicMock(spec=RuntimeStatePersistence)
+ keys = ["cameras.front_door.detect.enabled"]
+ dispatcher.clear_runtime_state_for_yaml_keys(keys)
+ dispatcher._runtime_state.clear_for_yaml_keys.assert_called_once_with(keys)
+
+ def test_clear_runtime_state_passthrough(self) -> None:
+ dispatcher = _build_dispatcher({})
+ dispatcher._runtime_state = MagicMock(spec=RuntimeStatePersistence)
+ dispatcher.clear_runtime_state()
+ dispatcher._runtime_state.clear_all.assert_called_once_with()
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/frigate/test/test_export.py b/frigate/test/test_export.py
new file mode 100644
index 0000000000..7612a4144f
--- /dev/null
+++ b/frigate/test/test_export.py
@@ -0,0 +1,132 @@
+import unittest
+
+from frigate.record.export import validate_ffmpeg_args
+
+
+class TestValidateFfmpegArgs(unittest.TestCase):
+ """Tests for the non-admin custom export ffmpeg arg validator.
+
+ The validator uses a structural allowlist: every token must be an
+ allowlisted flag or the value of one, filter values are restricted to a
+ safe set of filters, and no token may become a bare input/output URL.
+ """
+
+ def assertRejected(self, args: str) -> None:
+ valid, message = validate_ffmpeg_args(args)
+ self.assertFalse(valid, f"expected {args!r} to be rejected")
+ self.assertNotEqual(message, "")
+
+ def assertAllowed(self, args: str) -> None:
+ valid, message = validate_ffmpeg_args(args)
+ self.assertTrue(valid, f"expected {args!r} to be allowed, got: {message}")
+ self.assertEqual(message, "")
+
+ # --- legitimate use cases must keep working ---------------------------
+
+ def test_timelapse_setpts_allowed(self):
+ # The whole reason -vf cannot simply be blocked: timelapse exports.
+ self.assertAllowed("-vf setpts=PTS/60 -r 25")
+ self.assertAllowed("-vf setpts=0.04*PTS -r 30") # server default
+ self.assertAllowed("-filter:v setpts=PTS/60 -r 25")
+
+ def test_default_input_args_allowed(self):
+ self.assertAllowed("")
+ self.assertAllowed("-an -skip_frame nokey")
+
+ def test_encoding_args_allowed(self):
+ self.assertAllowed("-c:v libx264 -crf 23 -preset fast")
+ self.assertAllowed("-c:v copy -c:a copy")
+ self.assertAllowed("-c:v libx264 -b:v 2M -maxrate 2M -bufsize 4M")
+ self.assertAllowed("-movflags +faststart")
+ self.assertAllowed("-pix_fmt yuv420p -r 30 -g 30")
+
+ def test_safe_filters_allowed(self):
+ self.assertAllowed("-vf scale=640:480")
+ self.assertAllowed("-vf scale=640:480,setpts=0.5*PTS")
+ self.assertAllowed("-vf format=yuv420p")
+ self.assertAllowed("-vf transpose=1")
+ self.assertAllowed("-vf hflip")
+ self.assertAllowed("-vf fps=15")
+ self.assertAllowed("-vf setsar=1 -an")
+ self.assertAllowed("-vf setdar=16/9")
+
+ # --- the reported advisory and file-read class ------------------------
+
+ def test_reported_advisory_rejected(self):
+ self.assertRejected(
+ "-filter:v drawtext=textfile=/etc/passwd:fontcolor=white:fontsize=20"
+ )
+
+ def test_file_reading_filters_rejected(self):
+ self.assertRejected("-vf movie=/etc/passwd")
+ self.assertRejected("-vf drawtext=textfile=/etc/passwd")
+ self.assertRejected("-vf subtitles=/etc/passwd")
+ # marker embedded as an option of an otherwise-allowed filter name
+ self.assertRejected("-vf scale=movie=/etc/passwd")
+
+ def test_filtergraph_brackets_rejected(self):
+ # link labels aren't needed for safe filters; rejecting "[" / "]" keeps
+ # filtergraph validation linear (no ReDoS on attacker input)
+ self.assertRejected("-vf [in]scale=640:480[out]")
+ self.assertRejected("-vf " + "[" * 5000)
+
+ def test_preset_file_read_rejected(self):
+ # cwd-anchored traversal slipped past the old startswith() path check
+ self.assertRejected("-fpre frigate/../../../etc/passwd")
+ self.assertRejected("-fpre evil.preset")
+ self.assertRejected("-vpre x")
+ self.assertRejected("-apre x")
+ self.assertRejected("-pre x")
+
+ def test_slash_option_file_read_rejected(self):
+ # ffmpeg "-/option file" reads the option value from a file
+ self.assertRejected("-/filter:v graph.txt")
+ self.assertRejected("-/filter_complex graph.txt")
+
+ # --- network / SSRF class ---------------------------------------------
+
+ def test_schemeless_protocol_rejected(self):
+ self.assertRejected("-f mpegts tcp:10.0.0.5:4444")
+ self.assertRejected("tcp:10.0.0.5:4444")
+ self.assertRejected("udp:10.0.0.5:4444")
+ self.assertRejected("-progress http:attacker.example.com:80/p")
+
+ # --- file-write class --------------------------------------------------
+
+ def test_tee_write_rejected(self):
+ self.assertRejected("-c:v libx264 -map 0 -f tee [f=mpegts]/tmp/owned.ts")
+ self.assertRejected("-f tee [f=mpegts]/etc/frigate/x.ts")
+ self.assertRejected("tee:/tmp/x")
+
+ def test_bare_output_token_rejected(self):
+ self.assertRejected("evil.mp4")
+ self.assertRejected("-c copy evil.mp4")
+ self.assertRejected("x/../escaped.mkv")
+
+ def test_file_producing_muxers_rejected(self):
+ self.assertRejected("-f hls -hls_segment_filename pwn%03d.ts out.m3u8")
+ self.assertRejected("-f md5 victim.txt")
+ self.assertRejected("-f segment seg%03d.ts")
+
+ def test_write_flags_rejected(self):
+ self.assertRejected("-progress evil.log")
+ self.assertRejected("-stats_enc_pre evil.csv")
+ self.assertRejected("-report")
+
+ # --- resource exhaustion / misc ---------------------------------------
+
+ def test_dos_input_flags_rejected(self):
+ self.assertRejected("-stream_loop -1")
+ self.assertRejected("-readrate 0.001")
+
+ def test_disallowed_flags_rejected(self):
+ self.assertRejected("-map 0")
+ self.assertRejected("-i /etc/passwd")
+ self.assertRejected("-attach evil.bin")
+ self.assertRejected("-dump_attachment evil.bin")
+ self.assertRejected("/etc/passwd")
+ self.assertRejected("-metadata comment=x")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/frigate/test/test_export_progress.py b/frigate/test/test_export_progress.py
index 616a63503b..81dcdb96e2 100644
--- a/frigate/test/test_export_progress.py
+++ b/frigate/test/test_export_progress.py
@@ -1,6 +1,9 @@
"""Tests for export progress tracking, broadcast, and FFmpeg parsing."""
import io
+import os
+import shutil
+import tempfile
import unittest
from unittest.mock import MagicMock, patch
@@ -11,6 +14,7 @@ from frigate.jobs.export import (
)
from frigate.record.export import PlaybackSourceEnum, RecordingExporter
from frigate.types import JobStatusTypesEnum
+from frigate.util.ffmpeg import inject_progress_flags
def _make_exporter(
@@ -115,10 +119,9 @@ class TestExpectedOutputDuration(unittest.TestCase):
class TestProgressFlagInjection(unittest.TestCase):
def test_inserts_before_output_path(self) -> None:
- exporter = _make_exporter()
cmd = ["ffmpeg", "-i", "input.m3u8", "-c", "copy", "/tmp/output.mp4"]
- result = exporter._inject_progress_flags(cmd)
+ result = inject_progress_flags(cmd)
assert result == [
"ffmpeg",
@@ -133,8 +136,7 @@ class TestProgressFlagInjection(unittest.TestCase):
]
def test_handles_empty_cmd(self) -> None:
- exporter = _make_exporter()
- assert exporter._inject_progress_flags([]) == []
+ assert inject_progress_flags([]) == []
class TestFfmpegProgressParsing(unittest.TestCase):
@@ -164,7 +166,7 @@ class TestFfmpegProgressParsing(unittest.TestCase):
fake_proc.returncode = 0
fake_proc.wait = MagicMock(return_value=0)
- with patch("frigate.record.export.sp.Popen", return_value=fake_proc):
+ with patch("frigate.util.ffmpeg.sp.Popen", return_value=fake_proc):
returncode, _stderr = exporter._run_ffmpeg_with_progress(
["ffmpeg", "-i", "x.m3u8", "/tmp/out.mp4"], "playlist", step="encoding"
)
@@ -363,6 +365,121 @@ class TestBroadcastAggregation(unittest.TestCase):
assert job.progress_percent == 33.0
+class TestGetDatetimeFromTimestamp(unittest.TestCase):
+ """Auto-generated export name should honor config.ui.timezone, not
+ fall back to the container's UTC clock when a timezone is configured.
+ """
+
+ def test_uses_configured_ui_timezone(self) -> None:
+ exporter = _make_exporter()
+ exporter.config.ui.timezone = "America/New_York"
+ # 2025-01-15 12:00:00 UTC is 07:00:00 EST
+ assert exporter.get_datetime_from_timestamp(1736942400) == "2025-01-15 07:00:00"
+
+ def test_falls_back_to_local_when_timezone_unset(self) -> None:
+ exporter = _make_exporter()
+ exporter.config.ui.timezone = None
+ # No assertion on the exact wall-clock value — just confirm no
+ # exception and that pytz isn't required when the field is unset.
+ assert isinstance(exporter.get_datetime_from_timestamp(1736942400), str)
+
+ def test_invalid_timezone_falls_back_to_local(self) -> None:
+ exporter = _make_exporter()
+ exporter.config.ui.timezone = "Not/A_Real_Zone"
+ assert isinstance(exporter.get_datetime_from_timestamp(1736942400), str)
+
+
+class TestSaveThumbnailFromPreviewFrames(unittest.TestCase):
+ """Short exports in the current hour can fall between preview frame
+ writes (1-2 fps during activity, every 30s otherwise). When no frame
+ falls inside the export window, save_thumbnail should fall back to
+ the most recent prior frame instead of returning no thumbnail."""
+
+ def setUp(self) -> None:
+ self.tmp_root = tempfile.mkdtemp(prefix="frigate_thumb_test_")
+ self.preview_dir = os.path.join(self.tmp_root, "cache", "preview_frames")
+ self.export_clips = os.path.join(self.tmp_root, "clips", "export")
+ os.makedirs(self.preview_dir, exist_ok=True)
+ os.makedirs(self.export_clips, exist_ok=True)
+
+ def tearDown(self) -> None:
+ shutil.rmtree(self.tmp_root, ignore_errors=True)
+
+ def _write_frame(self, camera: str, frame_time: float) -> str:
+ path = os.path.join(self.preview_dir, f"preview_{camera}-{frame_time}.webp")
+ with open(path, "wb") as f:
+ f.write(b"fake-webp-bytes")
+ return path
+
+ def _make_short_current_hour_exporter(self) -> RecordingExporter:
+ # Use a "now-ish" timestamp so save_thumbnail's start-of-hour
+ # comparison takes the current-hour branch (preview frames).
+ import datetime
+
+ now = datetime.datetime.now(datetime.UTC).timestamp()
+ exporter = _make_exporter()
+ exporter.export_id = "thumb_short"
+ exporter.start_time = now
+ exporter.end_time = now + 3
+ return exporter
+
+ def test_short_export_falls_back_to_prior_preview_frame(self) -> None:
+ exporter = self._make_short_current_hour_exporter()
+ # Most recent preview frame is 10s before the export window
+ prior = self._write_frame(exporter.camera, exporter.start_time - 10.0)
+ thumb_target = os.path.join(self.export_clips, f"{exporter.export_id}.webp")
+
+ with (
+ patch(
+ "frigate.record.export.CACHE_DIR", os.path.join(self.tmp_root, "cache")
+ ),
+ patch(
+ "frigate.record.export.CLIPS_DIR", os.path.join(self.tmp_root, "clips")
+ ),
+ ):
+ result = exporter.save_thumbnail(exporter.export_id)
+
+ assert result == thumb_target
+ assert os.path.isfile(thumb_target)
+ with open(thumb_target, "rb") as f, open(prior, "rb") as src:
+ assert f.read() == src.read()
+
+ def test_returns_empty_when_no_preview_frames_exist(self) -> None:
+ exporter = self._make_short_current_hour_exporter()
+
+ with (
+ patch(
+ "frigate.record.export.CACHE_DIR", os.path.join(self.tmp_root, "cache")
+ ),
+ patch(
+ "frigate.record.export.CLIPS_DIR", os.path.join(self.tmp_root, "clips")
+ ),
+ ):
+ result = exporter.save_thumbnail(exporter.export_id)
+
+ assert result == ""
+
+ def test_prefers_in_window_frame_over_prior_frame(self) -> None:
+ exporter = self._make_short_current_hour_exporter()
+ self._write_frame(exporter.camera, exporter.start_time - 10.0)
+ in_window = self._write_frame(exporter.camera, exporter.start_time + 1.0)
+ thumb_target = os.path.join(self.export_clips, f"{exporter.export_id}.webp")
+
+ with (
+ patch(
+ "frigate.record.export.CACHE_DIR", os.path.join(self.tmp_root, "cache")
+ ),
+ patch(
+ "frigate.record.export.CLIPS_DIR", os.path.join(self.tmp_root, "clips")
+ ),
+ ):
+ result = exporter.save_thumbnail(exporter.export_id)
+
+ assert result == thumb_target
+ with open(thumb_target, "rb") as f, open(in_window, "rb") as src:
+ assert f.read() == src.read()
+
+
class TestSchedulesCleanup(unittest.TestCase):
def test_schedule_job_cleanup_removes_after_delay(self) -> None:
config = MagicMock()
@@ -381,5 +498,56 @@ class TestSchedulesCleanup(unittest.TestCase):
assert job.id not in manager.jobs
+class TestChapterMetadataInProgressReview(unittest.TestCase):
+ """Regression: in-progress review segments have end_time=NULL until the
+ activity closes. The chapter builder must clamp the chapter end to the
+ last recorded second instead of crashing on float(None)."""
+
+ def _fake_select_returning(self, rows: list) -> MagicMock:
+ mock_query = MagicMock()
+ mock_query.where.return_value = mock_query
+ mock_query.order_by.return_value = mock_query
+ mock_query.iterator.return_value = iter(rows)
+ return mock_query
+
+ def test_in_progress_review_does_not_crash_and_clamps_to_last_recording(
+ self,
+ ) -> None:
+ exporter = _make_exporter(end_minus_start=200)
+ # Recordings cover [1000, 1150]; export window is [1000, 1200] so
+ # the last recorded second is 1150 (a 50s gap at the tail).
+ recordings = [
+ MagicMock(start_time=1000.0, end_time=1150.0),
+ ]
+ in_progress = MagicMock(
+ start_time=1100.0,
+ end_time=None,
+ severity="alert",
+ data={"objects": ["person"]},
+ )
+
+ with tempfile.TemporaryDirectory() as tmpdir:
+ chapter_path = os.path.join(tmpdir, "chapters.txt")
+ exporter._chapter_metadata_path = lambda: chapter_path # type: ignore[method-assign]
+
+ with patch(
+ "frigate.record.export.ReviewSegment.select",
+ return_value=self._fake_select_returning([in_progress]),
+ ):
+ result = exporter._build_chapter_metadata_file(recordings)
+
+ assert result == chapter_path
+ with open(chapter_path) as f:
+ content = f.read()
+
+ # Output time is windows[-1][1] - windows[-1][0] = 150s.
+ # Review starts at wall=1100, output offset = 100s -> 100000ms.
+ # Clamped end = last_recorded_end (1150) -> output offset = 150s -> 150000ms.
+ assert "[CHAPTER]" in content
+ assert "START=100000" in content
+ assert "END=150000" in content
+ assert "title=Alert: person" in content
+
+
if __name__ == "__main__":
unittest.main()
diff --git a/frigate/test/test_ffmpeg_progress.py b/frigate/test/test_ffmpeg_progress.py
new file mode 100644
index 0000000000..5210511168
--- /dev/null
+++ b/frigate/test/test_ffmpeg_progress.py
@@ -0,0 +1,111 @@
+"""Tests for the shared ffmpeg progress helper."""
+
+import unittest
+from unittest.mock import MagicMock, patch
+
+from frigate.util.ffmpeg import inject_progress_flags, run_ffmpeg_with_progress
+
+
+class TestInjectProgressFlags(unittest.TestCase):
+ def test_inserts_flags_before_output_path(self):
+ cmd = ["ffmpeg", "-i", "in.mp4", "-c", "copy", "out.mp4"]
+ result = inject_progress_flags(cmd)
+ self.assertEqual(
+ result,
+ [
+ "ffmpeg",
+ "-i",
+ "in.mp4",
+ "-c",
+ "copy",
+ "-progress",
+ "pipe:2",
+ "-nostats",
+ "out.mp4",
+ ],
+ )
+
+ def test_empty_cmd_returns_empty(self):
+ self.assertEqual(inject_progress_flags([]), [])
+
+
+class TestRunFfmpegWithProgress(unittest.TestCase):
+ def _make_fake_proc(self, stderr_lines, returncode=0):
+ proc = MagicMock()
+ proc.stderr = iter(stderr_lines)
+ proc.stdin = MagicMock()
+ proc.returncode = returncode
+ proc.wait = MagicMock()
+ return proc
+
+ def test_emits_percent_from_out_time_us_lines(self):
+ captured: list[float] = []
+
+ def on_progress(percent: float) -> None:
+ captured.append(percent)
+
+ stderr_lines = [
+ "out_time_us=1000000\n",
+ "out_time_us=5000000\n",
+ "progress=end\n",
+ ]
+ proc = self._make_fake_proc(stderr_lines)
+ proc.stderr = MagicMock()
+ proc.stderr.__iter__ = lambda self: iter(stderr_lines)
+ proc.stderr.read = MagicMock(return_value="")
+
+ with patch("subprocess.Popen", return_value=proc):
+ returncode, _stderr = run_ffmpeg_with_progress(
+ ["ffmpeg", "-i", "in", "out"],
+ expected_duration_seconds=10.0,
+ on_progress=on_progress,
+ use_low_priority=False,
+ )
+
+ self.assertEqual(returncode, 0)
+ self.assertEqual(len(captured), 4) # initial 0.0 + two parsed + final 100.0
+ self.assertAlmostEqual(captured[0], 0.0)
+ self.assertAlmostEqual(captured[1], 10.0)
+ self.assertAlmostEqual(captured[2], 50.0)
+ self.assertAlmostEqual(captured[3], 100.0)
+
+ def test_passes_started_process_to_callback(self):
+ proc = self._make_fake_proc([])
+ proc.stderr = MagicMock()
+ proc.stderr.__iter__ = lambda self: iter([])
+ proc.stderr.read = MagicMock(return_value="")
+
+ seen: list = []
+
+ with patch("subprocess.Popen", return_value=proc):
+ run_ffmpeg_with_progress(
+ ["ffmpeg", "out"],
+ expected_duration_seconds=1.0,
+ process_started=lambda p: seen.append(p),
+ use_low_priority=False,
+ )
+
+ self.assertEqual(seen, [proc])
+
+ def test_clamps_percent_to_0_100(self):
+ captured: list[float] = []
+
+ def on_progress(percent: float) -> None:
+ captured.append(percent)
+
+ stderr_lines = ["out_time_us=999999999999\n"]
+ proc = self._make_fake_proc(stderr_lines)
+ proc.stderr = MagicMock()
+ proc.stderr.__iter__ = lambda self: iter(stderr_lines)
+ proc.stderr.read = MagicMock(return_value="")
+
+ with patch("subprocess.Popen", return_value=proc):
+ run_ffmpeg_with_progress(
+ ["ffmpeg", "out"],
+ expected_duration_seconds=10.0,
+ on_progress=on_progress,
+ use_low_priority=False,
+ )
+
+ # initial 0.0 then a clamped reading
+ self.assertEqual(captured[-1], 100.0)
diff --git a/frigate/test/test_genai_providers.py b/frigate/test/test_genai_providers.py
new file mode 100644
index 0000000000..d73d632f0a
--- /dev/null
+++ b/frigate/test/test_genai_providers.py
@@ -0,0 +1,496 @@
+"""Smoke tests for GenAI chat providers.
+
+Each provider's ``chat_with_tools_stream`` is driven with a canned "test
+response" so the two conversion layers are exercised without any network:
+
+ 1. Frigate (OpenAI-style) messages -> provider-native request format
+ 2. provider-native response -> Frigate ``("kind", value)`` stream events
+
+These guard against regressions such as tool-call arguments arriving as raw
+strings instead of dicts (which crash the ``ToolCall`` model), and multimodal
+user content (a list of text/image parts, as injected by ``get_live_context``)
+crashing message conversion.
+"""
+
+import asyncio
+import base64
+import json
+import unittest
+from types import SimpleNamespace
+from unittest.mock import AsyncMock, MagicMock, patch
+
+from frigate.config import GenAIConfig, GenAIProviderEnum
+from frigate.genai import PROVIDERS, load_providers
+
+load_providers()
+
+# A minimal but valid JPEG data URI, mirroring what get_live_context injects.
+_TINY_JPEG = base64.b64encode(b"\xff\xd8\xff\xd9").decode("ascii")
+_IMAGE_DATA_URI = f"data:image/jpeg;base64,{_TINY_JPEG}"
+
+# Conversation ending in a multimodal user message (text + live image), the
+# exact shape the chat endpoint builds after a get_live_context tool result.
+MULTIMODAL_MESSAGES = [
+ {"role": "system", "content": "You are a test assistant."},
+ {"role": "user", "content": "what do you see on the front camera?"},
+ {
+ "role": "assistant",
+ "content": "",
+ "tool_calls": [
+ {
+ "id": "call_1",
+ "type": "function",
+ "function": {
+ "name": "get_live_context",
+ "arguments": json.dumps({"camera": "front"}),
+ },
+ }
+ ],
+ },
+ {
+ "role": "tool",
+ "tool_call_id": "call_1",
+ "name": "get_live_context",
+ "content": json.dumps({"camera": "front"}),
+ },
+ {
+ "role": "user",
+ "content": [
+ {
+ "type": "text",
+ "text": "Here is the current live image from camera 'front'.",
+ },
+ {"type": "image_url", "image_url": {"url": _IMAGE_DATA_URI}},
+ ],
+ },
+]
+
+SIMPLE_MESSAGES = [
+ {"role": "system", "content": "You are a test assistant."},
+ {"role": "user", "content": "hello"},
+]
+
+TOOLS = [
+ {
+ "type": "function",
+ "function": {
+ "name": "search_objects",
+ "description": "Search tracked objects",
+ "parameters": {
+ "type": "object",
+ "properties": {"label": {"type": "string"}},
+ },
+ },
+ }
+]
+
+
+def _make_client(provider: str, **cfg_overrides):
+ """Build a provider client offline (no model validation, no network)."""
+ cfg = GenAIConfig(provider=provider, **cfg_overrides)
+ cls = PROVIDERS[GenAIProviderEnum(provider)]
+ return cls(cfg, timeout=5, validate_model=False)
+
+
+def _collect(client, messages, tools=TOOLS):
+ """Drain chat_with_tools_stream into a list of (kind, value) events."""
+
+ async def _run():
+ events = []
+ async for event in client.chat_with_tools_stream(
+ messages=messages, tools=tools, tool_choice="auto"
+ ):
+ events.append(event)
+ return events
+
+ return asyncio.run(_run())
+
+
+def _final_message(events) -> dict:
+ messages = [value for (kind, value) in events if kind == "message"]
+ assert messages, f"stream produced no final message: {events}"
+ return messages[-1]
+
+
+def _assert_tool_args_are_dicts(final: dict) -> None:
+ """Every returned tool call must expose arguments as a dict, never a string."""
+ for tool_call in final.get("tool_calls") or []:
+ assert isinstance(tool_call["arguments"], dict), (
+ f"tool call arguments must be a dict, got "
+ f"{type(tool_call['arguments']).__name__}: {tool_call['arguments']!r}"
+ )
+
+
+# ---------------------------------------------------------------------------
+# OpenAI
+# ---------------------------------------------------------------------------
+def _openai_tc(index, id=None, name=None, arguments=None):
+ return SimpleNamespace(
+ index=index,
+ id=id,
+ function=SimpleNamespace(name=name, arguments=arguments),
+ )
+
+
+def _openai_chunk(content=None, tool_calls=None, finish_reason=None, usage=None):
+ delta = SimpleNamespace(
+ content=content,
+ tool_calls=tool_calls,
+ reasoning_content=None,
+ reasoning=None,
+ )
+ choice = SimpleNamespace(delta=delta, finish_reason=finish_reason)
+ return SimpleNamespace(choices=[choice], usage=usage)
+
+
+class TestOpenAIProvider(unittest.TestCase):
+ def _client(self):
+ return _make_client(
+ "openai", model="gpt-4o", api_key="k", base_url="http://localhost:9999/v1"
+ )
+
+ def test_stream_tool_call_arguments_are_dict(self):
+ # Arguments arrive split across chunks, as the real API streams them.
+ chunks = [
+ _openai_chunk(
+ tool_calls=[
+ _openai_tc(0, id="c1", name="search_objects", arguments='{"label":')
+ ]
+ ),
+ _openai_chunk(tool_calls=[_openai_tc(0, arguments=' "person"}')]),
+ _openai_chunk(finish_reason="tool_calls"),
+ ]
+ client = self._client()
+ client.provider.chat.completions.create = MagicMock(return_value=iter(chunks))
+
+ final = _final_message(_collect(client, SIMPLE_MESSAGES))
+ self.assertEqual(final["finish_reason"], "tool_calls")
+ self.assertEqual(len(final["tool_calls"]), 1)
+ _assert_tool_args_are_dicts(final)
+ self.assertEqual(final["tool_calls"][0]["arguments"], {"label": "person"})
+
+ def test_stream_content_response(self):
+ chunks = [
+ _openai_chunk(content="hel"),
+ _openai_chunk(content="lo"),
+ _openai_chunk(finish_reason="stop"),
+ ]
+ client = self._client()
+ client.provider.chat.completions.create = MagicMock(return_value=iter(chunks))
+
+ events = _collect(client, SIMPLE_MESSAGES)
+ deltas = [v for (k, v) in events if k == "content_delta"]
+ self.assertEqual("".join(deltas), "hello")
+ self.assertEqual(_final_message(events)["content"], "hello")
+
+ def test_multimodal_message_does_not_crash(self):
+ client = self._client()
+ client.provider.chat.completions.create = MagicMock(
+ return_value=iter([_openai_chunk(content="ok", finish_reason="stop")])
+ )
+ # Passing the OpenAI-native multimodal list through must not raise.
+ final = _final_message(_collect(client, MULTIMODAL_MESSAGES))
+ self.assertEqual(final["content"], "ok")
+
+
+# ---------------------------------------------------------------------------
+# Gemini
+# ---------------------------------------------------------------------------
+def _gemini_part(text=None, thought=False, function_call=None, thought_signature=None):
+ return SimpleNamespace(
+ text=text,
+ thought=thought,
+ function_call=function_call,
+ thought_signature=thought_signature,
+ )
+
+
+def _gemini_chunk(parts, finish_reason=None, usage_metadata=None):
+ candidate = SimpleNamespace(
+ content=SimpleNamespace(parts=parts), finish_reason=finish_reason
+ )
+ return SimpleNamespace(candidates=[candidate], usage_metadata=usage_metadata)
+
+
+def _gemini_stream(chunks):
+ async def _agen(*args, **kwargs):
+ for chunk in chunks:
+ yield chunk
+
+ return _agen
+
+
+class TestGeminiProvider(unittest.TestCase):
+ def _client(self):
+ return _make_client("gemini", model="gemini-2.5-flash", api_key="k")
+
+ def _patch_stream(self, client, chunks):
+ client.provider = MagicMock()
+ client.provider.aio.models.generate_content_stream = AsyncMock(
+ side_effect=_gemini_stream(chunks)
+ )
+
+ def test_stream_parallel_tool_calls_stay_separate_dicts(self):
+ # Regression: Gemini streams complete function calls. Two calls to the
+ # same tool must NOT be merged into one concatenated arguments string.
+ from google.genai.types import FinishReason
+
+ chunks = [
+ _gemini_chunk(
+ parts=[
+ _gemini_part(
+ function_call=SimpleNamespace(
+ name="search_objects", args={"label": "person"}
+ )
+ ),
+ _gemini_part(
+ function_call=SimpleNamespace(
+ name="search_objects", args={"limit": 1}
+ )
+ ),
+ ],
+ finish_reason=FinishReason.STOP,
+ ),
+ ]
+ client = self._client()
+ self._patch_stream(client, chunks)
+
+ final = _final_message(_collect(client, SIMPLE_MESSAGES))
+ self.assertEqual(final["finish_reason"], "tool_calls")
+ self.assertEqual(len(final["tool_calls"]), 2)
+ _assert_tool_args_are_dicts(final)
+ self.assertEqual(final["tool_calls"][0]["arguments"], {"label": "person"})
+ self.assertEqual(final["tool_calls"][1]["arguments"], {"limit": 1})
+
+ def test_stream_content_response(self):
+ from google.genai.types import FinishReason
+
+ chunks = [
+ _gemini_chunk(parts=[_gemini_part(text="hel")]),
+ _gemini_chunk(
+ parts=[_gemini_part(text="lo")], finish_reason=FinishReason.STOP
+ ),
+ ]
+ client = self._client()
+ self._patch_stream(client, chunks)
+
+ events = _collect(client, SIMPLE_MESSAGES)
+ deltas = [v for (k, v) in events if k == "content_delta"]
+ self.assertEqual("".join(deltas), "hello")
+ self.assertEqual(_final_message(events)["content"], "hello")
+
+ def test_multimodal_message_converts_without_crash(self):
+ # Regression: a user message with list content (text + image_url) used
+ # to be handed to Part.from_text(text=) and raise ValidationError.
+ from google.genai.types import FinishReason
+
+ client = self._client()
+ self._patch_stream(
+ client,
+ [
+ _gemini_chunk(
+ parts=[_gemini_part(text="ok")], finish_reason=FinishReason.STOP
+ )
+ ],
+ )
+ final = _final_message(_collect(client, MULTIMODAL_MESSAGES))
+ self.assertEqual(final["content"], "ok")
+
+
+# ---------------------------------------------------------------------------
+# Ollama
+# ---------------------------------------------------------------------------
+class TestOllamaProvider(unittest.TestCase):
+ def _client(self):
+ return _make_client("ollama", model="llama3", base_url="http://localhost:9999")
+
+ def _run_with_response(self, client, response, messages):
+ # Ollama uses a non-streaming call when tools are present, via an
+ # internally-constructed async client.
+ fake_async = MagicMock()
+ fake_async.chat = AsyncMock(return_value=response)
+ with patch(
+ "frigate.genai.plugins.ollama.OllamaAsyncClient",
+ return_value=fake_async,
+ ):
+ return _collect(client, messages)
+
+ def test_tool_call_arguments_are_dict(self):
+ response = {
+ "message": {
+ "content": "",
+ "tool_calls": [
+ {
+ "function": {
+ "name": "search_objects",
+ "arguments": {"label": "person"},
+ }
+ }
+ ],
+ },
+ "done": True,
+ "done_reason": "stop",
+ "eval_count": 5,
+ "prompt_eval_count": 3,
+ "eval_duration": 1_000_000,
+ }
+ client = self._client()
+ final = _final_message(
+ self._run_with_response(client, response, SIMPLE_MESSAGES)
+ )
+ self.assertEqual(final["finish_reason"], "tool_calls")
+ _assert_tool_args_are_dicts(final)
+ self.assertEqual(final["tool_calls"][0]["arguments"], {"label": "person"})
+
+ def test_multimodal_message_normalizes_image(self):
+ # Ollama needs content as a string with images pulled into a separate
+ # field; the normalizer must extract both without crashing.
+ response = {
+ "message": {"content": "ok"},
+ "done": True,
+ "done_reason": "stop",
+ }
+ client = self._client()
+ final = _final_message(
+ self._run_with_response(client, response, MULTIMODAL_MESSAGES)
+ )
+ self.assertEqual(final["content"], "ok")
+
+ def test_normalize_multimodal_content(self):
+ from frigate.genai.plugins.ollama import _normalize_multimodal_content
+
+ text, images = _normalize_multimodal_content(MULTIMODAL_MESSAGES[-1]["content"])
+ self.assertIn("live image", text)
+ self.assertEqual(len(images), 1)
+ self.assertEqual(images[0], b"\xff\xd8\xff\xd9")
+
+
+# ---------------------------------------------------------------------------
+# llama.cpp
+# ---------------------------------------------------------------------------
+class _FakeStreamResponse:
+ def __init__(self, lines):
+ self._lines = lines
+
+ def raise_for_status(self):
+ return None
+
+ async def aiter_lines(self):
+ for line in self._lines:
+ yield line
+
+
+class _FakeStreamCtx:
+ def __init__(self, lines):
+ self._resp = _FakeStreamResponse(lines)
+
+ async def __aenter__(self):
+ return self._resp
+
+ async def __aexit__(self, *exc):
+ return False
+
+
+class _FakeAsyncClient:
+ def __init__(self, lines):
+ self._lines = lines
+
+ async def __aenter__(self):
+ return self
+
+ async def __aexit__(self, *exc):
+ return False
+
+ def stream(self, method, url, json=None, headers=None):
+ return _FakeStreamCtx(self._lines)
+
+
+class TestLlamaCppProvider(unittest.TestCase):
+ def _client(self):
+ return _make_client("llamacpp", model="m", base_url="http://localhost:9999")
+
+ def _run_with_lines(self, client, lines, messages):
+ with patch(
+ "frigate.genai.plugins.llama_cpp.httpx.AsyncClient",
+ return_value=_FakeAsyncClient(lines),
+ ):
+ return _collect(client, messages)
+
+ def test_stream_tool_call_arguments_are_dict(self):
+ lines = [
+ "data: "
+ + json.dumps(
+ {
+ "choices": [
+ {
+ "delta": {
+ "tool_calls": [
+ {
+ "index": 0,
+ "id": "c1",
+ "function": {
+ "name": "search_objects",
+ "arguments": '{"label":',
+ },
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ),
+ "data: "
+ + json.dumps(
+ {
+ "choices": [
+ {
+ "delta": {
+ "tool_calls": [
+ {
+ "index": 0,
+ "function": {"arguments": ' "person"}'},
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ),
+ "data: "
+ + json.dumps({"choices": [{"delta": {}, "finish_reason": "tool_calls"}]}),
+ "data: [DONE]",
+ ]
+ client = self._client()
+ final = _final_message(self._run_with_lines(client, lines, SIMPLE_MESSAGES))
+ self.assertEqual(final["finish_reason"], "tool_calls")
+ _assert_tool_args_are_dicts(final)
+ self.assertEqual(final["tool_calls"][0]["arguments"], {"label": "person"})
+
+ def test_stream_content_response(self):
+ lines = [
+ "data: " + json.dumps({"choices": [{"delta": {"content": "hel"}}]}),
+ "data: " + json.dumps({"choices": [{"delta": {"content": "lo"}}]}),
+ "data: "
+ + json.dumps({"choices": [{"delta": {}, "finish_reason": "stop"}]}),
+ "data: [DONE]",
+ ]
+ client = self._client()
+ events = self._run_with_lines(client, lines, SIMPLE_MESSAGES)
+ deltas = [v for (k, v) in events if k == "content_delta"]
+ self.assertEqual("".join(deltas), "hello")
+ self.assertEqual(_final_message(events)["content"], "hello")
+
+ def test_multimodal_message_does_not_crash(self):
+ lines = [
+ "data: " + json.dumps({"choices": [{"delta": {"content": "ok"}}]}),
+ "data: "
+ + json.dumps({"choices": [{"delta": {}, "finish_reason": "stop"}]}),
+ "data: [DONE]",
+ ]
+ client = self._client()
+ final = _final_message(self._run_with_lines(client, lines, MULTIMODAL_MESSAGES))
+ self.assertEqual(final["content"], "ok")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/frigate/test/test_go2rtc_stream_auth.py b/frigate/test/test_go2rtc_stream_auth.py
new file mode 100644
index 0000000000..b525c94ee1
--- /dev/null
+++ b/frigate/test/test_go2rtc_stream_auth.py
@@ -0,0 +1,175 @@
+"""Unit tests for `deny_response_for_go2rtc_stream`.
+
+Covers the camera-level authorization enforced in the `/auth` subrequest for
+the nginx-proxied go2rtc live-stream paths (MSE/WebRTC WebSockets and the
+WebRTC signaling endpoint). These paths name the stream via the `src` query
+param, which the static-media auth in `media_auth` does not inspect.
+"""
+
+import types
+import unittest
+
+from frigate.api.auth import deny_response_for_go2rtc_stream
+from frigate.config import FrigateConfig
+
+_CONFIG = {
+ "mqtt": {"host": "mqtt"},
+ "auth": {
+ "roles": {
+ "limited_user": ["front_door"],
+ "dual_user": ["front_door", "back_door"],
+ }
+ },
+ "cameras": {
+ "front_door": {
+ "ffmpeg": {
+ "inputs": [{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}]
+ },
+ "detect": {"height": 1080, "width": 1920, "fps": 5},
+ # go2rtc stream name differs from the camera name (substream)
+ "live": {"streams": {"Main Stream": "front_door_sub"}},
+ },
+ "back_door": {
+ "ffmpeg": {
+ "inputs": [{"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]}]
+ },
+ "detect": {"height": 1080, "width": 1920, "fps": 5},
+ },
+ "garage": {
+ "ffmpeg": {
+ "inputs": [{"path": "rtsp://10.0.0.3:554/video", "roles": ["detect"]}]
+ },
+ "detect": {"height": 1080, "width": 1920, "fps": 5},
+ },
+ },
+}
+
+
+def _request(config: FrigateConfig) -> types.SimpleNamespace:
+ return types.SimpleNamespace(app=types.SimpleNamespace(frigate_config=config))
+
+
+class TestDenyResponseForGo2rtcStream(unittest.TestCase):
+ def setUp(self) -> None:
+ self.config = FrigateConfig(**_CONFIG)
+ self.request = _request(self.config)
+
+ def _deny(self, url: str, role: str):
+ return deny_response_for_go2rtc_stream(url, role, self.request)
+
+ # --- non-stream paths pass through ---
+
+ def test_non_stream_path_passes_through(self):
+ self.assertIsNone(
+ self._deny("http://host/clips/back_door-1.jpg", "limited_user")
+ )
+
+ def test_empty_url_passes_through(self):
+ self.assertIsNone(self._deny("", "limited_user"))
+
+ def test_jsmpeg_path_not_handled_here(self):
+ # jsmpeg is authorized per-frame in the output pipeline, not here
+ self.assertIsNone(
+ self._deny("http://host/live/jsmpeg/back_door", "limited_user")
+ )
+
+ # --- restricted role: allowed vs forbidden cameras ---
+
+ def test_mse_allowed_camera(self):
+ self.assertIsNone(
+ self._deny("http://host/live/mse/api/ws?src=front_door", "limited_user")
+ )
+
+ def test_mse_forbidden_camera_denied(self):
+ self.assertEqual(
+ self._deny("http://host/live/mse/api/ws?src=back_door", "limited_user"),
+ 403,
+ )
+
+ def test_webrtc_ws_forbidden_camera_denied(self):
+ self.assertEqual(
+ self._deny("http://host/live/webrtc/api/ws?src=back_door", "limited_user"),
+ 403,
+ )
+
+ def test_webrtc_signaling_forbidden_camera_denied(self):
+ self.assertEqual(
+ self._deny("http://host/api/go2rtc/webrtc?src=back_door", "limited_user"),
+ 403,
+ )
+
+ def test_unknown_camera_denied(self):
+ self.assertEqual(
+ self._deny("http://host/live/mse/api/ws?src=nonexistent", "limited_user"),
+ 403,
+ )
+
+ def test_missing_src_denied(self):
+ self.assertEqual(self._deny("http://host/live/mse/api/ws", "limited_user"), 403)
+
+ # --- multi-camera role: each assigned camera allowed, others denied ---
+
+ def test_multi_camera_role_allows_first_assigned(self):
+ self.assertIsNone(
+ self._deny("http://host/live/mse/api/ws?src=front_door", "dual_user")
+ )
+
+ def test_multi_camera_role_allows_second_assigned(self):
+ self.assertIsNone(
+ self._deny("http://host/live/mse/api/ws?src=back_door", "dual_user")
+ )
+
+ def test_multi_camera_role_denies_unassigned(self):
+ # garage is configured but not in dual_user's allow-list
+ self.assertEqual(
+ self._deny("http://host/live/mse/api/ws?src=garage", "dual_user"),
+ 403,
+ )
+
+ # --- substream names resolve to their owning camera ---
+
+ def test_allowed_substream_resolves_to_owning_camera(self):
+ # front_door_sub is owned by front_door, which limited_user may access
+ self.assertIsNone(
+ self._deny("http://host/live/mse/api/ws?src=front_door_sub", "limited_user")
+ )
+
+ # --- multiple src values: deny if any is forbidden ---
+
+ def test_multiple_src_one_forbidden_denied(self):
+ self.assertEqual(
+ self._deny(
+ "http://host/live/mse/api/ws?src=front_door&src=back_door",
+ "limited_user",
+ ),
+ 403,
+ )
+
+ def test_multiple_src_all_allowed(self):
+ self.assertIsNone(
+ self._deny(
+ "http://host/live/mse/api/ws?src=front_door&src=front_door_sub",
+ "limited_user",
+ )
+ )
+
+ # --- privileged roles bypass the check ---
+
+ def test_admin_bypasses(self):
+ self.assertIsNone(
+ self._deny("http://host/live/mse/api/ws?src=back_door", "admin")
+ )
+
+ def test_builtin_viewer_role_bypasses(self):
+ # the built-in viewer role is not in the config allow-list map, so it
+ # is treated as full access
+ self.assertIsNone(
+ self._deny("http://host/live/mse/api/ws?src=back_door", "viewer")
+ )
+
+ def test_missing_role_bypasses(self):
+ self.assertIsNone(self._deny("http://host/live/mse/api/ws?src=back_door", None))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/frigate/test/test_gpu_stats.py b/frigate/test/test_gpu_stats.py
index 2604c4002c..e94625f645 100644
--- a/frigate/test/test_gpu_stats.py
+++ b/frigate/test/test_gpu_stats.py
@@ -7,8 +7,6 @@ from frigate.util.services import get_amd_gpu_stats, get_intel_gpu_stats
class TestGpuStats(unittest.TestCase):
def setUp(self):
self.amd_results = "Unknown Radeon card. <= R500 won't work, new cards might.\nDumping to -, line limit 1.\n1664070990.607556: bus 10, gpu 4.17%, ee 0.00%, vgt 0.00%, ta 0.00%, tc 0.00%, sx 0.00%, sh 0.00%, spi 0.83%, smx 0.00%, cr 0.00%, sc 0.00%, pa 0.00%, db 0.00%, cb 0.00%, vram 60.37% 294.04mb, gtt 0.33% 52.21mb, mclk 100.00% 1.800ghz, sclk 26.65% 0.533ghz\n"
- self.intel_results = """{"period":{"duration":1.194033,"unit":"ms"},"frequency":{"requested":0.000000,"actual":0.000000,"unit":"MHz"},"interrupts":{"count":3349.991164,"unit":"irq/s"},"rc6":{"value":47.844741,"unit":"%"},"engines":{"Render/3D/0":{"busy":0.000000,"sema":0.000000,"wait":0.000000,"unit":"%"},"Blitter/0":{"busy":0.000000,"sema":0.000000,"wait":0.000000,"unit":"%"},"Video/0":{"busy":4.533124,"sema":0.000000,"wait":0.000000,"unit":"%"},"Video/1":{"busy":6.194385,"sema":0.000000,"wait":0.000000,"unit":"%"},"VideoEnhance/0":{"busy":0.000000,"sema":0.000000,"wait":0.000000,"unit":"%"}}},{"period":{"duration":1.189291,"unit":"ms"},"frequency":{"requested":0.000000,"actual":0.000000,"unit":"MHz"},"interrupts":{"count":0.000000,"unit":"irq/s"},"rc6":{"value":100.000000,"unit":"%"},"engines":{"Render/3D/0":{"busy":0.000000,"sema":0.000000,"wait":0.000000,"unit":"%"},"Blitter/0":{"busy":0.000000,"sema":0.000000,"wait":0.000000,"unit":"%"},"Video/0":{"busy":0.000000,"sema":0.000000,"wait":0.000000,"unit":"%"},"Video/1":{"busy":0.000000,"sema":0.000000,"wait":0.000000,"unit":"%"},"VideoEnhance/0":{"busy":0.000000,"sema":0.000000,"wait":0.000000,"unit":"%"}}}"""
- self.nvidia_results = "name, utilization.gpu [%], memory.used [MiB], memory.total [MiB]\nNVIDIA GeForce RTX 3050, 42 %, 5036 MiB, 8192 MiB\n"
@patch("subprocess.run")
def test_amd_gpu_stats(self, sp):
@@ -19,32 +17,140 @@ class TestGpuStats(unittest.TestCase):
amd_stats = get_amd_gpu_stats()
assert amd_stats == {"gpu": "4.17%", "mem": "60.37%"}
- # @patch("subprocess.run")
- # def test_nvidia_gpu_stats(self, sp):
- # process = MagicMock()
- # process.returncode = 0
- # process.stdout = self.nvidia_results
- # sp.return_value = process
- # nvidia_stats = get_nvidia_gpu_stats()
- # assert nvidia_stats == {
- # "name": "NVIDIA GeForce RTX 3050",
- # "gpu": "42 %",
- # "mem": "61.5 %",
- # }
+ @patch("frigate.stats.intel_gpu_info.intel_gpu_name_resolver.get_names")
+ @patch("frigate.util.services.time.sleep")
+ @patch("frigate.util.services.time.monotonic")
+ @patch("frigate.util.services._read_intel_drm_fdinfo")
+ def test_intel_gpu_stats_fdinfo(self, read_fdinfo, monotonic, sleep, get_names):
+ # 1 second of wall clock between snapshots
+ monotonic.side_effect = [0.0, 1.0]
+ get_names.return_value = {"0000:00:02.0": "Intel Graphics"}
- @patch("subprocess.run")
- def test_intel_gpu_stats(self, sp):
- process = MagicMock()
- process.returncode = 124
- process.stdout = self.intel_results
- sp.return_value = process
- intel_stats = get_intel_gpu_stats(False)
- # rc6 values: 47.844741 and 100.0 → avg 73.92 → gpu = 100 - 73.92 = 26.08%
- # Render/3D/0: 0.0 and 0.0 → enc = 0.0%
- # Video/0: 4.533124 and 0.0 → dec = 2.27%
- assert intel_stats == {
- "gpu": "26.08%",
- "mem": "-%",
- "compute": "0.0%",
- "dec": "2.27%",
+ # Two i915 clients on the same iGPU. Engine values are cumulative ns.
+ # Deltas over the 1s window:
+ # client A (pid 100): render +200_000_000 (20%), video +500_000_000 (50%),
+ # video-enhance +100_000_000 (10%)
+ # client B (pid 200): compute +100_000_000 (10%)
+ # Engine totals → render 20, video 50, video-enhance 10, compute 10
+ # → compute = render + compute = 30
+ # → dec = video + video-enhance = 60
+ # → gpu = compute + dec = 90
+ snapshot_a = {
+ ("0000:00:02.0", "1", "100"): {
+ "driver": "i915",
+ "pid": "100",
+ "engines": {
+ "render": (1_000_000_000, 0, 1),
+ "video": (5_000_000_000, 0, 1),
+ "video-enhance": (200_000_000, 0, 1),
+ "compute": (0, 0, 1),
+ },
+ },
+ ("0000:00:02.0", "2", "200"): {
+ "driver": "i915",
+ "pid": "200",
+ "engines": {
+ "render": (0, 0, 1),
+ "compute": (2_000_000_000, 0, 1),
+ },
+ },
}
+ snapshot_b = {
+ ("0000:00:02.0", "1", "100"): {
+ "driver": "i915",
+ "pid": "100",
+ "engines": {
+ "render": (1_200_000_000, 0, 1),
+ "video": (5_500_000_000, 0, 1),
+ "video-enhance": (300_000_000, 0, 1),
+ "compute": (0, 0, 1),
+ },
+ },
+ ("0000:00:02.0", "2", "200"): {
+ "driver": "i915",
+ "pid": "200",
+ "engines": {
+ "render": (0, 0, 1),
+ "compute": (2_100_000_000, 0, 1),
+ },
+ },
+ }
+ read_fdinfo.side_effect = [snapshot_a, snapshot_b]
+
+ intel_stats = get_intel_gpu_stats(None)
+
+ sleep.assert_called_once()
+ assert intel_stats == {
+ "0000:00:02.0": {
+ "name": "Intel Graphics",
+ "vendor": "intel",
+ "gpu": "90.0%",
+ "mem": "-%",
+ "compute": "30.0%",
+ "dec": "60.0%",
+ "clients": {"100": "80.0%", "200": "10.0%"},
+ },
+ }
+
+ @patch("frigate.stats.intel_gpu_info.intel_gpu_name_resolver.get_names")
+ @patch("frigate.util.services.time.sleep")
+ @patch("frigate.util.services.time.monotonic")
+ @patch("frigate.util.services._read_intel_drm_fdinfo")
+ def test_intel_gpu_stats_xe_capacity(
+ self, read_fdinfo, monotonic, sleep, get_names
+ ):
+ # Xe engines report cumulative cycles paired with total cycles, plus a
+ # per-class capacity. drm-cycles-* is summed across every instance of a
+ # class, so on Battlemage (capacity 2 for vcs/vecs) busy/total must be
+ # divided by capacity to land in 0-100%.
+ monotonic.side_effect = [0.0, 1.0]
+ get_names.return_value = {"0000:03:00.0": "Intel Arc"}
+
+ # Deltas over the window (busy, total): render 200/1000 cap 1 = 20%,
+ # video 800/1000 cap 2 = 40%, video-enhance 400/1000 cap 2 = 20%,
+ # compute 100/1000 cap 1 = 10%. Without the capacity divisor video/
+ # video-enhance would read 80%/40% and dec would clamp at 100%.
+ snapshot_a = {
+ ("0000:03:00.0", "1", "300"): {
+ "driver": "xe",
+ "pid": "300",
+ "engines": {
+ "render": (0, 0, 1),
+ "video": (0, 0, 2),
+ "video-enhance": (0, 0, 2),
+ "compute": (0, 0, 1),
+ },
+ },
+ }
+ snapshot_b = {
+ ("0000:03:00.0", "1", "300"): {
+ "driver": "xe",
+ "pid": "300",
+ "engines": {
+ "render": (200, 1000, 1),
+ "video": (800, 1000, 2),
+ "video-enhance": (400, 1000, 2),
+ "compute": (100, 1000, 1),
+ },
+ },
+ }
+ read_fdinfo.side_effect = [snapshot_a, snapshot_b]
+
+ intel_stats = get_intel_gpu_stats(None)
+
+ assert intel_stats == {
+ "0000:03:00.0": {
+ "name": "Intel Arc",
+ "vendor": "intel",
+ "gpu": "90.0%",
+ "mem": "-%",
+ "compute": "30.0%",
+ "dec": "60.0%",
+ "clients": {"300": "90.0%"},
+ },
+ }
+
+ @patch("frigate.util.services._read_intel_drm_fdinfo")
+ def test_intel_gpu_stats_no_clients(self, read_fdinfo):
+ read_fdinfo.return_value = {}
+ assert get_intel_gpu_stats(None) is None
diff --git a/frigate/test/test_keyframe_analysis.py b/frigate/test/test_keyframe_analysis.py
new file mode 100644
index 0000000000..14dad5849d
--- /dev/null
+++ b/frigate/test/test_keyframe_analysis.py
@@ -0,0 +1,110 @@
+"""Tests for keyframe-spacing analysis used to detect smart/+ codecs."""
+
+import unittest
+from unittest.mock import AsyncMock, MagicMock, patch
+
+from frigate.util.services import (
+ analyze_record_keyframes,
+ classify_keyframe_gaps,
+ parse_keyframe_packets,
+)
+
+
+class TestClassifyKeyframeGaps(unittest.TestCase):
+ def test_ok_when_gaps_small(self):
+ # keyframes every ~1s
+ pts = [0.0, 1.0, 2.0, 3.0, 4.0]
+ result = classify_keyframe_gaps(pts, segment_time=10)
+ self.assertEqual(result["severity"], "ok")
+ self.assertEqual(result["max_gap"], 1.0)
+ self.assertEqual(result["keyframe_count"], 5)
+ self.assertEqual(result["thresholds"], {"warning": 4.0, "error": 10})
+
+ def test_warning_when_gap_exceeds_four_seconds(self):
+ pts = [0.0, 1.0, 6.5] # 5.5s gap
+ result = classify_keyframe_gaps(pts, segment_time=10)
+ self.assertEqual(result["severity"], "warning")
+ self.assertEqual(result["max_gap"], 5.5)
+
+ def test_error_when_gap_exceeds_segment_time(self):
+ pts = [0.0, 12.0] # 12s gap > 10s segment
+ result = classify_keyframe_gaps(pts, segment_time=10)
+ self.assertEqual(result["severity"], "error")
+
+ def test_error_threshold_tracks_segment_time(self):
+ pts = [0.0, 6.0] # 6s gap, segment_time=5 -> error
+ result = classify_keyframe_gaps(pts, segment_time=5)
+ self.assertEqual(result["severity"], "error")
+
+ def test_unknown_with_single_keyframe(self):
+ result = classify_keyframe_gaps([1.0], segment_time=10)
+ self.assertEqual(result["severity"], "unknown")
+ self.assertIsNone(result["max_gap"])
+ self.assertEqual(result["keyframe_count"], 1)
+
+ def test_unknown_with_no_keyframes(self):
+ result = classify_keyframe_gaps([], segment_time=10)
+ self.assertEqual(result["severity"], "unknown")
+ self.assertEqual(result["keyframe_count"], 0)
+
+
+class TestParseKeyframePackets(unittest.TestCase):
+ def test_extracts_keyframe_pts_and_max(self):
+ output = "0.000000,K__\n0.033333,___\n1.000000,K__\n1.500000,___\n"
+ keyframe_pts, max_pts = parse_keyframe_packets(output)
+ self.assertEqual(keyframe_pts, [0.0, 1.0])
+ self.assertEqual(max_pts, 1.5)
+
+ def test_skips_unparseable_and_empty_lines(self):
+ output = "N/A,K__\n\n2.0,K__\nbad line\n"
+ keyframe_pts, max_pts = parse_keyframe_packets(output)
+ self.assertEqual(keyframe_pts, [2.0])
+ self.assertEqual(max_pts, 2.0)
+
+ def test_empty_output(self):
+ keyframe_pts, max_pts = parse_keyframe_packets("")
+ self.assertEqual(keyframe_pts, [])
+ self.assertIsNone(max_pts)
+
+
+class TestAnalyzeRecordKeyframes(unittest.IsolatedAsyncioTestCase):
+ async def test_merges_duration_and_classification(self):
+ csv = b"0.0,K__\n1.0,___\n6.0,K__\n7.0,___\n"
+ proc = MagicMock()
+ proc.communicate = AsyncMock(return_value=(csv, b""))
+ ffmpeg = MagicMock()
+ ffmpeg.ffprobe_path = "/usr/bin/ffprobe"
+
+ with patch(
+ "frigate.util.services.asyncio.create_subprocess_exec",
+ AsyncMock(return_value=proc),
+ ):
+ result = await analyze_record_keyframes(
+ ffmpeg, "rtsp://cam/stream", segment_time=10
+ )
+
+ self.assertEqual(result["severity"], "warning") # 6s gap > 4s
+ self.assertEqual(result["max_gap"], 6.0)
+ self.assertEqual(result["duration_observed"], 7.0)
+
+ async def test_timeout_returns_unknown(self):
+ proc = MagicMock()
+ proc.communicate = AsyncMock(side_effect=TimeoutError())
+ proc.kill = MagicMock()
+ ffmpeg = MagicMock()
+ ffmpeg.ffprobe_path = "/usr/bin/ffprobe"
+
+ with patch(
+ "frigate.util.services.asyncio.create_subprocess_exec",
+ AsyncMock(return_value=proc),
+ ):
+ result = await analyze_record_keyframes(
+ ffmpeg, "rtsp://cam/stream", segment_time=10
+ )
+
+ self.assertEqual(result["severity"], "unknown")
+ proc.kill.assert_called_once()
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/frigate/test/test_maintainer.py b/frigate/test/test_maintainer.py
index 49712749ef..715cd5a1a1 100644
--- a/frigate/test/test_maintainer.py
+++ b/frigate/test/test_maintainer.py
@@ -1,3 +1,4 @@
+import datetime
import sys
import unittest
from unittest.mock import MagicMock, patch
@@ -74,6 +75,86 @@ class TestMaintainer(unittest.IsolatedAsyncioTestCase):
f"Expected a single warning for unexpected files, got {len(matching)}",
)
+ async def test_drops_quiet_segment_when_only_motion_retention(self):
+ # Regression: when motion retention is enabled but a segment has no
+ # motion and no review overlaps it, the segment must still be dropped.
+ # Otherwise it sits in cache forever, accumulates, and triggers the
+ # "Unable to keep up with recording segments in cache" warning every
+ # ~10s as the overflow trim in move_files discards the oldest one.
+ config = MagicMock(spec=FrigateConfig)
+
+ camera_config = MagicMock()
+ camera_config.record.enabled = True
+ camera_config.record.continuous.days = 0
+ camera_config.record.motion.days = 1
+ camera_config.record.event_pre_capture = 5
+ config.cameras = {"test_cam": camera_config}
+
+ stop_event = MagicMock()
+ maintainer = RecordingMaintainer(config, stop_event)
+
+ now = datetime.datetime.now(datetime.UTC)
+ start_time = now - datetime.timedelta(seconds=20)
+ end_time = now - datetime.timedelta(seconds=10)
+ cache_path = "/tmp/cache/test_cam@20260417150000+0000.mp4"
+
+ maintainer.end_time_cache = {cache_path: (end_time, 10.0)}
+ # Single processed frame well past end_time with no motion/objects.
+ maintainer.object_recordings_info["test_cam"] = [(now.timestamp(), [], [], [])]
+ maintainer.audio_recordings_info["test_cam"] = []
+
+ maintainer.drop_segment = MagicMock()
+ maintainer.recordings_publisher = MagicMock()
+
+ result = await maintainer.validate_and_move_segment(
+ "test_cam",
+ reviews=[],
+ recording={"start_time": start_time, "cache_path": cache_path},
+ )
+
+ self.assertIsNone(result)
+ maintainer.drop_segment.assert_called_once_with(cache_path)
+
+ async def test_expire_stale_recordings_info_drops_only_absent_cameras(self):
+ config = MagicMock(spec=FrigateConfig)
+ config.cameras = {}
+ stop_event = MagicMock()
+ maintainer = RecordingMaintainer(config, stop_event)
+
+ now = datetime.datetime.now().timestamp()
+ ancient = now - 86400
+ recent = now - 1
+
+ maintainer.object_recordings_info["present_cam"] = [(ancient, [], [], [])]
+ maintainer.audio_recordings_info["present_cam"] = [(ancient, 0, [])]
+
+ maintainer.object_recordings_info["absent_cam"] = [
+ (ancient, [], [], []),
+ (recent, [], [], []),
+ ]
+ maintainer.audio_recordings_info["absent_cam"] = [
+ (ancient, 0, []),
+ (recent, 0, []),
+ ]
+
+ grouped_recordings = {"present_cam": [{"start_time": ancient}]}
+
+ maintainer._expire_stale_recordings_info(grouped_recordings)
+
+ self.assertEqual(
+ maintainer.object_recordings_info["present_cam"], [(ancient, [], [], [])]
+ )
+ self.assertEqual(
+ maintainer.audio_recordings_info["present_cam"], [(ancient, 0, [])]
+ )
+
+ self.assertEqual(
+ maintainer.object_recordings_info["absent_cam"], [(recent, [], [], [])]
+ )
+ self.assertEqual(
+ maintainer.audio_recordings_info["absent_cam"], [(recent, 0, [])]
+ )
+
if __name__ == "__main__":
unittest.main()
diff --git a/frigate/test/test_media_auth.py b/frigate/test/test_media_auth.py
new file mode 100644
index 0000000000..d025fea614
--- /dev/null
+++ b/frigate/test/test_media_auth.py
@@ -0,0 +1,381 @@
+"""Unit tests for `frigate.api.media_auth`.
+
+Covers URI classification, the role-vs-camera decision matrix, and the export
+DB-lookup path. These are pure functions/DB lookups — no HTTP stack involved.
+"""
+
+import datetime
+import logging
+import os
+import unittest
+
+from peewee_migrate import Router
+from playhouse.sqlite_ext import SqliteExtDatabase
+from playhouse.sqliteq import SqliteQueueDatabase
+
+from frigate.api.media_auth import (
+ MediaAuthResolution,
+ deny_response_for_media_uri,
+ extract_path,
+ resolve_media_uri,
+)
+from frigate.config import FrigateConfig
+from frigate.models import Event, Export, Recordings, ReviewSegment
+from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS
+
+_CONFIG = {
+ "mqtt": {"host": "mqtt"},
+ "auth": {"roles": {"limited_user": ["front_door"]}},
+ "cameras": {
+ "front_door": {
+ "ffmpeg": {
+ "inputs": [{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}]
+ },
+ "detect": {"height": 1080, "width": 1920, "fps": 5},
+ },
+ "back_door": {
+ "ffmpeg": {
+ "inputs": [{"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]}]
+ },
+ "detect": {"height": 1080, "width": 1920, "fps": 5},
+ },
+ # Camera name with a hyphen — exercises longest-prefix match.
+ "back-yard": {
+ "ffmpeg": {
+ "inputs": [{"path": "rtsp://10.0.0.3:554/video", "roles": ["detect"]}]
+ },
+ "detect": {"height": 1080, "width": 1920, "fps": 5},
+ },
+ },
+}
+
+
+class TestExtractPath(unittest.TestCase):
+ def test_full_url(self):
+ self.assertEqual(
+ extract_path("http://host:8971/clips/front_door-1.jpg"),
+ "/clips/front_door-1.jpg",
+ )
+
+ def test_strips_query_string(self):
+ self.assertEqual(
+ extract_path("http://h/recordings/2026-05-11/14/front_door/00.00.mp4?t=1"),
+ "/recordings/2026-05-11/14/front_door/00.00.mp4",
+ )
+
+ def test_path_only(self):
+ self.assertEqual(extract_path("/exports/x.mp4"), "/exports/x.mp4")
+
+ def test_percent_decoded(self):
+ self.assertEqual(
+ extract_path("http://h/clips/front%20door-1.jpg"),
+ "/clips/front door-1.jpg",
+ )
+
+ def test_empty(self):
+ self.assertIsNone(extract_path(None))
+ self.assertIsNone(extract_path(""))
+
+
+class TestResolveMediaUri(unittest.TestCase):
+ def setUp(self):
+ self.config = FrigateConfig(**_CONFIG)
+
+ def _assert(self, uri, resolution, camera=None):
+ got_resolution, got_camera = resolve_media_uri(uri, self.config)
+ self.assertEqual(got_resolution, resolution, uri)
+ self.assertEqual(got_camera, camera, uri)
+
+ def test_unknown_paths(self):
+ self._assert("/api/events", MediaAuthResolution.UNKNOWN)
+ self._assert("/", MediaAuthResolution.UNKNOWN)
+ self._assert("", MediaAuthResolution.UNKNOWN)
+
+ def test_recordings(self):
+ self._assert("/recordings/", MediaAuthResolution.LISTING_NEUTRAL)
+ self._assert("/recordings/2026-05-11/", MediaAuthResolution.LISTING_NEUTRAL)
+ self._assert(
+ "/recordings/2026-05-11/14/", MediaAuthResolution.LISTING_MULTI_CAMERA
+ )
+ self._assert(
+ "/recordings/2026-05-11/14/front_door/",
+ MediaAuthResolution.CAMERA,
+ camera="front_door",
+ )
+ self._assert(
+ "/recordings/2026-05-11/14/back_door/00.00.mp4",
+ MediaAuthResolution.CAMERA,
+ camera="back_door",
+ )
+
+ def test_clip_flat_filename_resolves_camera(self):
+ self._assert(
+ "/clips/front_door-1234.jpg",
+ MediaAuthResolution.CAMERA,
+ camera="front_door",
+ )
+ self._assert(
+ "/clips/back_door-1234-clean.webp",
+ MediaAuthResolution.CAMERA,
+ camera="back_door",
+ )
+
+ def test_clip_filename_with_hyphenated_camera_name(self):
+ # Camera name "back-yard" itself contains a hyphen; longest-prefix
+ # match must pick `back-yard`, not the bogus `back` prefix.
+ self._assert(
+ "/clips/back-yard-1234.jpg",
+ MediaAuthResolution.CAMERA,
+ camera="back-yard",
+ )
+
+ def test_clip_filename_no_matching_camera(self):
+ # Looks like a media path but couldn't classify — fail closed for
+ # restricted users (UNRESOLVED_MEDIA), not pass-through.
+ self._assert(
+ "/clips/nonexistent-1234.jpg", MediaAuthResolution.UNRESOLVED_MEDIA
+ )
+
+ def test_clip_thumbs(self):
+ self._assert("/clips/thumbs/", MediaAuthResolution.LISTING_MULTI_CAMERA)
+ self._assert(
+ "/clips/thumbs/front_door/",
+ MediaAuthResolution.CAMERA,
+ camera="front_door",
+ )
+ self._assert(
+ "/clips/thumbs/back_door/abc.webp",
+ MediaAuthResolution.CAMERA,
+ camera="back_door",
+ )
+
+ def test_clip_previews(self):
+ self._assert("/clips/previews/", MediaAuthResolution.LISTING_MULTI_CAMERA)
+ self._assert(
+ "/clips/previews/front_door/",
+ MediaAuthResolution.CAMERA,
+ camera="front_door",
+ )
+ self._assert(
+ "/clips/previews/back_door/segment.mp4",
+ MediaAuthResolution.CAMERA,
+ camera="back_door",
+ )
+
+ def test_clip_review_thumbs(self):
+ # Format: /clips/review/thumb-{camera}-{review_id}.webp (frigate/review/maintainer.py).
+ self._assert(
+ "/clips/review/thumb-front_door-abc123.webp",
+ MediaAuthResolution.CAMERA,
+ camera="front_door",
+ )
+ # Hyphenated camera name — longest-prefix match.
+ self._assert(
+ "/clips/review/thumb-back-yard-abc123.webp",
+ MediaAuthResolution.CAMERA,
+ camera="back-yard",
+ )
+ # Unknown camera prefix → unresolved, not allowed for restricted users.
+ self._assert(
+ "/clips/review/thumb-unknown-cam-abc123.webp",
+ MediaAuthResolution.UNRESOLVED_MEDIA,
+ )
+
+ def test_clip_admin_only_subtrees(self):
+ self._assert("/clips/faces/train/foo.webp", MediaAuthResolution.ADMIN_ONLY)
+ self._assert("/clips/faces/", MediaAuthResolution.ADMIN_ONLY)
+ self._assert("/clips/genai-requests/x/0.webp", MediaAuthResolution.ADMIN_ONLY)
+ self._assert(
+ "/clips/preview_restart_cache/x.mp4", MediaAuthResolution.ADMIN_ONLY
+ )
+ self._assert("/clips/some_model/train/x.jpg", MediaAuthResolution.ADMIN_ONLY)
+ self._assert("/clips/some_model/dataset/x.jpg", MediaAuthResolution.ADMIN_ONLY)
+
+ def test_clip_unknown_subtree_is_unresolved(self):
+ # Unknown /clips/{x}/{y}/... subtree falls through as unresolved (not
+ # admin-only) so restricted users get 403 without admins being denied
+ # access to legitimate but unrecognized resources.
+ self._assert("/clips/random_dir/foo.jpg", MediaAuthResolution.UNRESOLVED_MEDIA)
+
+ def test_clip_top_level_listing(self):
+ self._assert("/clips/", MediaAuthResolution.LISTING_MULTI_CAMERA)
+
+ def test_exports_listing(self):
+ self._assert("/exports/", MediaAuthResolution.LISTING_MULTI_CAMERA)
+
+
+class TestExportResolution(unittest.TestCase):
+ """Export resolution requires a DB lookup."""
+
+ def setUp(self):
+ migrate_db = SqliteExtDatabase("test.db")
+ del logging.getLogger("peewee_migrate").handlers[:]
+ Router(migrate_db).run()
+ migrate_db.close()
+ self.db = SqliteQueueDatabase(TEST_DB)
+ self.db.bind([Event, ReviewSegment, Recordings, Export])
+ self.config = FrigateConfig(**_CONFIG)
+
+ def tearDown(self):
+ if not self.db.is_closed():
+ self.db.close()
+ for f in TEST_DB_CLEANUPS:
+ try:
+ os.remove(f)
+ except OSError:
+ pass
+
+ def _insert_export(self, export_id, camera, filename):
+ Export.insert(
+ id=export_id,
+ camera=camera,
+ name=f"export-{export_id}",
+ date=int(datetime.datetime.now().timestamp()),
+ video_path=f"/media/frigate/exports/{filename}",
+ thumb_path=f"/media/frigate/exports/{filename}.jpg",
+ in_progress=False,
+ ).execute()
+
+ def test_export_resolves_camera(self):
+ self._insert_export(
+ "exp1", "back_door", "back_door_20260511_140000-20260511_150000_abc123.mp4"
+ )
+ resolution, camera = resolve_media_uri(
+ "/exports/back_door_20260511_140000-20260511_150000_abc123.mp4",
+ self.config,
+ )
+ self.assertEqual(resolution, MediaAuthResolution.CAMERA)
+ self.assertEqual(camera, "back_door")
+
+ def test_unknown_export_is_unresolved(self):
+ # No matching row → UNRESOLVED_MEDIA (fail closed for restricted users),
+ # not UNKNOWN (which would pass-through).
+ resolution, camera = resolve_media_uri(
+ "/exports/does_not_exist.mp4", self.config
+ )
+ self.assertEqual(resolution, MediaAuthResolution.UNRESOLVED_MEDIA)
+ self.assertIsNone(camera)
+
+ def test_export_anchored_match_not_endswith(self):
+ # Anchored exact-path equality must NOT match by filename suffix.
+ # A request like /exports/clip.mp4 must not authorize against a row at
+ # /media/frigate/exports/back_door_clip.mp4 just because the suffix matches.
+ self._insert_export("exp_bd", "back_door", "back_door_clip.mp4")
+ self._insert_export("exp_fd", "front_door", "front_door_clip.mp4")
+ resolution, _ = resolve_media_uri("/exports/clip.mp4", self.config)
+ self.assertEqual(resolution, MediaAuthResolution.UNRESOLVED_MEDIA)
+
+
+class TestDenyResponseForMediaUri(unittest.TestCase):
+ """End-to-end decision check used by /auth."""
+
+ def setUp(self):
+ self.config = FrigateConfig(**_CONFIG)
+
+ def _deny(self, url, role):
+ return deny_response_for_media_uri(url, role, self.config)
+
+ def test_admin_always_allowed(self):
+ self.assertIsNone(self._deny("/clips/back_door-1.jpg", "admin"))
+ self.assertIsNone(self._deny("/clips/", "admin"))
+ self.assertIsNone(self._deny("/clips/faces/x.webp", "admin"))
+ self.assertIsNone(
+ self._deny("/recordings/2026-05-11/14/back_door/00.00.mp4", "admin")
+ )
+
+ def test_unrestricted_role_allowed(self):
+ # "viewer" role has no entry in roles_dict → full access (matches the
+ # behavior of require_camera_access).
+ self.assertIsNone(self._deny("/clips/back_door-1.jpg", "viewer"))
+ self.assertIsNone(self._deny("/clips/", "viewer"))
+
+ def test_restricted_role_allowed_camera(self):
+ self.assertIsNone(self._deny("/clips/front_door-1.jpg", "limited_user"))
+ self.assertIsNone(
+ self._deny("/recordings/2026-05-11/14/front_door/00.00.mp4", "limited_user")
+ )
+ self.assertIsNone(
+ self._deny("/clips/thumbs/front_door/abc.webp", "limited_user")
+ )
+
+ def test_restricted_role_blocked_other_camera(self):
+ self.assertEqual(self._deny("/clips/back_door-1.jpg", "limited_user"), 403)
+ self.assertEqual(
+ self._deny("/recordings/2026-05-11/14/back_door/00.00.mp4", "limited_user"),
+ 403,
+ )
+ self.assertEqual(
+ self._deny("/clips/thumbs/back_door/abc.webp", "limited_user"), 403
+ )
+
+ def test_restricted_role_blocked_admin_only(self):
+ self.assertEqual(self._deny("/clips/faces/train/foo.webp", "limited_user"), 403)
+
+ def test_restricted_role_blocked_multi_camera_listing(self):
+ self.assertEqual(self._deny("/clips/", "limited_user"), 403)
+ self.assertEqual(self._deny("/exports/", "limited_user"), 403)
+ self.assertEqual(self._deny("/recordings/2026-05-11/14/", "limited_user"), 403)
+
+ def test_restricted_role_allowed_neutral_listing(self):
+ self.assertIsNone(self._deny("/recordings/", "limited_user"))
+ self.assertIsNone(self._deny("/recordings/2026-05-11/", "limited_user"))
+
+ def test_non_media_uri_passes_through(self):
+ self.assertIsNone(self._deny("/api/events", "limited_user"))
+ self.assertIsNone(self._deny("http://h/login", "limited_user"))
+
+ def test_missing_header(self):
+ self.assertIsNone(self._deny(None, "limited_user"))
+ self.assertIsNone(self._deny("", "limited_user"))
+
+ def test_traversal_in_media_uri_denied_for_all_roles(self):
+ # Bypass attempt: parts[3] looks like an allowed camera, but the
+ # normalized path nginx would serve points at a forbidden camera.
+ # Both restricted and admin should be denied — the URI is malformed
+ # and we refuse to make an auth decision against it.
+ traversal_uris = [
+ "/recordings/2026-05-11/14/front_door/../back_door/00.00.mp4",
+ "/clips/front_door-1.jpg/../back_door-1.jpg",
+ "/exports/../recordings/2026-05-11/14/back_door/00.00.mp4",
+ "/clips/./back_door-1.jpg",
+ ]
+ for uri in traversal_uris:
+ self.assertEqual(self._deny(uri, "limited_user"), 403, uri)
+ self.assertEqual(self._deny(uri, "admin"), 403, uri)
+ self.assertEqual(self._deny(uri, "viewer"), 403, uri)
+
+ def test_traversal_outside_media_passes_through(self):
+ # `..` in non-media URIs is not our problem; the backend handles it.
+ self.assertIsNone(self._deny("/api/foo/../bar", "limited_user"))
+
+ def test_percent_encoded_traversal_denied(self):
+ # nginx may decode percent-encoded `%2E%2E` to `..` before serving;
+ # we must apply the same denial after percent-decoding.
+ self.assertEqual(
+ self._deny(
+ "/recordings/2026-05-11/14/front_door/%2E%2E/back_door/00.mp4",
+ "limited_user",
+ ),
+ 403,
+ )
+
+ def test_unresolved_media_fails_closed_for_restricted(self):
+ # Restricted user requesting a media URI we can't classify (no DB row,
+ # unknown clip prefix, unknown clip subtree) must be denied.
+ self.assertEqual(self._deny("/clips/nonexistent-1.jpg", "limited_user"), 403)
+ self.assertEqual(self._deny("/clips/random_dir/foo.jpg", "limited_user"), 403)
+ self.assertEqual(
+ self._deny("/clips/review/thumb-unknown_cam-1.webp", "limited_user"),
+ 403,
+ )
+
+ def test_unresolved_media_allowed_for_admin(self):
+ # Admin and full-access roles are *not* denied on UNRESOLVED_MEDIA —
+ # nginx returns 404 if the file doesn't exist on disk anyway, and we
+ # don't want a stale DB to lock out admins.
+ self.assertIsNone(self._deny("/clips/nonexistent-1.jpg", "admin"))
+ self.assertIsNone(self._deny("/clips/nonexistent-1.jpg", "viewer"))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/frigate/test/test_motion_search_batch.py b/frigate/test/test_motion_search_batch.py
new file mode 100644
index 0000000000..0e7fcdfd8a
--- /dev/null
+++ b/frigate/test/test_motion_search_batch.py
@@ -0,0 +1,58 @@
+"""Tests for motion search batch helpers (runs + timestamp mapping)."""
+
+import unittest
+from dataclasses import dataclass
+
+from frigate.jobs.motion_search_batch import (
+ build_segment_time_map,
+ coalesce_runs,
+ stream_time_to_absolute,
+)
+
+
+@dataclass
+class _Seg:
+ path: str
+ start_time: float
+ end_time: float
+
+
+def _run_seconds(run):
+ return float(run[-1].end_time) - float(run[0].start_time)
+
+
+class TestCoalesceRuns(unittest.TestCase):
+ def test_contiguous_segments_form_one_run(self):
+ segs = [_Seg("a", 0.0, 10.0), _Seg("b", 10.0, 20.0), _Seg("c", 20.0, 30.0)]
+ runs = coalesce_runs(segs, max_seconds=600.0, epsilon=0.5)
+ self.assertEqual(len(runs), 1)
+ self.assertEqual(len(runs[0]), 3)
+
+ def test_time_gap_splits_runs(self):
+ # b ends 20, c starts 25 -> 5s gap > epsilon -> two runs.
+ segs = [_Seg("a", 0.0, 10.0), _Seg("b", 10.0, 20.0), _Seg("c", 25.0, 35.0)]
+ runs = coalesce_runs(segs, max_seconds=600.0, epsilon=0.5)
+ self.assertEqual([len(r) for r in runs], [2, 1])
+
+ def test_max_duration_caps_a_run(self):
+ # Five contiguous 10s segments, cap 25s.
+ segs = [_Seg(str(i), i * 10.0, i * 10.0 + 10.0) for i in range(5)]
+ runs = coalesce_runs(segs, max_seconds=25.0, epsilon=0.5)
+ self.assertTrue(all(_run_seconds(r) <= 30.0 for r in runs))
+ self.assertEqual(sum(len(r) for r in runs), 5)
+
+ def test_empty(self):
+ self.assertEqual(coalesce_runs([], max_seconds=600.0, epsilon=0.5), [])
+
+
+class TestTimestampMapping(unittest.TestCase):
+ def test_gapfree_run_maps_to_start_plus_pts(self):
+ run = [_Seg("a", 1000.0, 1010.0), _Seg("b", 1010.0, 1020.0)]
+ time_map = build_segment_time_map(run)
+ self.assertAlmostEqual(stream_time_to_absolute(time_map, 3.0), 1003.0)
+ self.assertAlmostEqual(stream_time_to_absolute(time_map, 12.0), 1012.0)
+
+ def test_past_end_clamps(self):
+ run = [_Seg("a", 1000.0, 1010.0)]
+ time_map = build_segment_time_map(run)
+ self.assertAlmostEqual(stream_time_to_absolute(time_map, 9.9), 1009.9)
diff --git a/frigate/test/test_motion_search_decode.py b/frigate/test/test_motion_search_decode.py
new file mode 100644
index 0000000000..fda0837662
--- /dev/null
+++ b/frigate/test/test_motion_search_decode.py
@@ -0,0 +1,190 @@
+"""Tests for the motion search hardware-accelerated decode helpers."""
+
+import unittest
+from types import SimpleNamespace
+from unittest import mock
+
+from frigate.jobs.motion_search_decode import (
+ KEYFRAME_MAX_GAP_SECONDS,
+ build_vod_decode_command,
+ keyframe_sampling_eligible,
+ probe_video_dimensions,
+ probe_vod_keyframe_pts,
+ resolve_motion_decode_args,
+)
+
+
+def _fake_camera_config(
+ hwaccel_args, gpu=0, fps=5, width=1280, height=720, ffmpeg_path="ffmpeg"
+):
+ return SimpleNamespace(
+ ffmpeg=SimpleNamespace(
+ hwaccel_args=hwaccel_args, gpu=gpu, ffmpeg_path=ffmpeg_path
+ ),
+ detect=SimpleNamespace(fps=fps, width=width, height=height),
+ )
+
+
+class TestResolveMotionDecodeArgs(unittest.TestCase):
+ def test_vaapi_preset_is_accelerated(self):
+ args = resolve_motion_decode_args(_fake_camera_config("preset-vaapi"))
+ self.assertIn("-hwaccel", args)
+ self.assertIn("vaapi", args)
+
+ def test_non_nv12_preset_falls_back_to_software(self):
+ # rkmpp produces drm_prime surfaces that do not download to nv12, so it
+ # must resolve to software decode (empty args) rather than risk corrupt
+ # frames.
+ self.assertEqual(
+ resolve_motion_decode_args(_fake_camera_config("preset-rkmpp")), []
+ )
+
+ def test_custom_args_fall_back_to_software(self):
+ # Arbitrary custom hwaccel args (a list, not a preset) decode in software
+ # to preserve byte-identical results.
+ self.assertEqual(
+ resolve_motion_decode_args(_fake_camera_config(["-hwaccel", "vulkan"])),
+ [],
+ )
+
+ def test_nvidia_codec_preset_is_accelerated(self):
+ # Codec-specific nvidia presets resolve to the same cuda decode args as
+ # the bare preset, so eligibility is derived from -hwaccel_output_format
+ # rather than a hardcoded list that omitted these aliases.
+ args = resolve_motion_decode_args(_fake_camera_config("preset-nvidia-h264"))
+ self.assertIn("-hwaccel_output_format", args)
+ self.assertIn("cuda", args)
+
+ def test_software_only_preset_falls_back_to_software(self):
+ # A preset with no -hwaccel_output_format (decoder-based, no GPU surface)
+ # cannot use the nv12 download step, so it decodes in software.
+ self.assertEqual(
+ resolve_motion_decode_args(_fake_camera_config("preset-rpi-64-h264")), []
+ )
+
+
+class TestKeyframeEligibility(unittest.TestCase):
+ def test_regular_short_gop_is_eligible(self):
+ pts = [0.0, 0.5, 1.0, 1.5, 2.0] # 0.5s gaps
+ self.assertTrue(keyframe_sampling_eligible(pts))
+
+ def test_long_gop_is_ineligible(self):
+ pts = [0.0, 5.0, 10.0] # 5s gaps
+ self.assertFalse(keyframe_sampling_eligible(pts))
+
+ def test_irregular_gop_ineligible_when_a_gap_is_long(self):
+ pts = [0.0, 0.5, 1.0, 8.0] # one 7s gap
+ self.assertFalse(keyframe_sampling_eligible(pts))
+
+ def test_too_few_keyframes_ineligible(self):
+ self.assertFalse(keyframe_sampling_eligible([1.0]))
+ self.assertFalse(keyframe_sampling_eligible([]))
+
+ def test_default_max_gap_constant(self):
+ self.assertEqual(KEYFRAME_MAX_GAP_SECONDS, 2.0)
+
+
+class TestVodDecodeCommand(unittest.TestCase):
+ URL = "http://127.0.0.1:5000/vod/cam/start/1/end/2/index.m3u8"
+
+ def test_keyframe_command_shape(self):
+ cmd = build_vod_decode_command(
+ "ffmpeg",
+ self.URL,
+ decode_args=[],
+ crop=(100, 80, 10, 20),
+ scale=(50, 40),
+ gray=True,
+ skip_nonkey=True,
+ fps_rate=None,
+ )
+ joined = " ".join(cmd)
+ self.assertIn("-skip_frame nokey", joined)
+ self.assertIn("-protocol_whitelist pipe,file,http,tcp", joined)
+ self.assertIn(f"-i {self.URL}", joined)
+ self.assertIn("crop=100:80:10:20", joined)
+ self.assertIn("scale=50:40", joined)
+ self.assertIn("-pix_fmt gray", joined)
+ self.assertNotIn("fps=", joined)
+
+ def test_fps_command_uses_fps_filter_not_skip_frame(self):
+ cmd = build_vod_decode_command(
+ "ffmpeg",
+ self.URL,
+ decode_args=[],
+ crop=None,
+ scale=None,
+ gray=False,
+ skip_nonkey=False,
+ fps_rate=2.0,
+ )
+ joined = " ".join(cmd)
+ self.assertNotIn("skip_frame", joined)
+ self.assertIn("fps=2.0", joined)
+ self.assertIn("-pix_fmt bgr24", joined)
+
+ def test_hwaccel_inserts_hwdownload(self):
+ cmd = build_vod_decode_command(
+ "ffmpeg",
+ self.URL,
+ decode_args=["-hwaccel", "vaapi"],
+ crop=None,
+ scale=None,
+ gray=True,
+ skip_nonkey=True,
+ fps_rate=None,
+ )
+ joined = " ".join(cmd)
+ self.assertIn("hwdownload", joined)
+ self.assertIn("format=nv12", joined)
+
+
+class TestProbeVodKeyframePts(unittest.TestCase):
+ def test_parses_keyframe_packets(self):
+ sample = (
+ '{"packets":['
+ '{"pts_time":"0.000000","flags":"K__"},'
+ '{"pts_time":"1.000000","flags":"___"},'
+ '{"pts_time":"2.000000","flags":"K__"}]}'
+ )
+ completed = mock.Mock(stdout=sample, returncode=0)
+ with mock.patch(
+ "frigate.jobs.motion_search_decode.sp.run", return_value=completed
+ ):
+ pts = probe_vod_keyframe_pts("ffprobe", "http://x/index.m3u8")
+ self.assertEqual(pts, [0.0, 2.0])
+
+ def test_returns_empty_on_failure(self):
+ with mock.patch(
+ "frigate.jobs.motion_search_decode.sp.run",
+ side_effect=OSError("boom"),
+ ):
+ self.assertEqual(probe_vod_keyframe_pts("ffprobe", "http://x"), [])
+
+
+class TestProbeVideoDimensions(unittest.TestCase):
+ def test_parses_dimensions_and_fps(self):
+ sample = (
+ '{"streams":[{"width":1920,"height":1080,"avg_frame_rate":"30000/1001"}]}'
+ )
+ completed = mock.Mock(stdout=sample, returncode=0)
+ with mock.patch(
+ "frigate.jobs.motion_search_decode.sp.run", return_value=completed
+ ):
+ dims = probe_video_dimensions("ffprobe", "/tmp/a.mp4")
+ assert dims is not None
+ width, height, fps = dims
+ self.assertEqual((width, height), (1920, 1080))
+ self.assertAlmostEqual(fps, 29.97, places=2)
+
+ def test_returns_none_on_zero_dimensions(self):
+ sample = '{"streams":[{"width":0,"height":0,"avg_frame_rate":"0/0"}]}'
+ completed = mock.Mock(stdout=sample, returncode=0)
+ with mock.patch(
+ "frigate.jobs.motion_search_decode.sp.run", return_value=completed
+ ):
+ self.assertIsNone(probe_video_dimensions("ffprobe", "/tmp/a.mp4"))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/frigate/test/test_motion_search_spatial.py b/frigate/test/test_motion_search_spatial.py
new file mode 100644
index 0000000000..b8044df4b0
--- /dev/null
+++ b/frigate/test/test_motion_search_spatial.py
@@ -0,0 +1,87 @@
+"""Tests for motion search spatial (crop/scale/mask) helpers."""
+
+import unittest
+
+import numpy as np
+
+from frigate.jobs.motion_search import (
+ build_scaled_roi_mask,
+ compute_roi_crop_and_scale,
+ detect_motion_scaled,
+)
+
+
+class TestComputeRoiCropAndScale(unittest.TestCase):
+ def test_crop_box_in_record_pixels(self):
+ # ROI covering x [0.25, 0.75], y [0.5, 1.0] of a 1000x600 frame.
+ polygon = [[0.25, 0.5], [0.75, 0.5], [0.75, 1.0], [0.25, 1.0]]
+ crop, scaled = compute_roi_crop_and_scale(polygon, 1000, 600, scale_target=125)
+ cw, ch, cx, cy = crop
+ self.assertEqual((cx, cy), (250, 300))
+ self.assertEqual((cw, ch), (500, 300))
+ # longest side 500 -> factor 0.25 -> (125, 75), rounded down to even.
+ self.assertEqual(scaled, (124, 74))
+
+ def test_never_upscales(self):
+ polygon = [[0.0, 0.0], [0.1, 0.0], [0.1, 0.1], [0.0, 0.1]]
+ crop, scaled = compute_roi_crop_and_scale(polygon, 200, 200, scale_target=400)
+ cw, ch, _, _ = crop
+ # crop is 20x20; target 400 would upscale, so scaled == crop size.
+ self.assertEqual(scaled, (cw, ch))
+
+ def test_scaled_dims_are_at_least_one(self):
+ polygon = [[0.0, 0.0], [0.02, 0.0], [0.02, 0.02], [0.0, 0.02]]
+ crop, scaled = compute_roi_crop_and_scale(polygon, 50, 50, scale_target=1)
+ self.assertGreaterEqual(scaled[0], 1)
+ self.assertGreaterEqual(scaled[1], 1)
+
+ def test_all_dims_are_even_for_nv12(self):
+ # Odd-aligned ROI on an odd-ish frame must still yield even crop/scale so
+ # the nv12 hwdownload byte stream matches the expected frame size.
+ polygon = [[0.123, 0.321], [0.777, 0.321], [0.777, 0.901], [0.123, 0.901]]
+ crop, scaled = compute_roi_crop_and_scale(polygon, 1377, 911, scale_target=257)
+ for value in (*crop, *scaled):
+ self.assertEqual(value % 2, 0, f"{value} is not even")
+
+
+class TestBuildScaledRoiMask(unittest.TestCase):
+ def test_mask_matches_scaled_dims_and_has_coverage(self):
+ polygon = [[0.25, 0.5], [0.75, 0.5], [0.75, 1.0], [0.25, 1.0]]
+ crop, scaled = compute_roi_crop_and_scale(polygon, 1000, 600, scale_target=125)
+ mask = build_scaled_roi_mask(polygon, 1000, 600, crop, scaled)
+ self.assertEqual(mask.shape, (scaled[1], scaled[0]))
+ self.assertEqual(mask.dtype, np.uint8)
+ # A full rectangle ROI fills its whole crop -> mask is all 255.
+ self.assertGreater(np.count_nonzero(mask), 0)
+ self.assertEqual(np.count_nonzero(mask), mask.size)
+
+
+class TestDetectMotionScaled(unittest.TestCase):
+ def _ts(self, idx):
+ return float(idx)
+
+ def test_finds_change_between_frames(self):
+ mask = np.full((60, 80), 255, dtype=np.uint8)
+ f0 = np.zeros((60, 80), dtype=np.uint8)
+ f1 = np.zeros((60, 80), dtype=np.uint8)
+ f1[10:50, 20:60] = 255 # big bright block appears
+ frames = [(0, f0), (30, f1)]
+ results = detect_motion_scaled(
+ frames, mask, threshold=30, min_area=1.0, timestamp_fn=self._ts
+ )
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0].timestamp, 30.0)
+ self.assertGreater(results[0].change_percentage, 0.0)
+
+ def test_no_change_yields_nothing(self):
+ mask = np.full((60, 80), 255, dtype=np.uint8)
+ f0 = np.zeros((60, 80), dtype=np.uint8)
+ f1 = np.zeros((60, 80), dtype=np.uint8)
+ results = detect_motion_scaled(
+ [(0, f0), (30, f1)], mask, threshold=30, min_area=1.0, timestamp_fn=self._ts
+ )
+ self.assertEqual(results, [])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/frigate/test/test_mqtt_topic_registration.py b/frigate/test/test_mqtt_topic_registration.py
new file mode 100644
index 0000000000..309fafb983
--- /dev/null
+++ b/frigate/test/test_mqtt_topic_registration.py
@@ -0,0 +1,100 @@
+"""Tests for MQTT command topic callback registration."""
+
+import unittest
+from unittest.mock import MagicMock, patch
+
+from frigate.comms.mqtt import MqttClient
+
+
+def _make_camera_mock(
+ *,
+ enabled: bool = True,
+ notifications_enabled_in_config: bool = False,
+) -> MagicMock:
+ """Build a camera config mock with the fields _start() reads."""
+ camera = MagicMock()
+ camera.enabled = enabled
+ camera.notifications.enabled_in_config = notifications_enabled_in_config
+ camera.onvif.host = None
+ camera.motion.mask = {}
+ camera.objects.mask = {}
+ camera.zones = {}
+ return camera
+
+
+def _registered_topics(
+ cameras: dict[str, MagicMock],
+ *,
+ global_notifications_enabled_in_config: bool = False,
+) -> set[str]:
+ """Start an MqttClient against a mocked paho client and collect the
+ topics registered via message_callback_add."""
+ config = MagicMock()
+ config.cameras = cameras
+ config.notifications.enabled_in_config = global_notifications_enabled_in_config
+ config.mqtt.topic_prefix = "frigate"
+ config.mqtt.client_id = "frigate"
+ config.mqtt.user = None
+ config.mqtt.tls_ca_certs = None
+ config.mqtt.tls_insecure = None
+
+ with patch("frigate.comms.mqtt.mqtt.Client") as client_cls:
+ mqtt_client = MqttClient(config)
+ mqtt_client.subscribe(MagicMock())
+
+ paho_client = client_cls.return_value
+ return {call.args[0] for call in paho_client.message_callback_add.call_args_list}
+
+
+class TestMqttTopicRegistration(unittest.TestCase):
+ def test_camera_notification_topics_registered(self):
+ """Per-camera notification set/suspend must be registered so paho
+ routes them to the dispatcher (unregistered topics drop silently)."""
+ topics = _registered_topics(
+ {"front_door": _make_camera_mock(notifications_enabled_in_config=True)}
+ )
+
+ self.assertIn("frigate/front_door/notifications/set", topics)
+ self.assertIn("frigate/front_door/notifications/suspend", topics)
+
+ def test_global_set_registered_with_camera_only_notifications(self):
+ """The global topic must work when notifications are enabled only at
+ the camera level, matching the WebPushClient gating in app.py."""
+ topics = _registered_topics(
+ {"front_door": _make_camera_mock(notifications_enabled_in_config=True)},
+ global_notifications_enabled_in_config=False,
+ )
+
+ self.assertIn("frigate/notifications/set", topics)
+
+ def test_global_set_registered_with_global_notifications(self):
+ topics = _registered_topics(
+ {"front_door": _make_camera_mock()},
+ global_notifications_enabled_in_config=True,
+ )
+
+ self.assertIn("frigate/notifications/set", topics)
+
+ def test_global_set_not_registered_when_notifications_unconfigured(self):
+ topics = _registered_topics(
+ {"front_door": _make_camera_mock()},
+ global_notifications_enabled_in_config=False,
+ )
+
+ self.assertNotIn("frigate/notifications/set", topics)
+
+ def test_disabled_camera_does_not_enable_global_set(self):
+ topics = _registered_topics(
+ {
+ "front_door": _make_camera_mock(
+ enabled=False, notifications_enabled_in_config=True
+ )
+ },
+ global_notifications_enabled_in_config=False,
+ )
+
+ self.assertNotIn("frigate/notifications/set", topics)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/frigate/test/test_norfair_distance.py b/frigate/test/test_norfair_distance.py
new file mode 100644
index 0000000000..a79b91c3cf
--- /dev/null
+++ b/frigate/test/test_norfair_distance.py
@@ -0,0 +1,91 @@
+import math
+import unittest
+
+import numpy as np
+from norfair.camera_motion import (
+ HomographyTransformation,
+ TranslationTransformation,
+)
+
+from frigate.ptz.autotrack import transform_is_finite
+from frigate.track.norfair_tracker import distance
+
+
+class TestNorfairDistance(unittest.TestCase):
+ """Regression tests for the tracker distance guard.
+
+ norfair raises a hard ValueError on any nan distance, which kills the camera
+ process. During autotracking, an ill-conditioned homography can hand the
+ tracker a non-finite or degenerate estimate box, so distance() must never
+ return nan for any input.
+ """
+
+ def setUp(self) -> None:
+ # boxes are [[x1, y1], [x2, y2]]
+ self.detection = np.array([[805.0, 402.0], [864.0, 521.0]])
+ self.estimate = np.array([[800.0, 400.0], [860.0, 520.0]])
+
+ def test_finite_boxes_give_finite_distance(self) -> None:
+ d = distance(self.detection, self.estimate)
+ self.assertTrue(math.isfinite(d))
+
+ def test_inf_estimate_corner_does_not_return_nan(self) -> None:
+ estimate = np.array([[np.inf, 400.0], [860.0, 520.0]])
+ d = distance(self.detection, estimate)
+ self.assertFalse(math.isnan(d))
+ self.assertEqual(d, float("inf"))
+
+ def test_nan_estimate_corner_does_not_return_nan(self) -> None:
+ # the actual autotracking crash: a positive-only guard would miss this
+ # because nan <= 0 is False
+ estimate = np.array([[np.nan, 400.0], [860.0, 520.0]])
+ d = distance(self.detection, estimate)
+ self.assertFalse(math.isnan(d))
+ self.assertEqual(d, float("inf"))
+
+ def test_zero_area_estimate_does_not_return_nan(self) -> None:
+ estimate = np.array([[900.0, 500.0], [900.0, 500.0]])
+ d = distance(self.detection, estimate)
+ self.assertFalse(math.isnan(d))
+ self.assertEqual(d, float("inf"))
+
+ def test_zero_area_detection_does_not_return_nan(self) -> None:
+ detection = np.array([[805.0, 402.0], [805.0, 521.0]])
+ d = distance(detection, self.estimate)
+ self.assertFalse(math.isnan(d))
+ self.assertEqual(d, float("inf"))
+
+ def test_inverted_estimate_corners_do_not_return_nan(self) -> None:
+ # Kalman estimates can occasionally cross corners (x2 < x1)
+ estimate = np.array([[860.0, 520.0], [800.0, 400.0]])
+ d = distance(self.detection, estimate)
+ self.assertFalse(math.isnan(d))
+ self.assertEqual(d, float("inf"))
+
+
+class TestTransformIsFinite(unittest.TestCase):
+ def test_finite_homography_is_finite(self) -> None:
+ matrix = np.array([[1.0, 0.0, 5.0], [0.0, 1.0, 3.0], [0.0, 0.0, 1.0]])
+ self.assertTrue(transform_is_finite(HomographyTransformation(matrix)))
+
+ def test_finite_translation_is_finite(self) -> None:
+ self.assertTrue(
+ transform_is_finite(TranslationTransformation(np.array([12.0, -4.0])))
+ )
+
+ def test_non_finite_homography_is_not_finite(self) -> None:
+ transform = HomographyTransformation(np.eye(3))
+ # simulate accumulation overflowing to a non-finite matrix
+ transform.homography_matrix = np.array(
+ [[1.0, 0.0, np.inf], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
+ )
+ self.assertFalse(transform_is_finite(transform))
+
+ def test_nan_translation_is_not_finite(self) -> None:
+ self.assertFalse(
+ transform_is_finite(TranslationTransformation(np.array([np.nan, 0.0])))
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/frigate/test/test_obects.py b/frigate/test/test_obects.py
index 8fe831980e..ee0162ec01 100644
--- a/frigate/test/test_obects.py
+++ b/frigate/test/test_obects.py
@@ -1,6 +1,22 @@
+import random
import unittest
+import numpy as np
+
from frigate.track.tracked_object import TrackedObjectAttribute
+from frigate.util.object import average_boxes
+
+
+class TestBoxStatistics(unittest.TestCase):
+ def test_average_boxes_matches_numpy(self) -> None:
+ rng = random.Random(0)
+ for _ in range(5000):
+ boxes = [
+ [rng.randint(0, 4000) for _ in range(4)]
+ for _ in range(rng.randint(1, 10))
+ ]
+ expected = [float(np.mean([b[i] for b in boxes])) for i in range(4)]
+ self.assertEqual(average_boxes(boxes), expected)
class TestAttribute(unittest.TestCase):
diff --git a/frigate/test/test_onvif_probe.py b/frigate/test/test_onvif_probe.py
new file mode 100644
index 0000000000..aee7cc17a5
--- /dev/null
+++ b/frigate/test/test_onvif_probe.py
@@ -0,0 +1,124 @@
+import unittest
+from unittest.mock import AsyncMock, MagicMock, patch
+
+from zeep.exceptions import Fault, TransportError
+from zeep.transports import AsyncTransport
+
+from frigate.api.camera import _build_digest_transport, _connect_onvif_camera
+
+
+def _make_camera(update_side_effect=None):
+ """Build a mock ONVIFCamera whose update_xaddrs can raise or succeed."""
+ camera = MagicMock()
+ camera.update_xaddrs = AsyncMock(side_effect=update_side_effect)
+ return camera
+
+
+class TestConnectOnvifCamera(unittest.IsolatedAsyncioTestCase):
+ async def test_password_digest_succeeds_first(self):
+ # Cameras that accept PasswordDigest authenticate on the first attempt
+ # and should never trigger the PasswordText fallback.
+ camera = _make_camera()
+
+ with patch("frigate.api.camera.ONVIFCamera", return_value=camera) as mock_cls:
+ result = await _connect_onvif_camera(
+ "cam.local", 80, "user", "pass", None, "basic"
+ )
+
+ self.assertIs(result, camera)
+ mock_cls.assert_called_once()
+ self.assertTrue(mock_cls.call_args.kwargs["encrypt"])
+
+ async def test_falls_back_to_password_text(self):
+ # A PasswordDigest rejection should retry once with PasswordText.
+ camera_digest = _make_camera(update_side_effect=Fault("token rejected"))
+ camera_text = _make_camera()
+
+ with patch(
+ "frigate.api.camera.ONVIFCamera",
+ side_effect=[camera_digest, camera_text],
+ ) as mock_cls:
+ result = await _connect_onvif_camera(
+ "cam.local", 80, "user", "pass", None, "basic"
+ )
+
+ self.assertIs(result, camera_text)
+ self.assertEqual(mock_cls.call_count, 2)
+ self.assertTrue(mock_cls.call_args_list[0].kwargs["encrypt"])
+ self.assertFalse(mock_cls.call_args_list[1].kwargs["encrypt"])
+
+ async def test_both_encodings_fail_raises_first_fault(self):
+ # When both encodings fault, the original (PasswordDigest) fault is
+ # surfaced so the caller's existing Fault handler reports it.
+ first_fault = Fault("digest rejected")
+ camera_digest = _make_camera(update_side_effect=first_fault)
+ camera_text = _make_camera(update_side_effect=Fault("text rejected"))
+
+ with patch(
+ "frigate.api.camera.ONVIFCamera",
+ side_effect=[camera_digest, camera_text],
+ ) as mock_cls:
+ with self.assertRaises(Fault) as ctx:
+ await _connect_onvif_camera(
+ "cam.local", 80, "user", "pass", None, "basic"
+ )
+
+ self.assertIs(ctx.exception, first_fault)
+ self.assertEqual(mock_cls.call_count, 2)
+
+ async def test_transport_error_is_not_retried(self):
+ # Connection-level errors (timeout, refused, unreachable) should
+ # propagate immediately without doubling latency on a second encoding.
+ camera = _make_camera(update_side_effect=TransportError("unreachable"))
+
+ with patch("frigate.api.camera.ONVIFCamera", side_effect=[camera]) as mock_cls:
+ with self.assertRaises(TransportError):
+ await _connect_onvif_camera(
+ "cam.local", 80, "user", "pass", None, "basic"
+ )
+
+ mock_cls.assert_called_once()
+
+ async def test_digest_auth_replaces_service_transports(self):
+ # auth_type "digest" wires an HTTP digest transport onto each service,
+ # independently of the WS-Security encoding.
+ camera = _make_camera()
+
+ with (
+ patch("frigate.api.camera.ONVIFCamera", return_value=camera),
+ patch(
+ "frigate.api.camera._build_digest_transport",
+ return_value="TRANSPORT",
+ ) as mock_transport,
+ ):
+ result = await _connect_onvif_camera(
+ "cam.local", 80, "user", "pass", None, "digest"
+ )
+
+ self.assertIs(result, camera)
+ mock_transport.assert_called_once_with("user", "pass")
+ self.assertEqual(camera.devicemgmt.zeep_client.transport, "TRANSPORT")
+ self.assertEqual(camera.media.zeep_client.transport, "TRANSPORT")
+ self.assertEqual(camera.ptz.zeep_client.transport, "TRANSPORT")
+
+ async def test_basic_auth_does_not_replace_transports(self):
+ # Without digest auth, no transport override is built.
+ camera = _make_camera()
+
+ with (
+ patch("frigate.api.camera.ONVIFCamera", return_value=camera),
+ patch("frigate.api.camera._build_digest_transport") as mock_transport,
+ ):
+ await _connect_onvif_camera("cam.local", 80, "user", "pass", None, "basic")
+
+ mock_transport.assert_not_called()
+
+
+class TestBuildDigestTransport(unittest.TestCase):
+ def test_returns_async_transport(self):
+ transport = _build_digest_transport("user", "pass")
+ self.assertIsInstance(transport, AsyncTransport)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/frigate/test/test_output_ws_auth.py b/frigate/test/test_output_ws_auth.py
new file mode 100644
index 0000000000..ea4834ef13
--- /dev/null
+++ b/frigate/test/test_output_ws_auth.py
@@ -0,0 +1,57 @@
+"""Tests for JSMPEG websocket authorization."""
+
+import unittest
+from types import SimpleNamespace
+
+from frigate.config import FrigateConfig
+from frigate.output.ws_auth import ws_has_camera_access
+
+
+class TestWsHasCameraAccess(unittest.TestCase):
+ def setUp(self):
+ self.config = FrigateConfig(
+ mqtt={"host": "mqtt"},
+ auth={"roles": {"limited_user": ["front_door"]}},
+ cameras={
+ "front_door": {
+ "ffmpeg": {
+ "inputs": [
+ {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
+ ]
+ },
+ "detect": {"height": 1080, "width": 1920, "fps": 5},
+ },
+ "back_door": {
+ "ffmpeg": {
+ "inputs": [
+ {"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]}
+ ]
+ },
+ "detect": {"height": 1080, "width": 1920, "fps": 5},
+ },
+ },
+ )
+
+ def _make_ws(self, role: str):
+ return SimpleNamespace(environ={"HTTP_REMOTE_ROLE": role})
+
+ def test_restricted_role_only_gets_allowed_camera(self):
+ ws = self._make_ws("limited_user")
+ self.assertTrue(ws_has_camera_access(ws, "front_door", self.config))
+ self.assertFalse(ws_has_camera_access(ws, "back_door", self.config))
+
+ def test_unrestricted_role_can_access_any_camera(self):
+ ws = self._make_ws("viewer")
+ self.assertTrue(ws_has_camera_access(ws, "front_door", self.config))
+ self.assertTrue(ws_has_camera_access(ws, "back_door", self.config))
+
+ def test_birdseye_requires_unrestricted_access(self):
+ self.assertTrue(
+ ws_has_camera_access(self._make_ws("admin"), "birdseye", self.config)
+ )
+ self.assertTrue(
+ ws_has_camera_access(self._make_ws("viewer"), "birdseye", self.config)
+ )
+ self.assertFalse(
+ ws_has_camera_access(self._make_ws("limited_user"), "birdseye", self.config)
+ )
diff --git a/frigate/test/test_profiles.py b/frigate/test/test_profiles.py
index b73fa74a08..59dc077466 100644
--- a/frigate/test/test_profiles.py
+++ b/frigate/test/test_profiles.py
@@ -1,5 +1,6 @@
"""Tests for the profiles system."""
+import copy
import json
import os
import unittest
@@ -178,6 +179,141 @@ class TestCameraProfileConfig(unittest.TestCase):
with self.assertRaises(ValidationError):
FrigateConfig(**config_data)
+ def test_profile_zone_without_base_rejected(self):
+ """Profile defining a zone not present on the base camera is rejected."""
+ from pydantic import ValidationError
+
+ config_data = {
+ "mqtt": {"host": "mqtt"},
+ "profiles": {
+ "armed": {"friendly_name": "Armed"},
+ },
+ "cameras": {
+ "front": {
+ "ffmpeg": {
+ "inputs": [
+ {
+ "path": "rtsp://10.0.0.1:554/video",
+ "roles": ["detect"],
+ }
+ ]
+ },
+ "detect": {"height": 1080, "width": 1920, "fps": 5},
+ "zones": {
+ "front_yard": {"coordinates": "0,0,100,0,100,100,0,100"},
+ },
+ "profiles": {
+ "armed": {
+ "zones": {
+ "phantom": {
+ "coordinates": "0,0,50,0,50,50,0,50",
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+ with self.assertRaises(ValidationError) as ctx:
+ FrigateConfig(**config_data)
+ self.assertIn("phantom", str(ctx.exception))
+
+ def test_profile_motion_mask_without_base_rejected(self):
+ """Profile defining a motion mask not present on the base camera is rejected."""
+ from pydantic import ValidationError
+
+ config_data = {
+ "mqtt": {"host": "mqtt"},
+ "profiles": {
+ "armed": {"friendly_name": "Armed"},
+ },
+ "cameras": {
+ "front": {
+ "ffmpeg": {
+ "inputs": [
+ {
+ "path": "rtsp://10.0.0.1:554/video",
+ "roles": ["detect"],
+ }
+ ]
+ },
+ "detect": {"height": 1080, "width": 1920, "fps": 5},
+ "motion": {
+ "mask": {
+ "base_mask": {
+ "coordinates": "0,0,100,0,100,100,0,100",
+ },
+ },
+ },
+ "profiles": {
+ "armed": {
+ "motion": {
+ "mask": {
+ "phantom_mask": {
+ "coordinates": "0,0,50,0,50,50,0,50",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+ with self.assertRaises(ValidationError) as ctx:
+ FrigateConfig(**config_data)
+ self.assertIn("phantom_mask", str(ctx.exception))
+
+ def test_profile_overrides_matching_base_accepted(self):
+ """Profile overrides that reference existing base zones/masks parse cleanly."""
+ config_data = {
+ "mqtt": {"host": "mqtt"},
+ "profiles": {
+ "armed": {"friendly_name": "Armed"},
+ },
+ "cameras": {
+ "front": {
+ "ffmpeg": {
+ "inputs": [
+ {
+ "path": "rtsp://10.0.0.1:554/video",
+ "roles": ["detect"],
+ }
+ ]
+ },
+ "detect": {"height": 1080, "width": 1920, "fps": 5},
+ "zones": {
+ "front_yard": {"coordinates": "0,0,100,0,100,100,0,100"},
+ },
+ "motion": {
+ "mask": {
+ "tree": {
+ "coordinates": "0,0,100,0,100,100,0,100",
+ },
+ },
+ },
+ "profiles": {
+ "armed": {
+ "zones": {
+ "front_yard": {
+ "coordinates": "0,0,50,0,50,50,0,50",
+ "inertia": 5,
+ },
+ },
+ "motion": {
+ "mask": {
+ "tree": {
+ "coordinates": "0,0,75,0,75,75,0,75",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+ config = FrigateConfig(**config_data)
+ assert "armed" in config.cameras["front"].profiles
+
class TestProfileInConfig(unittest.TestCase):
"""Test that profiles parse correctly in FrigateConfig."""
@@ -592,6 +728,85 @@ class TestProfileManager(unittest.TestCase):
# Should not raise
json.dumps(api_base)
+ @patch.object(ProfileManager, "_persist_active_profile")
+ def test_activate_profile_clears_dispatcher_runtime_state(self, mock_persist):
+ """User-initiated activation drops runtime overrides (steady-state rule)."""
+ dispatcher = MagicMock()
+ manager = ProfileManager(self.config, self.mock_updater, dispatcher)
+ manager.activate_profile("armed")
+ dispatcher.clear_runtime_state.assert_called_once_with()
+
+ @patch.object(ProfileManager, "_persist_active_profile")
+ def test_deactivate_profile_clears_dispatcher_runtime_state(self, mock_persist):
+ """Deactivating a profile also drops runtime overrides."""
+ dispatcher = MagicMock()
+ manager = ProfileManager(self.config, self.mock_updater, dispatcher)
+ manager.activate_profile("armed")
+ dispatcher.clear_runtime_state.reset_mock()
+
+ manager.activate_profile(None)
+ dispatcher.clear_runtime_state.assert_called_once_with()
+
+ @patch.object(ProfileManager, "_persist_active_profile")
+ def test_profile_change_republishes_switch_states(self, mock_persist):
+ """Profile changes republish MQTT switch states so HA stays in sync.
+
+ Regression: activating/deactivating a profile updated the in-memory
+ config (and Frigate's behavior) but left the retained MQTT state
+ topics stale, so external integrations like Home Assistant kept
+ showing the pre-profile toggle position.
+ """
+ config_data = copy.deepcopy(self.config_data)
+ config_data["cameras"]["front"]["profiles"]["disarmed"]["review"] = {
+ "alerts": {"enabled": False},
+ }
+ config = FrigateConfig(**config_data)
+ dispatcher = MagicMock()
+ manager = ProfileManager(config, self.mock_updater, dispatcher)
+
+ # Activating disarmed turns alerts off -> MQTT state must follow
+ manager.activate_profile("disarmed")
+ dispatcher.publish.assert_any_call(
+ "front/review_alerts/state", "OFF", retain=True
+ )
+
+ # Deactivating restores the base (alerts on) -> MQTT state must follow
+ dispatcher.publish.reset_mock()
+ manager.activate_profile(None)
+ dispatcher.publish.assert_any_call(
+ "front/review_alerts/state", "ON", retain=True
+ )
+
+ @patch.object(ProfileManager, "_persist_active_profile")
+ def test_startup_replay_does_not_clear_runtime_state(self, mock_persist):
+ """Startup callers pass clear_runtime_overrides=False to preserve state."""
+ dispatcher = MagicMock()
+ manager = ProfileManager(self.config, self.mock_updater, dispatcher)
+ manager.activate_profile("armed", clear_runtime_overrides=False)
+ dispatcher.clear_runtime_state.assert_not_called()
+
+ @patch.object(ProfileManager, "_persist_active_profile")
+ def test_update_config_clears_when_active_profile_reapplies(self, mock_persist):
+ """After /api/config/set, an active-profile re-application drops state."""
+ dispatcher = MagicMock()
+ manager = ProfileManager(self.config, self.mock_updater, dispatcher)
+ manager.activate_profile("armed")
+ dispatcher.clear_runtime_state.reset_mock()
+
+ new_config = FrigateConfig(**self.config_data)
+ manager.update_config(new_config)
+ dispatcher.clear_runtime_state.assert_called_once_with()
+
+ @patch.object(ProfileManager, "_persist_active_profile")
+ def test_update_config_does_not_clear_when_no_active_profile(self, mock_persist):
+ """Plain /api/config/set without a profile doesn't trigger the broad clear."""
+ dispatcher = MagicMock()
+ manager = ProfileManager(self.config, self.mock_updater, dispatcher)
+ # No activate_profile call — config.active_profile is None
+ new_config = FrigateConfig(**self.config_data)
+ manager.update_config(new_config)
+ dispatcher.clear_runtime_state.assert_not_called()
+
class TestProfilePersistence(unittest.TestCase):
"""Test profile persistence to disk."""
diff --git a/frigate/test/test_runtime_state.py b/frigate/test/test_runtime_state.py
new file mode 100644
index 0000000000..6143184030
--- /dev/null
+++ b/frigate/test/test_runtime_state.py
@@ -0,0 +1,136 @@
+"""Tests for RuntimeStatePersistence."""
+
+import json
+import os
+import tempfile
+import unittest
+from unittest.mock import patch
+
+from frigate.comms.runtime_state import RuntimeStatePersistence
+
+
+class TestRuntimeStatePersistence(unittest.TestCase):
+ """Unit tests for the JSON-backed runtime state store."""
+
+ def setUp(self) -> None:
+ self.tmp_dir = tempfile.mkdtemp()
+ self.config_path = os.path.join(self.tmp_dir, "config.yml")
+ # Touch a placeholder config.yml so find_config_file returns a real path
+ with open(self.config_path, "w") as f:
+ f.write("")
+ self._patcher = patch(
+ "frigate.comms.runtime_state.find_config_file",
+ return_value=self.config_path,
+ )
+ self._patcher.start()
+ self.store = RuntimeStatePersistence()
+
+ def tearDown(self) -> None:
+ self._patcher.stop()
+ for name in os.listdir(self.tmp_dir):
+ os.remove(os.path.join(self.tmp_dir, name))
+ os.rmdir(self.tmp_dir)
+
+ def test_load_returns_empty_when_file_missing(self) -> None:
+ self.assertEqual(self.store.load(), {})
+
+ def test_set_then_load_round_trip(self) -> None:
+ self.store.set("front_door", "detect", False)
+ self.store.set("front_door", "recordings", True)
+ self.store.set("back_yard", "audio", False)
+
+ result = self.store.load()
+ self.assertEqual(
+ result,
+ {
+ "front_door": {"detect": False, "recordings": True},
+ "back_yard": {"audio": False},
+ },
+ )
+
+ def test_set_with_untracked_topic_is_noop(self) -> None:
+ self.store.set("front_door", "ptz_autotracker", True)
+ self.assertEqual(self.store.load(), {})
+ # File should not even be created if no tracked entries were written
+ runtime_path = os.path.join(self.tmp_dir, ".runtime_state.json")
+ self.assertFalse(os.path.exists(runtime_path))
+
+ def test_set_overwrites_previous_value(self) -> None:
+ self.store.set("front_door", "detect", True)
+ self.store.set("front_door", "detect", False)
+ self.assertEqual(self.store.load(), {"front_door": {"detect": False}})
+
+ def test_load_returns_empty_when_file_corrupt(self) -> None:
+ runtime_path = os.path.join(self.tmp_dir, ".runtime_state.json")
+ with open(runtime_path, "w") as f:
+ f.write("{not valid json")
+ self.assertEqual(self.store.load(), {})
+
+ def test_load_handles_unexpected_top_level_shape(self) -> None:
+ runtime_path = os.path.join(self.tmp_dir, ".runtime_state.json")
+ with open(runtime_path, "w") as f:
+ json.dump(["unexpected", "list"], f)
+ self.assertEqual(self.store.load(), {})
+
+ def test_clear_for_yaml_keys_removes_matching_entries(self) -> None:
+ self.store.set("front_door", "detect", False)
+ self.store.set("front_door", "recordings", False)
+ self.store.set("back_yard", "audio", False)
+
+ self.store.clear_for_yaml_keys(
+ [
+ "cameras.front_door.detect.enabled",
+ "cameras.back_yard.audio.enabled",
+ ]
+ )
+
+ self.assertEqual(
+ self.store.load(),
+ {"front_door": {"recordings": False}},
+ )
+
+ def test_clear_for_yaml_keys_collapses_empty_camera_dict(self) -> None:
+ self.store.set("front_door", "detect", False)
+ self.store.clear_for_yaml_keys(["cameras.front_door.detect.enabled"])
+ self.assertEqual(self.store.load(), {})
+
+ def test_clear_for_yaml_keys_ignores_unrelated_keys(self) -> None:
+ self.store.set("front_door", "detect", False)
+ self.store.clear_for_yaml_keys(
+ [
+ "ui.theme",
+ "go2rtc.streams.x",
+ "cameras.front_door.ffmpeg.inputs",
+ "not_cameras.front_door.detect.enabled",
+ ]
+ )
+ self.assertEqual(self.store.load(), {"front_door": {"detect": False}})
+
+ def test_clear_for_yaml_keys_handles_empty_iterable(self) -> None:
+ self.store.set("front_door", "detect", False)
+ self.store.clear_for_yaml_keys([])
+ self.assertEqual(self.store.load(), {"front_door": {"detect": False}})
+
+ def test_camera_level_enabled_uses_top_level_yaml_key(self) -> None:
+ """`enabled` topic maps to the camera-level `cameras..enabled` key."""
+ self.store.set("front_door", "enabled", False)
+ self.store.clear_for_yaml_keys(["cameras.front_door.enabled"])
+ self.assertEqual(self.store.load(), {})
+
+ def test_clear_all_wipes_every_entry(self) -> None:
+ self.store.set("front_door", "detect", False)
+ self.store.set("front_door", "recordings", True)
+ self.store.set("back_yard", "audio", False)
+
+ self.store.clear_all()
+
+ self.assertEqual(self.store.load(), {})
+
+ def test_clear_all_is_safe_when_file_missing(self) -> None:
+ # No prior set() calls — file does not exist
+ self.store.clear_all()
+ self.assertEqual(self.store.load(), {})
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/frigate/test/test_shared_memory_frame_manager.py b/frigate/test/test_shared_memory_frame_manager.py
new file mode 100644
index 0000000000..63c96f732d
--- /dev/null
+++ b/frigate/test/test_shared_memory_frame_manager.py
@@ -0,0 +1,156 @@
+"""Tests for SharedMemoryFrameManager cache invalidation.
+
+Covers the case where a SHM segment is unlinked and recreated at a
+different size across a camera add/remove cycle while a long-lived
+in-process cache (e.g. TrackedObjectProcessor) still holds a ref to
+the old, smaller segment.
+"""
+
+import unittest
+from types import SimpleNamespace
+from unittest.mock import patch
+
+import numpy as np
+
+from frigate.util.image import SharedMemoryFrameManager
+
+
+def _fake_shm(size: int) -> SimpleNamespace:
+ """A minimal stand-in for UntrackedSharedMemory with .size and .buf."""
+ return SimpleNamespace(size=size, buf=bytearray(size), close=lambda: None)
+
+
+class TestSharedMemoryFrameManagerGet(unittest.TestCase):
+ def test_get_reopens_when_cached_segment_is_smaller_than_shape(self) -> None:
+ """A cached ref to an older smaller segment must be dropped and the
+ current (correctly sized) segment reopened. Without this, np.ndarray
+ would raise "buffer is too small for requested array" when the
+ in-memory cache pointed at an old SHM after a same-name resize."""
+ manager = SharedMemoryFrameManager()
+
+ small = _fake_shm(size=100)
+ current = _fake_shm(size=2_500)
+ manager.shm_store["cam_frame0"] = small
+
+ with patch("frigate.util.image.UntrackedSharedMemory", return_value=current):
+ arr = manager.get("cam_frame0", (50, 50))
+
+ self.assertIsNotNone(arr)
+ self.assertEqual(arr.shape, (50, 50))
+ self.assertIs(manager.shm_store["cam_frame0"], current)
+
+ def test_get_reopens_when_cached_segment_is_larger_than_shape(self) -> None:
+ """Symmetric to the smaller-cache case: when detect resolution drops,
+ the SHM is unlinked and recreated at a smaller size. A cached ref to
+ the old, larger segment still satisfies any size check but points at
+ an orphaned inode whose stale bytes get reinterpreted at the new
+ shape — producing miscolored, distorted YUV frames downstream. Drop
+ the cache so we reopen by name and bind to the current segment."""
+ manager = SharedMemoryFrameManager()
+
+ old_large = _fake_shm(size=10_000)
+ current = _fake_shm(size=2_500)
+ manager.shm_store["cam_frame0"] = old_large
+
+ with patch("frigate.util.image.UntrackedSharedMemory", return_value=current):
+ arr = manager.get("cam_frame0", (50, 50))
+
+ self.assertIsNotNone(arr)
+ self.assertEqual(arr.shape, (50, 50))
+ self.assertIs(manager.shm_store["cam_frame0"], current)
+
+ def test_get_keeps_cached_segment_when_size_matches(self) -> None:
+ """Don't pay the reopen cost when the cached ref is the right size."""
+ manager = SharedMemoryFrameManager()
+
+ cached = _fake_shm(size=2_500)
+ manager.shm_store["cam_frame0"] = cached
+
+ with patch("frigate.util.image.UntrackedSharedMemory") as untracked_shm_cls:
+ arr = manager.get("cam_frame0", (50, 50))
+ untracked_shm_cls.assert_not_called()
+
+ self.assertIsNotNone(arr)
+ self.assertIs(manager.shm_store["cam_frame0"], cached)
+
+ def test_get_opens_fresh_when_no_cache_entry(self) -> None:
+ manager = SharedMemoryFrameManager()
+ fresh = _fake_shm(size=2_500)
+
+ with patch("frigate.util.image.UntrackedSharedMemory", return_value=fresh):
+ arr = manager.get("cam_frame0", (50, 50))
+
+ self.assertIsNotNone(arr)
+ self.assertIs(manager.shm_store["cam_frame0"], fresh)
+
+ def test_get_returns_none_when_segment_missing(self) -> None:
+ manager = SharedMemoryFrameManager()
+
+ with patch(
+ "frigate.util.image.UntrackedSharedMemory",
+ side_effect=FileNotFoundError,
+ ):
+ arr = manager.get("cam_frame0", (50, 50))
+
+ self.assertIsNone(arr)
+
+ def test_get_returns_none_when_reopened_segment_is_still_too_small(self) -> None:
+ """Race during a same-name SHM recreate: cache is stale, we reopen
+ by name, but the maintainer hasn't allocated the new segment yet —
+ the reopened ref is also too small. Skip the frame (return None)
+ rather than crash on np.ndarray."""
+ manager = SharedMemoryFrameManager()
+
+ small_cached = _fake_shm(size=100)
+ still_small_after_reopen = _fake_shm(size=100)
+ manager.shm_store["cam_frame0"] = small_cached
+
+ with patch(
+ "frigate.util.image.UntrackedSharedMemory",
+ return_value=still_small_after_reopen,
+ ):
+ arr = manager.get("cam_frame0", (50, 50))
+
+ self.assertIsNone(arr)
+ # Don't cache the too-small reopened ref — next call will re-open
+ # once the maintainer has finished recreating the segment.
+ self.assertNotIn("cam_frame0", manager.shm_store)
+
+ def test_get_handles_n_dimensional_shape(self) -> None:
+ """np.prod must be used (not raw multiplication) for tuple shapes."""
+ manager = SharedMemoryFrameManager()
+ # YUV-shaped frame: (height * 3/2, width) for 1920x1080 = 3,110,400
+ big_enough = _fake_shm(size=3_110_400)
+ manager.shm_store["cam_frame0"] = big_enough
+
+ with patch("frigate.util.image.UntrackedSharedMemory") as untracked_shm_cls:
+ arr = manager.get("cam_frame0", (1620, 1920))
+ untracked_shm_cls.assert_not_called()
+
+ self.assertIsNotNone(arr)
+ self.assertEqual(arr.shape, (1620, 1920))
+
+
+class TestSharedMemoryFrameManagerGetRecreatesLargerSegment(unittest.TestCase):
+ """End-to-end-style: simulates the full unlink-and-recreate cycle."""
+
+ def test_segment_grows_then_get_succeeds(self) -> None:
+ manager = SharedMemoryFrameManager()
+
+ # Phase 1: existing camera at 320x240 YUV — 320 * 240 * 1.5 = 115_200
+ small = _fake_shm(size=115_200)
+ manager.shm_store["cam_frame0"] = small
+ arr_small = np.ndarray((360, 320), dtype=np.uint8, buffer=small.buf)
+ self.assertEqual(arr_small.shape, (360, 320))
+
+ # Phase 2: restart at 1920x1080 — new SHM segment, larger size.
+ large = _fake_shm(size=3_110_400)
+ with patch("frigate.util.image.UntrackedSharedMemory", return_value=large):
+ arr_large = manager.get("cam_frame0", (1620, 1920))
+
+ self.assertIsNotNone(arr_large)
+ self.assertEqual(arr_large.shape, (1620, 1920))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/frigate/test/test_sqlitevecq_regexp.py b/frigate/test/test_sqlitevecq_regexp.py
new file mode 100644
index 0000000000..71f4fcb6b4
--- /dev/null
+++ b/frigate/test/test_sqlitevecq_regexp.py
@@ -0,0 +1,54 @@
+"""Tests for the REGEXP function registered on the main Frigate database.
+
+Regression coverage for GHSA-q8jx-q884-jcq9: an attacker-controlled
+catastrophic (ReDoS) pattern reaching the REGEXP sink must not be able to
+stall the serialized database worker thread.
+"""
+
+import sqlite3
+import time
+import unittest
+
+from frigate.db.sqlitevecq import REGEXP_TIMEOUT_SECONDS, SqliteVecQueueDatabase
+
+
+class TestRegexpFunction(unittest.TestCase):
+ def setUp(self) -> None:
+ # autostart=False keeps the queue worker thread from spinning up; we
+ # only need the REGEXP registration, exercised on our own connection.
+ self.db = SqliteVecQueueDatabase(":memory:", autostart=False)
+ self.conn = sqlite3.connect(":memory:")
+ self.db._register_regexp(self.conn)
+
+ def tearDown(self) -> None:
+ self.conn.close()
+
+ def _regexp(self, value: str | None, pattern: str) -> int | None:
+ # SQLite maps "value REGEXP pattern" to regexp(pattern, value).
+ return self.conn.execute("SELECT ? REGEXP ?", (value, pattern)).fetchone()[0]
+
+ def test_normal_patterns_still_match(self) -> None:
+ self.assertTrue(self._regexp("ABC123", "^ABC"))
+ self.assertTrue(self._regexp("ABC123", "ABC.*"))
+ self.assertTrue(self._regexp("ABC123", "[0-9]+$"))
+ self.assertFalse(self._regexp("ABC123", "^XYZ"))
+
+ def test_null_value_does_not_match(self) -> None:
+ self.assertFalse(self._regexp(None, ".*"))
+
+ def test_invalid_pattern_does_not_raise(self) -> None:
+ self.assertFalse(self._regexp("ABC123", "(unclosed"))
+
+ def test_catastrophic_pattern_is_time_bounded(self) -> None:
+ # Without the timeout this evaluation backtracks for minutes to hours
+ # and wedges the whole database thread (GHSA-q8jx-q884-jcq9).
+ catastrophic = "(a{2,})+c"
+ subject = "a" * 4000
+
+ start = time.monotonic()
+ result = self._regexp(subject, catastrophic)
+ elapsed = time.monotonic() - start
+
+ # The pattern does not match; the guarantee is that it returns quickly.
+ self.assertFalse(result)
+ self.assertLess(elapsed, REGEXP_TIMEOUT_SECONDS + 2.0)
diff --git a/frigate/test/test_stationary_classifier.py b/frigate/test/test_stationary_classifier.py
new file mode 100644
index 0000000000..bd5e2c41e2
--- /dev/null
+++ b/frigate/test/test_stationary_classifier.py
@@ -0,0 +1,39 @@
+"""Tests for stationary object classification thresholds."""
+
+import unittest
+
+from frigate.track.stationary_classifier import (
+ DEFAULT_OBJECT_THRESHOLDS,
+ DYNAMIC_OBJECT_THRESHOLDS,
+ NON_STATIONARY_OBJECT_THRESHOLDS,
+ STATIONARY_OBJECT_THRESHOLDS,
+ StationaryThresholds,
+ get_stationary_threshold,
+)
+
+
+class TestStationaryThresholds(unittest.TestCase):
+ def test_known_labels_return_expected_singletons(self) -> None:
+ self.assertIs(get_stationary_threshold("package"), STATIONARY_OBJECT_THRESHOLDS)
+ self.assertIs(get_stationary_threshold("car"), DYNAMIC_OBJECT_THRESHOLDS)
+ self.assertIs(
+ get_stationary_threshold("license_plate"),
+ NON_STATIONARY_OBJECT_THRESHOLDS,
+ )
+
+ def test_unknown_label_returns_shared_default(self) -> None:
+ # an unknown label must reuse the shared default instance, not allocate
+ # a fresh one on every call (this runs per object per frame)
+ first = get_stationary_threshold("person")
+ second = get_stationary_threshold("dog")
+ self.assertIs(first, DEFAULT_OBJECT_THRESHOLDS)
+ self.assertIs(second, DEFAULT_OBJECT_THRESHOLDS)
+
+ def test_default_matches_a_fresh_instance(self) -> None:
+ # the shared default must be value-equivalent to the previous
+ # per-call StationaryThresholds()
+ self.assertEqual(DEFAULT_OBJECT_THRESHOLDS, StationaryThresholds())
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/frigate/test/test_update_yaml.py b/frigate/test/test_update_yaml.py
new file mode 100644
index 0000000000..e9e160c8f4
--- /dev/null
+++ b/frigate/test/test_update_yaml.py
@@ -0,0 +1,183 @@
+"""Test in-place yaml config updates."""
+
+import os
+import tempfile
+import unittest
+
+from ruamel.yaml import YAML
+
+from frigate.util.builtin import update_yaml_file_bulk
+
+
+class TestUpdateYaml(unittest.TestCase):
+ def setUp(self) -> None:
+ self.yaml = YAML()
+ fd, self.config_path = tempfile.mkstemp(suffix=".yml")
+ os.close(fd)
+
+ def tearDown(self) -> None:
+ os.unlink(self.config_path)
+
+ def _write(self, text: str) -> None:
+ with open(self.config_path, "w") as f:
+ f.write(text)
+
+ def _read(self) -> str:
+ with open(self.config_path) as f:
+ return f.read()
+
+ def _load(self):
+ with open(self.config_path) as f:
+ return self.yaml.load(f)
+
+ def test_delete_key(self):
+ """Deleting a key removes it and leaves valid yaml."""
+ self._write(
+ "cameras:\n"
+ " cam1:\n"
+ " objects:\n"
+ " filters:\n"
+ " car:\n"
+ " mask: 0,0.45,0.245,0.45\n"
+ )
+ update_yaml_file_bulk(
+ self.config_path, {"cameras.cam1.objects.filters.car.mask": ""}
+ )
+ data = self._load()
+ assert "mask" not in data["cameras"]["cam1"]["objects"]["filters"]["car"]
+
+ def test_delete_commented_key_emptying_map(self):
+ """Deleting the only key of a map whose key carries comments must not
+ emit unparseable yaml (orphaned comment tokens above a flow-style {})."""
+ self._write(
+ "cameras:\n"
+ " cam1:\n"
+ " objects:\n"
+ " filters:\n"
+ " car:\n"
+ " # cars parked across the street\n"
+ " # second comment line\n"
+ " mask: 0,0.45,0.245,0.45\n"
+ " motion:\n"
+ " mask: 0,0.449,0.686,0.395\n"
+ )
+ update_yaml_file_bulk(
+ self.config_path, {"cameras.cam1.objects.filters.car.mask": ""}
+ )
+ # must re-parse cleanly
+ data = self._load()
+ assert "mask" not in data["cameras"]["cam1"]["objects"]["filters"]["car"]
+ assert data["cameras"]["cam1"]["motion"]["mask"] == "0,0.449,0.686,0.395"
+ # the orphaned comments must be gone from the file, not just parseable
+ content = self._read()
+ assert "cars parked across the street" not in content
+ assert "second comment line" not in content
+
+ def test_delete_last_named_mask_emptying_map(self):
+ """The path the current UI actually sends: a named object mask deleted
+ down to an empty `mask` map, with a comment inside that map."""
+ self._write(
+ "cameras:\n"
+ " cam1:\n"
+ " objects:\n"
+ " filters:\n"
+ " car:\n"
+ " mask:\n"
+ " # ignore the neighbor's driveway\n"
+ " driveway:\n"
+ " coordinates: 0,0.1,0.2,0.3\n"
+ )
+ update_yaml_file_bulk(
+ self.config_path,
+ {"cameras.cam1.objects.filters.car.mask.driveway": ""},
+ )
+ data = self._load()
+ assert data["cameras"]["cam1"]["objects"]["filters"]["car"]["mask"] == {}
+ assert "ignore the neighbor's driveway" not in self._read()
+
+ def test_delete_last_commented_list_item(self):
+ """Deleting the last element of a commented sequence must not emit
+ an orphaned comment above a flow-style [] at column 0."""
+ self._write(
+ "cameras:\n"
+ " cam1:\n"
+ " motion:\n"
+ " mask:\n"
+ " # driveway motion mask\n"
+ " - 0,0.4,0.6,0.4\n"
+ )
+ update_yaml_file_bulk(self.config_path, {"cameras.cam1.motion.mask.0": ""})
+ data = self._load()
+ assert data["cameras"]["cam1"]["motion"]["mask"] == []
+ assert "driveway motion mask" not in self._read()
+
+ def test_delete_list_item_preserves_remaining(self):
+ """Deleting one element of a sequence keeps the others and stays valid."""
+ self._write(
+ "cameras:\n"
+ " cam1:\n"
+ " motion:\n"
+ " mask:\n"
+ " - 0,0.4,0.6,0.4\n"
+ " - 0,0.1,0.2,0.3\n"
+ )
+ update_yaml_file_bulk(self.config_path, {"cameras.cam1.motion.mask.0": ""})
+ data = self._load()
+ assert data["cameras"]["cam1"]["motion"]["mask"] == ["0,0.1,0.2,0.3"]
+
+ def test_delete_key_preserves_siblings(self):
+ """Deleting one key among several keeps the sibling entries and any
+ comments on keys preceding the deleted one."""
+ self._write(
+ "cameras:\n"
+ " cam1:\n"
+ " objects:\n"
+ " filters:\n"
+ " car:\n"
+ " # mask drawn around the parked suv\n"
+ " mask: 0,0.45,0.245,0.45\n"
+ " threshold: 0.8\n"
+ )
+ update_yaml_file_bulk(
+ self.config_path, {"cameras.cam1.objects.filters.car.threshold": ""}
+ )
+ data = self._load()
+ car = data["cameras"]["cam1"]["objects"]["filters"]["car"]
+ assert "threshold" not in car
+ assert car["mask"] == "0,0.45,0.245,0.45"
+ assert "# mask drawn around the parked suv" in self._read()
+
+ def test_delete_first_commented_key_keeps_map_valid(self):
+ """Deleting a commented key from a map that still has other keys
+ leaves the remaining entries intact and the file parseable."""
+ self._write(
+ "cameras:\n"
+ " cam1:\n"
+ " objects:\n"
+ " filters:\n"
+ " car:\n"
+ " # comment on the deleted key\n"
+ " mask: 0,0.45,0.245,0.45\n"
+ " threshold: 0.8\n"
+ )
+ update_yaml_file_bulk(
+ self.config_path, {"cameras.cam1.objects.filters.car.mask": ""}
+ )
+ data = self._load()
+ car = data["cameras"]["cam1"]["objects"]["filters"]["car"]
+ assert "mask" not in car
+ assert car["threshold"] == 0.8
+
+ def test_update_value_preserves_comments(self):
+ """Updating a value keeps surrounding comments intact."""
+ self._write(
+ "cameras:\n cam1:\n detect:\n # tuned for the pi\n fps: 4\n"
+ )
+ update_yaml_file_bulk(self.config_path, {"cameras.cam1.detect.fps": 5})
+ data = self._load()
+ assert data["cameras"]["cam1"]["detect"]["fps"] == 5
+ assert "# tuned for the pi" in self._read()
+
+
+if __name__ == "__main__":
+ unittest.main(verbosity=2)
diff --git a/frigate/test/test_video.py b/frigate/test/test_video.py
index 8612990e2e..d09a60559f 100644
--- a/frigate/test/test_video.py
+++ b/frigate/test/test_video.py
@@ -82,6 +82,24 @@ class TestRegion(unittest.TestCase):
assert len(cluster_candidates) == 2
+ def test_cluster_candidates_partition_boxes(self):
+ # every box index must appear in exactly one cluster (no box used twice,
+ # none dropped) - the invariant the used-box tracking enforces
+ boxes = [
+ (100, 100, 200, 200),
+ (202, 150, 252, 200),
+ (210, 160, 260, 210),
+ (900, 900, 950, 950),
+ (905, 905, 955, 955),
+ ]
+
+ cluster_candidates = get_cluster_candidates(
+ self.frame_shape, self.min_region_size, boxes
+ )
+
+ assigned = [idx for cluster in cluster_candidates for idx in cluster]
+ self.assertEqual(sorted(assigned), list(range(len(boxes))))
+
def test_transliterate_to_latin(self):
self.assertEqual(transliterate_to_latin("frégate"), "fregate")
self.assertEqual(transliterate_to_latin("utilité"), "utilite")
diff --git a/frigate/test/test_webpush_camera_monitoring.py b/frigate/test/test_webpush_camera_monitoring.py
new file mode 100644
index 0000000000..fa9172ad20
--- /dev/null
+++ b/frigate/test/test_webpush_camera_monitoring.py
@@ -0,0 +1,29 @@
+"""Tests for camera monitoring notification authorization."""
+
+import unittest
+from types import SimpleNamespace
+from unittest.mock import MagicMock
+
+from frigate.comms.webpush import WebPushClient
+
+
+class TestCameraMonitoringNotifications(unittest.TestCase):
+ def test_send_camera_monitoring_filters_by_camera_access(self):
+ client = WebPushClient.__new__(WebPushClient)
+ client.config = SimpleNamespace(
+ cameras={"front_door": SimpleNamespace(friendly_name=None)}
+ )
+ client.web_pushers = {"allowed": [], "denied": []}
+ client.user_cameras = {"allowed": {"front_door"}, "denied": set()}
+ client.check_registrations = MagicMock()
+ client.cleanup_registrations = MagicMock()
+ client.send_push_notification = MagicMock()
+
+ client.send_camera_monitoring(
+ {"camera": "front_door", "message": "Monitoring condition met"}
+ )
+
+ self.assertEqual(client.send_push_notification.call_count, 1)
+ self.assertEqual(
+ client.send_push_notification.call_args.kwargs["user"], "allowed"
+ )
diff --git a/frigate/test/test_webpush_suspend.py b/frigate/test/test_webpush_suspend.py
new file mode 100644
index 0000000000..1442bad21d
--- /dev/null
+++ b/frigate/test/test_webpush_suspend.py
@@ -0,0 +1,64 @@
+"""Tests for notification suspension expiry handling."""
+
+import datetime
+import unittest
+from unittest.mock import MagicMock
+
+from frigate.comms.webpush import WebPushClient
+
+
+class TestSuspensionExpiry(unittest.TestCase):
+ def _make_client(self, suspended_cameras: dict[str, int]) -> WebPushClient:
+ client = WebPushClient.__new__(WebPushClient)
+ client.suspended_cameras = suspended_cameras
+ client.suspension_broadcaster = MagicMock()
+ return client
+
+ def test_clears_and_broadcasts_expired_suspension(self):
+ now = datetime.datetime.now().timestamp()
+ client = self._make_client({"front_door": int(now - 60)})
+
+ client._clear_expired_suspensions()
+
+ self.assertEqual(client.suspended_cameras["front_door"], 0)
+ client.suspension_broadcaster.assert_called_once_with(
+ "front_door/notifications/suspended", "0", True
+ )
+
+ def test_leaves_active_suspension_untouched(self):
+ now = datetime.datetime.now().timestamp()
+ suspend_until = int(now + 3600)
+ client = self._make_client({"front_door": suspend_until})
+
+ client._clear_expired_suspensions()
+
+ self.assertEqual(client.suspended_cameras["front_door"], suspend_until)
+ client.suspension_broadcaster.assert_not_called()
+
+ def test_ignores_unsuspended_camera(self):
+ client = self._make_client({"front_door": 0})
+
+ client._clear_expired_suspensions()
+
+ self.assertEqual(client.suspended_cameras["front_door"], 0)
+ client.suspension_broadcaster.assert_not_called()
+
+ def test_only_expired_cameras_are_cleared(self):
+ now = datetime.datetime.now().timestamp()
+ active_until = int(now + 3600)
+ client = self._make_client(
+ {
+ "expired": int(now - 5),
+ "active": active_until,
+ "idle": 0,
+ }
+ )
+
+ client._clear_expired_suspensions()
+
+ self.assertEqual(client.suspended_cameras["expired"], 0)
+ self.assertEqual(client.suspended_cameras["active"], active_until)
+ self.assertEqual(client.suspended_cameras["idle"], 0)
+ client.suspension_broadcaster.assert_called_once_with(
+ "expired/notifications/suspended", "0", True
+ )
diff --git a/frigate/test/test_ws_auth.py b/frigate/test/test_ws_auth.py
new file mode 100644
index 0000000000..a9fc6e1320
--- /dev/null
+++ b/frigate/test/test_ws_auth.py
@@ -0,0 +1,294 @@
+"""Tests for WebSocket authorization checks."""
+
+import unittest
+
+from frigate.comms.ws import _check_ws_authorization
+from frigate.const import INSERT_MANY_RECORDINGS, UPDATE_CAMERA_ACTIVITY
+
+
+class TestCheckWsAuthorization(unittest.TestCase):
+ """Tests for the _check_ws_authorization pure function."""
+
+ DEFAULT_SEPARATOR = ","
+
+ # admin/viewer are reserved and always map to all cameras (empty list);
+ # custom roles map to a specific set of cameras.
+ ROLES_CONFIG = {
+ "admin": [],
+ "viewer": [],
+ "yard": ["front_door", "backyard"],
+ "garage_only": ["garage"],
+ }
+ CAMERA_NAMES = {"front_door", "backyard", "garage"}
+
+ # --- IPC topic blocking (unconditional, regardless of role) ---
+
+ def test_ipc_topic_blocked_for_admin(self):
+ self.assertFalse(
+ _check_ws_authorization(
+ INSERT_MANY_RECORDINGS, "admin", self.DEFAULT_SEPARATOR
+ )
+ )
+
+ def test_ipc_topic_blocked_for_viewer(self):
+ self.assertFalse(
+ _check_ws_authorization(
+ UPDATE_CAMERA_ACTIVITY, "viewer", self.DEFAULT_SEPARATOR
+ )
+ )
+
+ def test_ipc_topic_blocked_when_no_role(self):
+ self.assertFalse(
+ _check_ws_authorization(
+ INSERT_MANY_RECORDINGS, None, self.DEFAULT_SEPARATOR
+ )
+ )
+
+ # --- Viewer allowed topics ---
+
+ def test_viewer_can_send_on_connect(self):
+ self.assertTrue(
+ _check_ws_authorization("onConnect", "viewer", self.DEFAULT_SEPARATOR)
+ )
+
+ def test_viewer_can_send_model_state(self):
+ self.assertTrue(
+ _check_ws_authorization("modelState", "viewer", self.DEFAULT_SEPARATOR)
+ )
+
+ def test_viewer_can_send_audio_transcription_state(self):
+ self.assertTrue(
+ _check_ws_authorization(
+ "audioTranscriptionState", "viewer", self.DEFAULT_SEPARATOR
+ )
+ )
+
+ def test_viewer_can_send_birdseye_layout(self):
+ self.assertTrue(
+ _check_ws_authorization("birdseyeLayout", "viewer", self.DEFAULT_SEPARATOR)
+ )
+
+ def test_viewer_can_send_embeddings_reindex_progress(self):
+ self.assertTrue(
+ _check_ws_authorization(
+ "embeddingsReindexProgress", "viewer", self.DEFAULT_SEPARATOR
+ )
+ )
+
+ # --- Viewer blocked from admin topics ---
+
+ def test_viewer_blocked_from_restart(self):
+ self.assertFalse(
+ _check_ws_authorization("restart", "viewer", self.DEFAULT_SEPARATOR)
+ )
+
+ def test_viewer_blocked_from_camera_detect_set(self):
+ self.assertFalse(
+ _check_ws_authorization(
+ "front_door/detect/set", "viewer", self.DEFAULT_SEPARATOR
+ )
+ )
+
+ def test_viewer_blocked_from_camera_ptz(self):
+ self.assertFalse(
+ _check_ws_authorization("front_door/ptz", "viewer", self.DEFAULT_SEPARATOR)
+ )
+
+ def test_viewer_blocked_from_global_notifications_set(self):
+ self.assertFalse(
+ _check_ws_authorization(
+ "notifications/set", "viewer", self.DEFAULT_SEPARATOR
+ )
+ )
+
+ def test_viewer_blocked_from_camera_notifications_suspend(self):
+ self.assertFalse(
+ _check_ws_authorization(
+ "front_door/notifications/suspend", "viewer", self.DEFAULT_SEPARATOR
+ )
+ )
+
+ def test_viewer_blocked_from_arbitrary_unknown_topic(self):
+ self.assertFalse(
+ _check_ws_authorization(
+ "some_random_topic", "viewer", self.DEFAULT_SEPARATOR
+ )
+ )
+
+ # --- Admin access ---
+
+ def test_admin_can_send_restart(self):
+ self.assertTrue(
+ _check_ws_authorization("restart", "admin", self.DEFAULT_SEPARATOR)
+ )
+
+ def test_admin_can_send_camera_detect_set(self):
+ self.assertTrue(
+ _check_ws_authorization(
+ "front_door/detect/set", "admin", self.DEFAULT_SEPARATOR
+ )
+ )
+
+ def test_admin_can_send_camera_ptz(self):
+ self.assertTrue(
+ _check_ws_authorization("front_door/ptz", "admin", self.DEFAULT_SEPARATOR)
+ )
+
+ # --- Comma-separated roles ---
+
+ def test_comma_separated_admin_viewer_grants_admin(self):
+ self.assertTrue(
+ _check_ws_authorization("restart", "admin,viewer", self.DEFAULT_SEPARATOR)
+ )
+
+ def test_comma_separated_viewer_admin_grants_admin(self):
+ self.assertTrue(
+ _check_ws_authorization("restart", "viewer,admin", self.DEFAULT_SEPARATOR)
+ )
+
+ def test_comma_separated_with_spaces(self):
+ self.assertTrue(
+ _check_ws_authorization("restart", "viewer, admin", self.DEFAULT_SEPARATOR)
+ )
+
+ # --- Custom separator ---
+
+ def test_pipe_separator(self):
+ self.assertTrue(_check_ws_authorization("restart", "viewer|admin", "|"))
+
+ def test_pipe_separator_no_admin(self):
+ self.assertFalse(_check_ws_authorization("restart", "viewer|editor", "|"))
+
+ # --- No role header (fail-closed) ---
+
+ def test_no_role_header_blocks_admin_topics(self):
+ self.assertFalse(
+ _check_ws_authorization("restart", None, self.DEFAULT_SEPARATOR)
+ )
+
+ def test_no_role_header_allows_viewer_topics(self):
+ self.assertTrue(
+ _check_ws_authorization("onConnect", None, self.DEFAULT_SEPARATOR)
+ )
+
+ # --- Camera-scoped PTZ access (non-admin with camera access) ---
+
+ def test_viewer_can_ptz_camera_with_access(self):
+ # viewer maps to all cameras, so PTZ is allowed
+ self.assertTrue(
+ _check_ws_authorization(
+ "front_door/ptz",
+ "viewer",
+ self.DEFAULT_SEPARATOR,
+ self.ROLES_CONFIG,
+ self.CAMERA_NAMES,
+ )
+ )
+
+ def test_custom_role_can_ptz_assigned_camera(self):
+ self.assertTrue(
+ _check_ws_authorization(
+ "front_door/ptz",
+ "yard",
+ self.DEFAULT_SEPARATOR,
+ self.ROLES_CONFIG,
+ self.CAMERA_NAMES,
+ )
+ )
+
+ def test_custom_role_blocked_from_ptz_unassigned_camera(self):
+ self.assertFalse(
+ _check_ws_authorization(
+ "garage/ptz",
+ "yard",
+ self.DEFAULT_SEPARATOR,
+ self.ROLES_CONFIG,
+ self.CAMERA_NAMES,
+ )
+ )
+
+ def test_multiple_roles_union_camera_access_for_ptz(self):
+ # "yard" covers front_door/backyard, "garage_only" covers garage
+ self.assertTrue(
+ _check_ws_authorization(
+ "garage/ptz",
+ "yard,garage_only",
+ self.DEFAULT_SEPARATOR,
+ self.ROLES_CONFIG,
+ self.CAMERA_NAMES,
+ )
+ )
+
+ def test_unknown_role_blocked_from_ptz(self):
+ self.assertFalse(
+ _check_ws_authorization(
+ "front_door/ptz",
+ "nonexistent",
+ self.DEFAULT_SEPARATOR,
+ self.ROLES_CONFIG,
+ self.CAMERA_NAMES,
+ )
+ )
+
+ def test_no_role_header_treated_as_viewer_for_ptz(self):
+ # proxy-only / auth-disabled setups default to the viewer role
+ self.assertTrue(
+ _check_ws_authorization(
+ "front_door/ptz",
+ None,
+ self.DEFAULT_SEPARATOR,
+ self.ROLES_CONFIG,
+ self.CAMERA_NAMES,
+ )
+ )
+
+ def test_camera_access_does_not_grant_set_commands(self):
+ # camera access enables PTZ only, not config-changing "set" commands
+ self.assertFalse(
+ _check_ws_authorization(
+ "front_door/detect/set",
+ "yard",
+ self.DEFAULT_SEPARATOR,
+ self.ROLES_CONFIG,
+ self.CAMERA_NAMES,
+ )
+ )
+
+ def test_ptz_autotracker_stays_admin_only(self):
+ # ptz_autotracker is a config toggle, not a live-view action
+ self.assertFalse(
+ _check_ws_authorization(
+ "front_door/ptz_autotracker/set",
+ "viewer",
+ self.DEFAULT_SEPARATOR,
+ self.ROLES_CONFIG,
+ self.CAMERA_NAMES,
+ )
+ )
+
+ def test_admin_can_ptz_any_camera_with_config(self):
+ self.assertTrue(
+ _check_ws_authorization(
+ "garage/ptz",
+ "admin",
+ self.DEFAULT_SEPARATOR,
+ self.ROLES_CONFIG,
+ self.CAMERA_NAMES,
+ )
+ )
+
+ def test_ipc_topic_still_blocked_with_camera_access(self):
+ # IPC topics are blocked unconditionally, even with camera access
+ self.assertFalse(
+ _check_ws_authorization(
+ UPDATE_CAMERA_ACTIVITY,
+ "viewer",
+ self.DEFAULT_SEPARATOR,
+ self.ROLES_CONFIG,
+ self.CAMERA_NAMES,
+ )
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/frigate/test/test_ws_outbound_filter.py b/frigate/test/test_ws_outbound_filter.py
new file mode 100644
index 0000000000..ab1489da54
--- /dev/null
+++ b/frigate/test/test_ws_outbound_filter.py
@@ -0,0 +1,806 @@
+"""Tests for outbound WebSocket broadcast filtering."""
+
+import json
+import threading
+import unittest
+from types import SimpleNamespace
+from typing import Any
+
+from frigate.comms.ws import (
+ WebSocketClient,
+ _classify_outbound,
+ _collect_zone_names,
+ _extract_payload_camera,
+ _materialize_for_ws,
+ _ws_allowed_cameras,
+ _ws_is_unrestricted,
+)
+from frigate.config import FrigateConfig
+
+
+def _build_config(
+ *,
+ extra_roles: dict[str, list[str]] | None = None,
+ extra_cameras: dict[str, dict[str, Any]] | None = None,
+ extra_zones: dict[str, dict[str, dict[str, Any]]] | None = None,
+) -> FrigateConfig:
+ """Construct a FrigateConfig used by the outbound filter tests.
+
+ The default fixture has three cameras: front_door, back_door, garage.
+ Restricted role "house_only" sees front_door + back_door but not garage.
+ """
+ cameras: dict[str, dict[str, Any]] = {
+ "front_door": {
+ "ffmpeg": {
+ "inputs": [{"path": "rtsp://10.0.0.1:554/v", "roles": ["detect"]}],
+ },
+ "detect": {"height": 1080, "width": 1920, "fps": 5},
+ },
+ "back_door": {
+ "ffmpeg": {
+ "inputs": [{"path": "rtsp://10.0.0.2:554/v", "roles": ["detect"]}],
+ },
+ "detect": {"height": 1080, "width": 1920, "fps": 5},
+ },
+ "garage": {
+ "ffmpeg": {
+ "inputs": [{"path": "rtsp://10.0.0.3:554/v", "roles": ["detect"]}],
+ },
+ "detect": {"height": 1080, "width": 1920, "fps": 5},
+ },
+ }
+ if extra_cameras:
+ cameras.update(extra_cameras)
+ if extra_zones:
+ for cam_name, zones in extra_zones.items():
+ cameras[cam_name]["zones"] = zones
+
+ roles = {"house_only": ["front_door", "back_door"]}
+ if extra_roles:
+ roles.update(extra_roles)
+
+ return FrigateConfig(
+ mqtt={"host": "mqtt"},
+ auth={"roles": roles},
+ cameras=cameras,
+ )
+
+
+def _ws(role: str | None) -> Any:
+ """Build a fake ws4py-style websocket exposing ``environ``."""
+ environ = {} if role is None else {"HTTP_REMOTE_ROLE": role}
+ return SimpleNamespace(environ=environ, terminated=False, sent=[])
+
+
+class TestClassifyOutbound(unittest.TestCase):
+ """The pure classifier — bucket every topic into a scope."""
+
+ def setUp(self):
+ self.config = _build_config(
+ extra_zones={"front_door": {"driveway": {"coordinates": "0,0,1,0,1,1,0,1"}}}
+ )
+ self.all_cameras = set(self.config.cameras.keys())
+ self.all_zones = _collect_zone_names(self.config)
+
+ def _classify(self, topic: str) -> tuple[str, Any]:
+ return _classify_outbound(topic, self.all_cameras, self.all_zones)
+
+ # --- Global allowlist ---
+
+ def test_model_state_is_global(self):
+ self.assertEqual(self._classify("model_state"), ("global", None))
+
+ def test_profile_state_is_global(self):
+ self.assertEqual(self._classify("profile/state"), ("global", None))
+
+ def test_bare_notifications_state_is_global(self):
+ """The 2-segment ``notifications/state`` is global; the 3-segment
+ ``/notifications/state`` is camera-scoped (see below)."""
+ self.assertEqual(self._classify("notifications/state"), ("global", None))
+
+ def test_notification_test_is_global(self):
+ self.assertEqual(self._classify("notification_test"), ("global", None))
+
+ # --- Unrestricted-only ---
+
+ def test_birdseye_layout_is_unrestricted_only(self):
+ self.assertEqual(self._classify("birdseye_layout"), ("unrestricted_only", None))
+
+ # --- Camera-prefixed ---
+
+ def test_camera_state_topic_resolves_to_camera(self):
+ self.assertEqual(
+ self._classify("front_door/detect/state"), ("camera", "front_door")
+ )
+
+ def test_camera_motion_topic_resolves_to_camera(self):
+ self.assertEqual(self._classify("back_door/motion"), ("camera", "back_door"))
+
+ def test_camera_per_notification_topic_resolves_to_camera(self):
+ self.assertEqual(
+ self._classify("front_door/notifications/state"),
+ ("camera", "front_door"),
+ )
+
+ def test_camera_label_counter_resolves_to_camera(self):
+ self.assertEqual(self._classify("front_door/person"), ("camera", "front_door"))
+
+ def test_camera_object_mask_state_resolves_to_camera(self):
+ self.assertEqual(
+ self._classify("front_door/object_mask/zone_1/state"),
+ ("camera", "front_door"),
+ )
+
+ # --- Zone-prefixed ---
+
+ def test_zone_aggregate_topic_is_unrestricted_only(self):
+ self.assertEqual(self._classify("driveway/person"), ("unrestricted_only", None))
+
+ def test_zone_all_topic_is_unrestricted_only(self):
+ self.assertEqual(self._classify("driveway/all"), ("unrestricted_only", None))
+
+ # --- Payload-camera ---
+
+ def test_events_topic_marks_payload_camera_path(self):
+ self.assertEqual(
+ self._classify("events"), ("payload_camera", ("after", "camera"))
+ )
+
+ def test_reviews_topic_marks_payload_camera_path(self):
+ self.assertEqual(
+ self._classify("reviews"), ("payload_camera", ("after", "camera"))
+ )
+
+ def test_triggers_topic_marks_payload_camera_path(self):
+ self.assertEqual(self._classify("triggers"), ("payload_camera", ("camera",)))
+
+ def test_tracked_object_update_marks_payload_camera_path(self):
+ self.assertEqual(
+ self._classify("tracked_object_update"), ("payload_camera", ("camera",))
+ )
+
+ # --- Reshape ---
+
+ def test_camera_activity_is_reshape_by_camera_key(self):
+ self.assertEqual(
+ self._classify("camera_activity"), ("reshape_by_camera_key", None)
+ )
+
+ def test_audio_detections_is_reshape_by_camera_key(self):
+ self.assertEqual(
+ self._classify("audio_detections"), ("reshape_by_camera_key", None)
+ )
+
+ def test_job_state_is_reshape_job_state(self):
+ self.assertEqual(self._classify("job_state"), ("reshape_job_state", None))
+
+ def test_stats_is_reshape_stats(self):
+ self.assertEqual(self._classify("stats"), ("reshape_stats", None))
+
+ # --- Fail-closed ---
+
+ def test_unknown_topic_is_dropped(self):
+ self.assertEqual(self._classify("some_random_topic"), ("drop", None))
+
+ def test_unknown_camera_prefix_is_dropped(self):
+ self.assertEqual(self._classify("ghost_camera/detect/state"), ("drop", None))
+
+
+class TestCollectZoneNames(unittest.TestCase):
+ def test_zones_from_all_cameras(self):
+ config = _build_config(
+ extra_zones={
+ "front_door": {"driveway": {"coordinates": "0,0,1,0,1,1,0,1"}},
+ "back_door": {"yard": {"coordinates": "0,0,1,0,1,1,0,1"}},
+ }
+ )
+ self.assertEqual(_collect_zone_names(config), {"driveway", "yard"})
+
+ def test_no_zones_returns_empty(self):
+ self.assertEqual(_collect_zone_names(_build_config()), set())
+
+
+class TestExtractPayloadCamera(unittest.TestCase):
+ def test_extract_from_dict_path(self):
+ payload = {"after": {"camera": "front_door"}}
+ self.assertEqual(
+ _extract_payload_camera(payload, ("after", "camera")), "front_door"
+ )
+
+ def test_extract_from_json_string(self):
+ payload = json.dumps({"after": {"camera": "front_door"}})
+ self.assertEqual(
+ _extract_payload_camera(payload, ("after", "camera")), "front_door"
+ )
+
+ def test_extract_single_segment_path(self):
+ self.assertEqual(
+ _extract_payload_camera({"camera": "garage"}, ("camera",)), "garage"
+ )
+
+ def test_missing_key_returns_none(self):
+ self.assertIsNone(_extract_payload_camera({}, ("after", "camera")))
+
+ def test_malformed_json_returns_none(self):
+ self.assertIsNone(_extract_payload_camera("not-json", ("camera",)))
+
+ def test_non_string_camera_returns_none(self):
+ self.assertIsNone(_extract_payload_camera({"camera": 42}, ("camera",)))
+
+
+class TestWsRoleHelpers(unittest.TestCase):
+ def setUp(self):
+ self.config = _build_config()
+
+ def test_admin_is_unrestricted(self):
+ self.assertTrue(_ws_is_unrestricted(_ws("admin"), self.config))
+
+ def test_viewer_is_unrestricted(self):
+ self.assertTrue(_ws_is_unrestricted(_ws("viewer"), self.config))
+
+ def test_restricted_role_is_not_unrestricted(self):
+ self.assertFalse(_ws_is_unrestricted(_ws("house_only"), self.config))
+
+ def test_missing_role_is_not_unrestricted(self):
+ self.assertFalse(_ws_is_unrestricted(_ws(None), self.config))
+
+ def test_unknown_role_is_not_unrestricted(self):
+ self.assertFalse(_ws_is_unrestricted(_ws("ghost"), self.config))
+
+ def test_admin_allowed_cameras_is_all(self):
+ self.assertEqual(
+ _ws_allowed_cameras(_ws("admin"), self.config),
+ {"front_door", "back_door", "garage"},
+ )
+
+ def test_restricted_role_allowed_cameras_is_subset(self):
+ self.assertEqual(
+ _ws_allowed_cameras(_ws("house_only"), self.config),
+ {"front_door", "back_door"},
+ )
+
+ def test_missing_role_allowed_cameras_is_empty(self):
+ self.assertEqual(_ws_allowed_cameras(_ws(None), self.config), set())
+
+ def test_multi_role_union_grants_widest(self):
+ self.assertEqual(
+ _ws_allowed_cameras(_ws("house_only,admin"), self.config),
+ {"front_door", "back_door", "garage"},
+ )
+
+
+class TestMaterializeForWs(unittest.TestCase):
+ def setUp(self):
+ self.config = _build_config(
+ extra_zones={"front_door": {"driveway": {"coordinates": "0,0,1,0,1,1,0,1"}}}
+ )
+ self.all_cameras = set(self.config.cameras.keys())
+ self.all_zones = _collect_zone_names(self.config)
+
+ def _materialize(self, ws: Any, topic: str, payload: Any) -> str | None:
+ scope = _classify_outbound(topic, self.all_cameras, self.all_zones)
+ from frigate.comms.ws import _parse_json_payload
+
+ parsed = (
+ _parse_json_payload(payload)
+ if scope[0]
+ in (
+ "payload_camera",
+ "reshape_by_camera_key",
+ "reshape_job_state",
+ "reshape_stats",
+ )
+ else None
+ )
+ full = json.dumps({"topic": topic, "payload": payload})
+ return _materialize_for_ws(ws, topic, full, scope, parsed, self.config)
+
+ # --- Globals: every authenticated client sees them ---
+
+ def test_globals_reach_admin(self):
+ self.assertIsNotNone(self._materialize(_ws("admin"), "model_state", "{}"))
+
+ def test_globals_reach_restricted(self):
+ self.assertIsNotNone(self._materialize(_ws("house_only"), "model_state", "{}"))
+
+ def test_globals_reach_no_role(self):
+ """A missing role header still gets globals (matches viewer-default
+ for inbound)."""
+ self.assertIsNotNone(self._materialize(_ws(None), "model_state", "{}"))
+
+ # --- Unknown topic dropped for everyone ---
+
+ def test_unknown_topic_dropped_for_admin(self):
+ self.assertIsNone(self._materialize(_ws("admin"), "rogue_topic", "{}"))
+
+ # --- Non-global topics require a role (fail-closed) ---
+
+ def test_no_role_blocked_from_camera_topic(self):
+ self.assertIsNone(self._materialize(_ws(None), "front_door/detect/state", "ON"))
+
+ def test_no_role_blocked_from_events(self):
+ payload = json.dumps({"after": {"camera": "front_door"}})
+ self.assertIsNone(self._materialize(_ws(None), "events", payload))
+
+ # --- Camera-prefixed ---
+
+ def test_restricted_role_sees_allowed_camera(self):
+ self.assertIsNotNone(
+ self._materialize(_ws("house_only"), "front_door/detect/state", "ON")
+ )
+
+ def test_restricted_role_blocked_from_unallowed_camera(self):
+ self.assertIsNone(
+ self._materialize(_ws("house_only"), "garage/detect/state", "ON")
+ )
+
+ def test_admin_sees_all_camera_topics(self):
+ self.assertIsNotNone(
+ self._materialize(_ws("admin"), "garage/detect/state", "ON")
+ )
+
+ # --- Unrestricted-only (zones, birdseye_layout) ---
+
+ def test_zone_aggregate_blocked_for_restricted(self):
+ self.assertIsNone(self._materialize(_ws("house_only"), "driveway/person", 3))
+
+ def test_zone_aggregate_visible_to_admin(self):
+ self.assertIsNotNone(self._materialize(_ws("admin"), "driveway/person", 3))
+
+ def test_birdseye_layout_blocked_for_restricted(self):
+ payload = json.dumps(
+ {"front_door": {"x": 0, "y": 0, "width": 100, "height": 100}}
+ )
+ self.assertIsNone(
+ self._materialize(_ws("house_only"), "birdseye_layout", payload)
+ )
+
+ def test_birdseye_layout_visible_to_admin(self):
+ payload = json.dumps(
+ {"front_door": {"x": 0, "y": 0, "width": 100, "height": 100}}
+ )
+ self.assertIsNotNone(
+ self._materialize(_ws("admin"), "birdseye_layout", payload)
+ )
+
+ # --- Payload-camera ---
+
+ def test_events_filtered_by_payload_camera(self):
+ payload = json.dumps({"after": {"camera": "garage"}})
+ self.assertIsNone(self._materialize(_ws("house_only"), "events", payload))
+
+ payload = json.dumps({"after": {"camera": "front_door"}})
+ self.assertIsNotNone(self._materialize(_ws("house_only"), "events", payload))
+
+ def test_events_with_missing_camera_dropped(self):
+ payload = json.dumps({"after": {}})
+ self.assertIsNone(self._materialize(_ws("house_only"), "events", payload))
+
+ def test_triggers_filtered_by_payload_camera(self):
+ payload = json.dumps({"name": "t1", "camera": "garage"})
+ self.assertIsNone(self._materialize(_ws("house_only"), "triggers", payload))
+
+ # --- Reshape: dict keyed by camera ---
+
+ def test_camera_activity_filtered_to_allowed_keys(self):
+ payload = json.dumps(
+ {
+ "front_door": {"objects": 1},
+ "back_door": {"objects": 0},
+ "garage": {"objects": 2},
+ }
+ )
+ message = self._materialize(_ws("house_only"), "camera_activity", payload)
+ self.assertIsNotNone(message)
+ envelope = json.loads(message) # type: ignore[arg-type]
+ inner = json.loads(envelope["payload"])
+ self.assertEqual(set(inner.keys()), {"front_door", "back_door"})
+ self.assertNotIn("garage", inner)
+
+ def test_camera_activity_unchanged_for_admin(self):
+ payload = json.dumps({"front_door": {}, "back_door": {}, "garage": {}})
+ message = self._materialize(_ws("admin"), "camera_activity", payload)
+ envelope = json.loads(message) # type: ignore[arg-type]
+ self.assertEqual(envelope["payload"], payload)
+
+ def test_camera_activity_with_no_allowed_returns_none(self):
+ payload = json.dumps({"garage": {"objects": 2}})
+ self.assertIsNone(
+ self._materialize(_ws("house_only"), "camera_activity", payload)
+ )
+
+ def test_audio_detections_filtered_to_allowed_keys(self):
+ payload = json.dumps({"front_door": {"bark": {}}, "garage": {"speech": {}}})
+ message = self._materialize(_ws("house_only"), "audio_detections", payload)
+ envelope = json.loads(message) # type: ignore[arg-type]
+ inner = json.loads(envelope["payload"])
+ self.assertEqual(set(inner.keys()), {"front_door"})
+
+ # --- Reshape: job_state ---
+
+ def test_job_state_admin_sees_full_payload(self):
+ payload = json.dumps(
+ {
+ "motion_search": {"job_type": "motion_search", "camera": "garage"},
+ "media_sync": {"job_type": "media_sync"},
+ }
+ )
+ message = self._materialize(_ws("admin"), "job_state", payload)
+ envelope = json.loads(message) # type: ignore[arg-type]
+ self.assertEqual(envelope["payload"], payload)
+
+ def test_job_state_restricted_keeps_allowed_camera_jobs(self):
+ """Top-level camera field on a job entry: drop if not allowed."""
+ payload = json.dumps(
+ {
+ "motion_search": {"job_type": "motion_search", "camera": "front_door"},
+ "vlm_watch": {"job_type": "vlm_watch", "camera": "garage"},
+ }
+ )
+ message = self._materialize(_ws("house_only"), "job_state", payload)
+ envelope = json.loads(message) # type: ignore[arg-type]
+ inner = json.loads(envelope["payload"])
+ self.assertIn("motion_search", inner)
+ self.assertNotIn("vlm_watch", inner)
+
+ def test_job_state_export_results_jobs_filtered_per_recipient(self):
+ """The aggregated export broadcast nests per-camera sub-jobs under
+ ``results.jobs``. Restricted users must only see allowed entries."""
+ payload = json.dumps(
+ {
+ "export": {
+ "job_type": "export",
+ "status": "running",
+ "results": {
+ "jobs": [
+ {"job_type": "export", "camera": "front_door", "id": "a"},
+ {"job_type": "export", "camera": "garage", "id": "b"},
+ {"job_type": "export", "camera": "back_door", "id": "c"},
+ ]
+ },
+ }
+ }
+ )
+ message = self._materialize(_ws("house_only"), "job_state", payload)
+ envelope = json.loads(message) # type: ignore[arg-type]
+ inner = json.loads(envelope["payload"])
+ self.assertIn("export", inner)
+ kept_cameras = [j["camera"] for j in inner["export"]["results"]["jobs"]]
+ self.assertEqual(kept_cameras, ["front_door", "back_door"])
+ # Sibling fields like ``status`` must survive reshaping.
+ self.assertEqual(inner["export"]["status"], "running")
+
+ def test_job_state_export_entry_dropped_when_no_jobs_allowed(self):
+ payload = json.dumps(
+ {
+ "export": {
+ "job_type": "export",
+ "status": "running",
+ "results": {
+ "jobs": [
+ {"job_type": "export", "camera": "garage", "id": "b"},
+ ]
+ },
+ }
+ }
+ )
+ self.assertIsNone(self._materialize(_ws("house_only"), "job_state", payload))
+
+ # --- Reshape: stats ---
+
+ def _stats_payload(self) -> str:
+ return json.dumps(
+ {
+ "cameras": {
+ "front_door": {"camera_fps": 5.0, "pid": 1234},
+ "back_door": {"camera_fps": 5.0, "pid": 1235},
+ "garage": {"camera_fps": 5.0, "pid": 1236},
+ },
+ "detectors": {"cpu": {"detection_start": 0.0, "inference_speed": 10}},
+ "service": {"uptime": 12345, "version": "0.16.0"},
+ "camera_fps": 15.0,
+ "detection_fps": 6.0,
+ }
+ )
+
+ def test_stats_admin_sees_full_payload(self):
+ message = self._materialize(_ws("admin"), "stats", self._stats_payload())
+ envelope = json.loads(message) # type: ignore[arg-type]
+ self.assertEqual(envelope["payload"], self._stats_payload())
+
+ def test_stats_restricted_filters_camera_keys_but_keeps_aggregates(self):
+ message = self._materialize(_ws("house_only"), "stats", self._stats_payload())
+ envelope = json.loads(message) # type: ignore[arg-type]
+ inner = json.loads(envelope["payload"])
+ self.assertEqual(set(inner["cameras"].keys()), {"front_door", "back_door"})
+ self.assertNotIn("garage", inner["cameras"])
+ # Aggregates, detectors, and service block must survive.
+ self.assertEqual(inner["camera_fps"], 15.0)
+ self.assertEqual(inner["detection_fps"], 6.0)
+ self.assertIn("detectors", inner)
+ self.assertIn("service", inner)
+
+ def test_stats_restricted_with_no_allowed_cameras_still_sends_aggregates(self):
+ """A restricted role whose allow-list contains only nonexistent cameras
+ still gets the global aggregates and service block."""
+ config = _build_config(extra_roles={"empty_role": ["nonexistent"]})
+ from frigate.comms.ws import _parse_json_payload
+
+ payload = self._stats_payload()
+ all_cameras = set(config.cameras.keys())
+ scope = _classify_outbound("stats", all_cameras, _collect_zone_names(config))
+ full = json.dumps({"topic": "stats", "payload": payload})
+ message = _materialize_for_ws(
+ _ws("empty_role"),
+ "stats",
+ full,
+ scope,
+ _parse_json_payload(payload),
+ config,
+ )
+ envelope = json.loads(message) # type: ignore[arg-type]
+ inner = json.loads(envelope["payload"])
+ self.assertEqual(inner["cameras"], {})
+ self.assertEqual(inner["camera_fps"], 15.0)
+ self.assertIn("service", inner)
+
+ def test_stats_without_cameras_key_passes_through(self):
+ """A malformed stats payload missing the cameras sub-dict shouldn't
+ break delivery for restricted users — fall back to the full message."""
+ payload = json.dumps({"detectors": {}, "service": {}, "detection_fps": 0.0})
+ message = self._materialize(_ws("house_only"), "stats", payload)
+ envelope = json.loads(message) # type: ignore[arg-type]
+ self.assertEqual(envelope["payload"], payload)
+
+ def test_job_state_export_entry_unchanged_for_admin(self):
+ payload = json.dumps(
+ {
+ "export": {
+ "job_type": "export",
+ "status": "running",
+ "results": {
+ "jobs": [
+ {"job_type": "export", "camera": "garage", "id": "b"},
+ ]
+ },
+ }
+ }
+ )
+ message = self._materialize(_ws("admin"), "job_state", payload)
+ envelope = json.loads(message) # type: ignore[arg-type]
+ self.assertEqual(envelope["payload"], payload)
+
+ def test_job_state_restricted_keeps_global_jobs(self):
+ """media_sync has no camera field; restricted users still see it."""
+ payload = json.dumps(
+ {"media_sync": {"job_type": "media_sync", "status": "running"}}
+ )
+ message = self._materialize(_ws("house_only"), "job_state", payload)
+ envelope = json.loads(message) # type: ignore[arg-type]
+ inner = json.loads(envelope["payload"])
+ self.assertIn("media_sync", inner)
+
+ def test_job_state_debug_replay_nested_source_camera_filtered(self):
+ """debug_replay puts ``source_camera`` inside ``results`` (see
+ jobs/debug_replay.py:to_dict). Restricted users must not receive
+ entries whose nested source camera is unauthorized."""
+ payload = json.dumps(
+ {
+ "debug_replay": {
+ "id": "bd6dc99d-a7d",
+ "job_type": "debug_replay",
+ "status": "running",
+ "start_time": 1.0,
+ "end_time": None,
+ "error_message": None,
+ "results": {
+ "current_step": "preparing_clip",
+ "progress_percent": 0.0,
+ "source_camera": "garage",
+ "replay_camera_name": "_replay_garage",
+ "start_ts": 0.0,
+ "end_ts": 1.0,
+ },
+ }
+ }
+ )
+ self.assertIsNone(self._materialize(_ws("house_only"), "job_state", payload))
+
+ def test_job_state_debug_replay_nested_source_camera_allowed(self):
+ payload = json.dumps(
+ {
+ "debug_replay": {
+ "id": "bd6dc99d-a7d",
+ "job_type": "debug_replay",
+ "status": "running",
+ "results": {
+ "source_camera": "front_door",
+ "replay_camera_name": "_replay_front_door",
+ },
+ }
+ }
+ )
+ message = self._materialize(_ws("house_only"), "job_state", payload)
+ envelope = json.loads(message) # type: ignore[arg-type]
+ inner = json.loads(envelope["payload"])
+ self.assertIn("debug_replay", inner)
+ self.assertEqual(
+ inner["debug_replay"]["results"]["source_camera"], "front_door"
+ )
+
+
+class _FakeManager:
+ """Minimal ws4py manager: holds clients and exposes a lock."""
+
+ def __init__(self, clients: list[Any]) -> None:
+ self.lock = threading.Lock()
+ self.websockets = {id(c): c for c in clients}
+
+
+class _FakeServer:
+ def __init__(self, manager: _FakeManager) -> None:
+ self.manager = manager
+
+
+class _CapturingWs(SimpleNamespace):
+ """Fake ws4py client that records what was sent."""
+
+ def __init__(self, role: str | None) -> None:
+ environ = {} if role is None else {"HTTP_REMOTE_ROLE": role}
+ super().__init__(environ=environ, terminated=False)
+ self.sent: list[str] = []
+
+ def send(self, message: str) -> None: # noqa: D401 - matches ws4py API
+ self.sent.append(message)
+
+
+class TestPublishEndToEnd(unittest.TestCase):
+ """Drive WebSocketClient.publish() against fake clients with different roles."""
+
+ def setUp(self):
+ self.config = _build_config(
+ extra_zones={"front_door": {"driveway": {"coordinates": "0,0,1,0,1,1,0,1"}}}
+ )
+ self.admin = _CapturingWs("admin")
+ self.restricted = _CapturingWs("house_only")
+ self.anon = _CapturingWs(None)
+ self.client = WebSocketClient(self.config)
+ self.client.websocket_server = _FakeServer(
+ _FakeManager([self.admin, self.restricted, self.anon])
+ )
+
+ def _payloads(self, ws: _CapturingWs) -> list[Any]:
+ return [json.loads(m)["payload"] for m in ws.sent]
+
+ def test_global_topic_reaches_everyone(self):
+ self.client.publish("model_state", "{}")
+ self.assertEqual(len(self.admin.sent), 1)
+ self.assertEqual(len(self.restricted.sent), 1)
+ self.assertEqual(len(self.anon.sent), 1)
+
+ def test_camera_topic_filters_restricted_recipient(self):
+ self.client.publish("garage/detect/state", "ON")
+ self.assertEqual(len(self.admin.sent), 1)
+ self.assertEqual(len(self.restricted.sent), 0)
+ self.assertEqual(len(self.anon.sent), 0)
+
+ def test_camera_topic_allows_restricted_recipient_for_allowed_camera(self):
+ self.client.publish("front_door/detect/state", "ON")
+ self.assertEqual(len(self.admin.sent), 1)
+ self.assertEqual(len(self.restricted.sent), 1)
+ self.assertEqual(len(self.anon.sent), 0)
+
+ def test_events_payload_filtered(self):
+ self.client.publish("events", json.dumps({"after": {"camera": "garage"}}))
+ self.assertEqual(len(self.admin.sent), 1)
+ self.assertEqual(len(self.restricted.sent), 0)
+
+ def test_camera_activity_reshaped_per_recipient(self):
+ self.client.publish(
+ "camera_activity",
+ json.dumps(
+ {
+ "front_door": {"objects": 1},
+ "back_door": {"objects": 0},
+ "garage": {"objects": 2},
+ }
+ ),
+ )
+ self.assertEqual(len(self.admin.sent), 1)
+ admin_inner = json.loads(self._payloads(self.admin)[0])
+ self.assertEqual(set(admin_inner.keys()), {"front_door", "back_door", "garage"})
+
+ self.assertEqual(len(self.restricted.sent), 1)
+ restricted_inner = json.loads(self._payloads(self.restricted)[0])
+ self.assertEqual(set(restricted_inner.keys()), {"front_door", "back_door"})
+
+ self.assertEqual(len(self.anon.sent), 0)
+
+ def test_birdseye_layout_blocked_for_restricted_and_anon(self):
+ self.client.publish(
+ "birdseye_layout",
+ json.dumps({"front_door": {"x": 0, "y": 0, "width": 1, "height": 1}}),
+ )
+ self.assertEqual(len(self.admin.sent), 1)
+ self.assertEqual(len(self.restricted.sent), 0)
+ self.assertEqual(len(self.anon.sent), 0)
+
+ def test_zone_aggregate_blocked_for_restricted(self):
+ self.client.publish("driveway/person", 2)
+ self.assertEqual(len(self.admin.sent), 1)
+ self.assertEqual(len(self.restricted.sent), 0)
+
+ def test_stats_reshaped_per_recipient(self):
+ self.client.publish(
+ "stats",
+ json.dumps(
+ {
+ "cameras": {
+ "front_door": {"camera_fps": 5.0},
+ "garage": {"camera_fps": 5.0},
+ },
+ "service": {"uptime": 1},
+ "camera_fps": 10.0,
+ }
+ ),
+ )
+ self.assertEqual(len(self.admin.sent), 1)
+ admin_inner = json.loads(self._payloads(self.admin)[0])
+ self.assertEqual(set(admin_inner["cameras"].keys()), {"front_door", "garage"})
+
+ self.assertEqual(len(self.restricted.sent), 1)
+ restricted_inner = json.loads(self._payloads(self.restricted)[0])
+ self.assertEqual(set(restricted_inner["cameras"].keys()), {"front_door"})
+ self.assertEqual(restricted_inner["camera_fps"], 10.0)
+ self.assertIn("service", restricted_inner)
+
+ # Stats requires a role; anonymous gets nothing.
+ self.assertEqual(len(self.anon.sent), 0)
+
+ def test_export_job_state_filters_results_jobs_per_recipient(self):
+ self.client.publish(
+ "job_state",
+ json.dumps(
+ {
+ "export": {
+ "job_type": "export",
+ "status": "running",
+ "results": {
+ "jobs": [
+ {"camera": "front_door", "id": "a"},
+ {"camera": "garage", "id": "b"},
+ ]
+ },
+ }
+ }
+ ),
+ )
+ self.assertEqual(len(self.admin.sent), 1)
+ admin_inner = json.loads(self._payloads(self.admin)[0])
+ self.assertEqual(
+ [j["camera"] for j in admin_inner["export"]["results"]["jobs"]],
+ ["front_door", "garage"],
+ )
+
+ self.assertEqual(len(self.restricted.sent), 1)
+ restricted_inner = json.loads(self._payloads(self.restricted)[0])
+ self.assertEqual(
+ [j["camera"] for j in restricted_inner["export"]["results"]["jobs"]],
+ ["front_door"],
+ )
+
+ def test_unknown_topic_dropped_for_everyone(self):
+ self.client.publish("some_rogue_topic", "data")
+ self.assertEqual(self.admin.sent, [])
+ self.assertEqual(self.restricted.sent, [])
+ self.assertEqual(self.anon.sent, [])
+
+ def test_terminated_client_is_skipped(self):
+ self.restricted.terminated = True
+ self.client.publish("front_door/detect/state", "ON")
+ self.assertEqual(len(self.admin.sent), 1)
+ self.assertEqual(len(self.restricted.sent), 0)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/frigate/track/norfair_tracker.py b/frigate/track/norfair_tracker.py
index 84a0f390a3..6b39885c0d 100644
--- a/frigate/track/norfair_tracker.py
+++ b/frigate/track/norfair_tracker.py
@@ -1,7 +1,8 @@
import logging
import random
import string
-from typing import Any, Sequence, cast
+from collections.abc import Sequence
+from typing import Any, cast
import cv2
import numpy as np
@@ -45,6 +46,17 @@ def distance(detection: np.ndarray, estimate: np.ndarray) -> float:
estimate_dim = np.diff(estimate, axis=0).flatten()
detection_dim = np.diff(detection, axis=0).flatten()
+ # Guard against degenerate or non-finite boxes
+ if (
+ not np.all(np.isfinite(estimate_dim))
+ or not np.all(np.isfinite(detection_dim))
+ or estimate_dim[0] <= 0
+ or estimate_dim[1] <= 0
+ or detection_dim[0] <= 0
+ or detection_dim[1] <= 0
+ ):
+ return float("inf")
+
# get bottom center positions
detection_position = np.array(
[np.average(detection[:, 0]), np.max(detection[:, 1])]
@@ -630,9 +642,11 @@ class NorfairTracker(ObjectTracker):
self.deregister(self.track_id_map[e_id], e_id)
# update list of object boxes that don't have a tracked object yet
- tracked_object_boxes = [obj["box"] for obj in self.tracked_objects.values()]
+ tracked_object_boxes = {
+ tuple(obj["box"]) for obj in self.tracked_objects.values()
+ }
self.untracked_object_boxes = [
- o[2] for o in detections if o[2] not in tracked_object_boxes
+ o[2] for o in detections if tuple(o[2]) not in tracked_object_boxes
]
def print_objects_as_table(self, tracked_objects: Sequence) -> None:
diff --git a/frigate/track/object_processing.py b/frigate/track/object_processing.py
index 3fae8da6f4..5832d8cdb8 100644
--- a/frigate/track/object_processing.py
+++ b/frigate/track/object_processing.py
@@ -357,6 +357,9 @@ class TrackedObjectProcessor(threading.Thread):
def get_current_frame_time(self, camera: str) -> float:
"""Returns the latest frame time for a given camera."""
+ if camera not in self.camera_states:
+ return 0.0
+
return self.camera_states[camera].current_frame_time
def set_sub_label(
@@ -773,7 +776,9 @@ class TrackedObjectProcessor(threading.Thread):
logger.debug(f"Camera {camera} disabled, skipping update")
continue
- camera_state = self.camera_states[camera]
+ camera_state = self.camera_states.get(camera)
+ if camera_state is None:
+ continue
camera_state.update(
frame_name, frame_time, current_tracked_objects, motion_boxes, regions
diff --git a/frigate/track/stationary_classifier.py b/frigate/track/stationary_classifier.py
index bea37f641b..1e22ec6f6d 100644
--- a/frigate/track/stationary_classifier.py
+++ b/frigate/track/stationary_classifier.py
@@ -63,6 +63,9 @@ NON_STATIONARY_OBJECT_THRESHOLDS = StationaryThresholds(
max_stationary_history=4,
)
+# Default thresholds for any other object label
+DEFAULT_OBJECT_THRESHOLDS = StationaryThresholds()
+
def get_stationary_threshold(label: str) -> StationaryThresholds:
"""Get the stationary thresholds for a given object label."""
@@ -76,7 +79,7 @@ def get_stationary_threshold(label: str) -> StationaryThresholds:
if label in NON_STATIONARY_OBJECT_THRESHOLDS.objects:
return NON_STATIONARY_OBJECT_THRESHOLDS
- return StationaryThresholds()
+ return DEFAULT_OBJECT_THRESHOLDS
class StationaryMotionClassifier:
diff --git a/frigate/track/tracked_object.py b/frigate/track/tracked_object.py
index 4fda92afd0..0a9c6f74f0 100644
--- a/frigate/track/tracked_object.py
+++ b/frigate/track/tracked_object.py
@@ -5,7 +5,7 @@ import math
import os
from collections import defaultdict
from statistics import median
-from typing import Any, Optional, cast
+from typing import Any, cast
import cv2
import numpy as np
@@ -81,7 +81,7 @@ class TrackedObject:
self.previous = self.to_dict()
@property
- def max_severity(self) -> Optional[str]:
+ def max_severity(self) -> str | None:
review_config = self.camera_config.review
if (
@@ -330,7 +330,12 @@ class TrackedObject:
if self.obj_data["position_changes"] != obj_data["position_changes"]:
significant_change = True
- if self.obj_data["attributes"] != obj_data["attributes"]:
+ # disappearance of a per-frame attribute can be caused by detection
+ # skipping the object on a frame (stationary objects on non-interval
+ # frames), so only flag when a new attribute label appears
+ prev_labels = {a["label"] for a in self.obj_data["attributes"]}
+ curr_labels = {a["label"] for a in obj_data["attributes"]}
+ if curr_labels - prev_labels:
significant_change = True
# if the state changed between stationary and active
@@ -526,8 +531,7 @@ class TrackedObject:
directory = os.path.join(THUMB_DIR, self.camera_config.name)
- if not os.path.exists(directory):
- os.makedirs(directory)
+ os.makedirs(directory, exist_ok=True)
thumb_bytes = self.get_thumbnail("webp")
@@ -586,7 +590,7 @@ class TrackedObjectAttribute:
"box": self.box,
}
- def find_best_object(self, objects: list[dict[str, Any]]) -> Optional[str]:
+ def find_best_object(self, objects: list[dict[str, Any]]) -> str | None:
"""Find the best attribute for each object and return its ID."""
best_object_area: float | None = None
best_object_id: str | None = None
diff --git a/frigate/util/audio.py b/frigate/util/audio.py
index eede9c0ea0..28fb0a6bad 100644
--- a/frigate/util/audio.py
+++ b/frigate/util/audio.py
@@ -3,7 +3,6 @@
import logging
import os
import subprocess as sp
-from typing import Optional
from pathvalidate import sanitize_filename
@@ -19,7 +18,7 @@ def get_audio_from_recording(
start_ts: float,
end_ts: float,
sample_rate: int = 16000,
-) -> Optional[bytes]:
+) -> bytes | None:
"""Extract audio from recording files between start_ts and end_ts in WAV format suitable for sherpa-onnx.
Args:
diff --git a/frigate/util/builtin.py b/frigate/util/builtin.py
index bd45a4a1f1..35dba2f3a7 100644
--- a/frigate/util/builtin.py
+++ b/frigate/util/builtin.py
@@ -2,7 +2,6 @@
import ast
import copy
-import datetime
import logging
import math
import multiprocessing.queues
@@ -10,17 +9,22 @@ import queue
import re
import shlex
import struct
+import time
import urllib.parse
+from collections import deque
from collections.abc import Mapping
from multiprocessing.managers import ValueProxy
from pathlib import Path
-from typing import Any, Dict, Optional, Tuple, Union
+from typing import TYPE_CHECKING, Any
import numpy as np
from ruamel.yaml import YAML
from frigate.const import REGEX_HTTP_CAMERA_USER_PASS, REGEX_RTSP_CAMERA_USER_PASS
+if TYPE_CHECKING:
+ from frigate.config import CameraConfig
+
logger = logging.getLogger(__name__)
@@ -29,23 +33,20 @@ class EventsPerSecond:
self._start = None
self._max_events = max_events
self._last_n_seconds = last_n_seconds
- self._timestamps = []
+ self._timestamps: deque[float] = deque(maxlen=max_events)
def start(self) -> None:
- self._start = datetime.datetime.now().timestamp()
+ self._start = time.monotonic()
def update(self) -> None:
- now = datetime.datetime.now().timestamp()
+ now = time.monotonic()
if self._start is None:
self._start = now
self._timestamps.append(now)
- # truncate the list when it goes 100 over the max_size
- if len(self._timestamps) > self._max_events + 100:
- self._timestamps = self._timestamps[(1 - self._max_events) :]
self.expire_timestamps(now)
def eps(self) -> float:
- now = datetime.datetime.now().timestamp()
+ now = time.monotonic()
if self._start is None:
self._start = now
# compute the (approximate) events in the last n seconds
@@ -60,7 +61,7 @@ class EventsPerSecond:
def expire_timestamps(self, now: float) -> None:
threshold = now - self._last_n_seconds
while self._timestamps and self._timestamps[0] < threshold:
- del self._timestamps[0]
+ self._timestamps.popleft()
class InferenceSpeed:
@@ -132,8 +133,26 @@ def get_ffmpeg_arg_list(arg: Any) -> list:
return arg if isinstance(arg, list) else shlex.split(arg)
+# all built-in record presets use this segment_time
+DEFAULT_RECORD_SEGMENT_TIME = 10
+
+
+def get_record_segment_time(config: "CameraConfig") -> int:
+ """Extract -segment_time from the camera's record output args."""
+ record_args = get_ffmpeg_arg_list(config.ffmpeg.output_args.record)
+
+ if record_args and record_args[0].startswith("preset"):
+ return DEFAULT_RECORD_SEGMENT_TIME
+
+ try:
+ idx = record_args.index("-segment_time")
+ return int(record_args[idx + 1])
+ except (ValueError, IndexError):
+ return DEFAULT_RECORD_SEGMENT_TIME
+
+
def load_labels(
- path: Optional[str], encoding="utf-8", prefill=91, indexed: bool | None = None
+ path: str | None, encoding="utf-8", prefill=91, indexed: bool | None = None
):
"""Loads labels from file (with or without index numbers).
Args:
@@ -145,7 +164,7 @@ def load_labels(
if path is None:
return {}
- with open(path, "r", encoding=encoding) as f:
+ with open(path, encoding=encoding) as f:
labels = {index: "unknown" for index in range(prefill)}
lines = f.readlines()
if not lines:
@@ -161,8 +180,8 @@ def load_labels(
def to_relative_box(
- width: int, height: int, box: Tuple[int, int, int, int]
-) -> Tuple[int | float, int | float, int | float, int | float]:
+ width: int, height: int, box: tuple[int, int, int, int]
+) -> tuple[int | float, int | float, int | float, int | float]:
return (
box[0] / width, # x
box[1] / height, # y
@@ -176,7 +195,7 @@ def create_mask(frame_shape, mask):
mask_img[:] = 255
-def process_config_query_string(query_string: Dict[str, list]) -> Dict[str, Any]:
+def process_config_query_string(query_string: dict[str, list]) -> dict[str, Any]:
updates = {}
for key_path_str, new_value_list in query_string.items():
# use the string key as-is for updates dictionary
@@ -194,8 +213,8 @@ def process_config_query_string(query_string: Dict[str, list]) -> Dict[str, Any]
def flatten_config_data(
- config_data: Dict[str, Any], parent_key: str = ""
-) -> Dict[str, Any]:
+ config_data: dict[str, Any], parent_key: str = ""
+) -> dict[str, Any]:
items = []
for key, value in config_data.items():
escaped_key = escape_config_key_segment(str(key))
@@ -242,12 +261,12 @@ def split_config_key_path(key_path_str: str) -> list[str]:
return parts
-def update_yaml_file_bulk(file_path: str, updates: Dict[str, Any]):
+def update_yaml_file_bulk(file_path: str, updates: dict[str, Any]):
yaml = YAML()
yaml.indent(mapping=2, sequence=4, offset=2)
try:
- with open(file_path, "r") as f:
+ with open(file_path) as f:
data = yaml.load(f)
except FileNotFoundError:
logger.error(
@@ -274,26 +293,51 @@ def update_yaml_file_bulk(file_path: str, updates: Dict[str, Any]):
logger.error(f"Unable to write to Frigate config file {file_path}: {e}")
+def clear_orphaned_comments(collection, parent, parent_key) -> None:
+ """Drop stale ruamel comment tokens after a deletion empties a collection.
+
+ When the last entry of a mapping or sequence is removed, any comments that
+ lived inside that collection's block are orphaned. ruamel then emits them
+ above a flow-style `{}`/`[]` dedented to column 0, which is unparseable and
+ corrupts the config. Clearing the emptied collection's own comment metadata
+ (and the parent's entry pointing at it) keeps the dump valid. Non-empty
+ collections are left untouched so comments on remaining siblings survive.
+ """
+ if not hasattr(collection, "ca") or len(collection) != 0:
+ return
+
+ collection.ca.items.clear()
+ collection.ca.comment = None
+ if parent is not None and hasattr(parent, "ca"):
+ parent.ca.items.pop(parent_key, None)
+
+
def update_yaml(data, key_path, new_value):
temp = data
+ parent = None
+ parent_key = None
for key in key_path[:-1]:
if isinstance(key, tuple):
if key[0] not in temp:
temp[key[0]] = [{}] * max(1, key[1] + 1)
elif len(temp[key[0]]) <= key[1]:
temp[key[0]] += [{}] * (key[1] - len(temp[key[0]]) + 1)
+ parent, parent_key = temp[key[0]], key[1]
temp = temp[key[0]][key[1]]
else:
if key not in temp or temp[key] is None:
temp[key] = {}
+ parent, parent_key = temp, key
temp = temp[key]
last_key = key_path[-1]
if new_value == "":
if isinstance(last_key, tuple):
del temp[last_key[0]][last_key[1]]
+ clear_orphaned_comments(temp[last_key[0]], temp, last_key[0])
else:
del temp[last_key]
+ clear_orphaned_comments(temp, parent, parent_key)
else:
if isinstance(last_key, tuple):
if last_key[0] not in temp:
@@ -394,9 +438,7 @@ def generate_color_palette(n):
return colors
-def serialize(
- vector: Union[list[float], np.ndarray, float], pack: bool = True
-) -> bytes:
+def serialize(vector: list[float] | np.ndarray | float, pack: bool = True) -> bytes:
"""Serializes a list of floats, numpy array, or single float into a compact "raw bytes" format"""
if isinstance(vector, np.ndarray):
# Convert numpy array to list of floats
@@ -415,7 +457,7 @@ def serialize(
else:
return vector
except struct.error as e:
- raise ValueError(f"Failed to pack vector: {e}. Vector: {vector}")
+ raise ValueError(f"Failed to pack vector: {e}. Vector: {vector}") from e
def deserialize(bytes_data: bytes) -> list[float]:
diff --git a/frigate/util/classification.py b/frigate/util/classification.py
index ada3ee1f71..a9345bbc56 100644
--- a/frigate/util/classification.py
+++ b/frigate/util/classification.py
@@ -24,8 +24,12 @@ from frigate.log import redirect_output_to_logger, suppress_stderr_during
from frigate.models import Event, Recordings, ReviewSegment
from frigate.types import ModelStatusTypesEnum
from frigate.util.downloader import ModelDownloader
-from frigate.util.file import get_event_thumbnail_bytes
-from frigate.util.image import get_image_from_recording
+from frigate.util.file import get_event_thumbnail_bytes, load_event_snapshot_image
+from frigate.util.image import (
+ calculate_region,
+ get_image_from_recording,
+ relative_box_to_absolute,
+)
from frigate.util.process import FrigateProcess
BATCH_SIZE = 16
@@ -80,7 +84,7 @@ def read_training_metadata(model_name: str) -> dict[str, any] | None:
return None
try:
- with open(metadata_path, "r") as f:
+ with open(metadata_path) as f:
metadata = json.load(f)
return metadata
except Exception as e:
@@ -290,7 +294,7 @@ class ClassificationTrainingProcess(FrigateProcess):
return True
except Exception as e:
- logger.error(f"Training failed for {self.model_name}: {e}", exc_info=True)
+ logger.exception(f"Training failed for {self.model_name}: {e}")
return False
@@ -390,7 +394,7 @@ def collect_state_classification_examples(
# Step 3: Extract keyframes from recordings with crops applied
keyframes = _extract_keyframes(
- "/usr/lib/ffmpeg/7.0/bin/ffmpeg", timestamps, temp_dir, cameras
+ "/usr/lib/ffmpeg/8.0/bin/ffmpeg", timestamps, temp_dir, cameras
)
# Step 4: Select 24 most visually distinct images (they're already cropped)
@@ -562,7 +566,7 @@ def _extract_keyframes(
relative_time = timestamp - recording.start_time
try:
- config = FfmpegConfig(path="/usr/lib/ffmpeg/7.0")
+ config = FfmpegConfig(path="/usr/lib/ffmpeg/8.0")
image_data = get_image_from_recording(
config,
recording.path,
@@ -713,7 +717,7 @@ def collect_object_classification_examples(
This function:
1. Queries events for the specified label
2. Selects 100 balanced events across different cameras and times
- 3. Retrieves thumbnails for selected events (with 33% center crop applied)
+ 3. Crops each event's clean snapshot around the object bounding box
4. Selects 24 most visually distinct thumbnails
5. Saves to dataset directory
@@ -728,7 +732,7 @@ def collect_object_classification_examples(
# Step 1: Query events for the specified label and cameras
events = list(
- Event.select().where((Event.label == label)).order_by(Event.start_time.asc())
+ Event.select().where(Event.label == label).order_by(Event.start_time.asc())
)
if not events:
@@ -832,66 +836,106 @@ def _select_balanced_events(
def _extract_event_thumbnails(events: list[Event], output_dir: str) -> list[str]:
"""
- Extract thumbnails from events and save to disk.
+ Extract a training image for each event.
+
+ Preferred path: load the full-frame clean snapshot and crop around the
+ stored bounding box with the same calculate_region(..., max(w, h), 1.0)
+ call the live ObjectClassificationProcessor uses, so wizard examples
+ are framed like inference-time inputs.
+
+ Fallback: if no clean snapshot exists (snapshots disabled, or only a
+ legacy annotated JPG is on disk), center-crop the stored thumbnail
+ using a step ladder sized from the box/region area ratio.
Args:
events: List of Event objects
- output_dir: Directory to save thumbnails
+ output_dir: Directory to save crops
Returns:
- List of paths to successfully extracted thumbnail images
+ List of paths to successfully extracted images
"""
- thumbnail_paths = []
+ image_paths = []
for idx, event in enumerate(events):
try:
- thumbnail_bytes = get_event_thumbnail_bytes(event)
+ img = _load_event_classification_crop(event)
+ if img is None:
+ continue
- if thumbnail_bytes:
- nparr = np.frombuffer(thumbnail_bytes, np.uint8)
- img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
-
- if img is not None:
- height, width = img.shape[:2]
-
- crop_size = 1.0
- if event.data and "box" in event.data and "region" in event.data:
- box = event.data["box"]
- region = event.data["region"]
-
- if len(box) == 4 and len(region) == 4:
- box_w, box_h = box[2], box[3]
- region_w, region_h = region[2], region[3]
-
- box_area = (box_w * box_h) / (region_w * region_h)
-
- if box_area < 0.05:
- crop_size = 0.4
- elif box_area < 0.10:
- crop_size = 0.5
- elif box_area < 0.20:
- crop_size = 0.65
- elif box_area < 0.35:
- crop_size = 0.80
- else:
- crop_size = 0.95
-
- crop_width = int(width * crop_size)
- crop_height = int(height * crop_size)
-
- x1 = (width - crop_width) // 2
- y1 = (height - crop_height) // 2
- x2 = x1 + crop_width
- y2 = y1 + crop_height
-
- cropped = img[y1:y2, x1:x2]
- resized = cv2.resize(cropped, (224, 224))
- output_path = os.path.join(output_dir, f"thumbnail_{idx:04d}.jpg")
- cv2.imwrite(output_path, resized)
- thumbnail_paths.append(output_path)
+ resized = cv2.resize(img, (224, 224))
+ output_path = os.path.join(output_dir, f"thumbnail_{idx:04d}.jpg")
+ cv2.imwrite(output_path, resized)
+ image_paths.append(output_path)
except Exception as e:
- logger.debug(f"Failed to extract thumbnail for event {event.id}: {e}")
+ logger.debug(f"Failed to extract image for event {event.id}: {e}")
continue
- return thumbnail_paths
+ return image_paths
+
+
+def _load_event_classification_crop(event: Event) -> np.ndarray | None:
+ """Prefer a snapshot-based object crop; fall back to a center-cropped thumbnail."""
+ if event.data and "box" in event.data:
+ snapshot, _ = load_event_snapshot_image(event, clean_only=True)
+ if snapshot is not None:
+ abs_box = relative_box_to_absolute(snapshot.shape, event.data["box"])
+ if abs_box is not None:
+ xmin, ymin, xmax, ymax = abs_box
+ box_w = xmax - xmin
+ box_h = ymax - ymin
+ if box_w > 0 and box_h > 0:
+ x1, y1, x2, y2 = calculate_region(
+ snapshot.shape,
+ xmin,
+ ymin,
+ xmax,
+ ymax,
+ max(box_w, box_h),
+ 1.0,
+ )
+ cropped = snapshot[y1:y2, x1:x2]
+ if cropped.size > 0:
+ return cropped
+
+ thumbnail_bytes = get_event_thumbnail_bytes(event)
+ if not thumbnail_bytes:
+ return None
+
+ nparr = np.frombuffer(thumbnail_bytes, np.uint8)
+ img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
+ if img is None or img.size == 0:
+ return None
+
+ height, width = img.shape[:2]
+ crop_size = 1.0
+
+ if event.data and "box" in event.data and "region" in event.data:
+ box = event.data["box"]
+ region = event.data["region"]
+
+ if len(box) == 4 and len(region) == 4:
+ box_w, box_h = box[2], box[3]
+ region_w, region_h = region[2], region[3]
+ box_area = (box_w * box_h) / (region_w * region_h)
+
+ if box_area < 0.05:
+ crop_size = 0.4
+ elif box_area < 0.10:
+ crop_size = 0.5
+ elif box_area < 0.20:
+ crop_size = 0.65
+ elif box_area < 0.35:
+ crop_size = 0.80
+ else:
+ crop_size = 0.95
+
+ crop_width = int(width * crop_size)
+ crop_height = int(height * crop_size)
+ x1 = (width - crop_width) // 2
+ y1 = (height - crop_height) // 2
+ cropped = img[y1 : y1 + crop_height, x1 : x1 + crop_width]
+ if cropped.size == 0:
+ return None
+
+ return cropped
diff --git a/frigate/util/config.py b/frigate/util/config.py
index 578ec18527..3e093a9783 100644
--- a/frigate/util/config.py
+++ b/frigate/util/config.py
@@ -4,11 +4,17 @@ import asyncio
import logging
import os
import shutil
-from typing import Any, Optional, Union
+from typing import Any
from ruamel.yaml import YAML
-from frigate.const import CONFIG_DIR, EXPORT_DIR
+from frigate.const import (
+ CONFIG_DIR,
+ DEFAULT_FFMPEG_VERSION,
+ EXPORT_DIR,
+ INCLUDED_FFMPEG_VERSIONS,
+ REDACTED_CREDENTIAL_SENTINEL,
+)
from frigate.util.builtin import deep_merge
from frigate.util.services import get_video_properties
@@ -18,6 +24,41 @@ CURRENT_CONFIG_VERSION = "0.18-0"
DEFAULT_CONFIG_FILE = os.path.join(CONFIG_DIR, "config.yml")
+def resolve_ffmpeg_path(path: str, binary: str = "ffmpeg") -> str:
+ """Resolve an ffmpeg version alias or custom path to a binary path.
+
+ A bare version alias that is no longer bundled (for example one that was
+ dropped when the default version changed) falls back to the default
+ bundled version so existing configs keep working across an upgrade or a
+ revert. Custom install paths (anything absolute) are used as-is.
+ """
+ if path == "default" or (
+ not path.startswith("/") and path not in INCLUDED_FFMPEG_VERSIONS
+ ):
+ version = DEFAULT_FFMPEG_VERSION
+ elif path in INCLUDED_FFMPEG_VERSIONS:
+ version = path
+ else:
+ return f"{path}/bin/{binary}"
+
+ return f"/usr/lib/ffmpeg/{version}/bin/{binary}"
+
+
+def redact_credential(obj: dict[str, Any], key: str) -> None:
+ """Replace obj[key] with the redaction sentinel if a value is saved, else drop.
+
+ Used when shaping the /config response so saved credentials never leave
+ the server. The frontend recognizes REDACTED_CREDENTIAL_SENTINEL, renders
+ the field as empty with a "saved — leave blank to keep" placeholder, and
+ /config/set strips it from any incoming payload so the YAML value is
+ preserved when the user doesn't touch the field.
+ """
+ if obj.get(key):
+ obj[key] = REDACTED_CREDENTIAL_SENTINEL
+ else:
+ obj.pop(key, None)
+
+
def find_config_file() -> str:
config_path = os.environ.get("CONFIG_FILE", DEFAULT_CONFIG_FILE)
@@ -37,7 +78,7 @@ def migrate_frigate_config(config_file: str):
yaml = YAML()
yaml.indent(mapping=2, sequence=4, offset=2)
- with open(config_file, "r") as f:
+ with open(config_file) as f:
config: dict[str, dict[str, Any]] = yaml.load(f)
if config is None:
@@ -436,7 +477,7 @@ def migrate_017_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]
def _convert_legacy_mask_to_dict(
- mask: Optional[Union[str, list]], mask_type: str = "motion_mask", label: str = ""
+ mask: str | list | None, mask_type: str = "motion_mask", label: str = ""
) -> dict[str, dict[str, Any]]:
"""Convert legacy mask format (str or list[str]) to new dict format.
@@ -492,7 +533,7 @@ def migrate_018_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]
genai = new_config.get("genai")
if genai and genai.get("provider"):
- genai["roles"] = ["embeddings", "vision", "tools"]
+ genai["roles"] = ["embeddings", "descriptions", "chat"]
new_config["genai"] = {"default": genai}
# Remove deprecated sync_recordings from global record config
@@ -603,16 +644,29 @@ def migrate_018_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]
new_config["cameras"][name] = camera_config
+ # Remove deprecated date_style and time_style from global ui config
+ global_ui = new_config.get("ui", {})
+ if global_ui.get("date_style") is not None:
+ del new_config["ui"]["date_style"]
+ if global_ui.get("time_style") is not None:
+ del new_config["ui"]["time_style"]
+ # Remove ui section if empty
+ if "ui" in new_config and not new_config["ui"]:
+ del new_config["ui"]
+
new_config["version"] = "0.18-0"
return new_config
def get_relative_coordinates(
- mask: Optional[Union[str, list]], frame_shape: tuple[int, int]
-) -> Union[str, list]:
+ mask: str | list | None,
+ frame_shape: tuple[int, int],
+ camera_name: str = "",
+) -> str | list:
# masks and zones are saved as relative coordinates
# we know if any points are > 1 then it is using the
# old native resolution coordinates
+ where = f" for camera {camera_name}" if camera_name else ""
if mask:
if isinstance(mask, list) and any(x > "1.0" for x in mask[0].split(",")):
relative_masks = []
@@ -627,7 +681,7 @@ def get_relative_coordinates(
if x > frame_shape[1] or y > frame_shape[0]:
logger.error(
- f"Not applying mask due to invalid coordinates. {x},{y} is outside of the detection resolution {frame_shape[1]}x{frame_shape[0]}. Use the editor in the UI to correct the mask."
+ f"Not applying mask due to invalid coordinates{where}. {x},{y} is outside of the detection resolution {frame_shape[1]}x{frame_shape[0]}. Use the editor in the UI to correct the mask."
)
continue
@@ -650,7 +704,7 @@ def get_relative_coordinates(
if x > frame_shape[1] or y > frame_shape[0]:
logger.error(
- f"Not applying mask due to invalid coordinates. {x},{y} is outside of the detection resolution {frame_shape[1]}x{frame_shape[0]}. Use the editor in the UI to correct the mask."
+ f"Not applying mask due to invalid coordinates{where}. {x},{y} is outside of the detection resolution {frame_shape[1]}x{frame_shape[0]}. Use the editor in the UI to correct the mask."
)
return []
@@ -666,7 +720,7 @@ def get_relative_coordinates(
def convert_area_to_pixels(
- area_value: Union[int, float], frame_shape: tuple[int, int]
+ area_value: int | float, frame_shape: tuple[int, int]
) -> int:
"""
Convert area specification to pixels.
@@ -708,7 +762,7 @@ class StreamInfoRetriever:
return info
-def apply_section_update(camera_config, section: str, update: dict) -> Optional[str]:
+def apply_section_update(camera_config, section: str, update: dict) -> str | None:
"""Merge an update dict into a camera config section and rebuild runtime variants.
For motion and object filter sections, the plain Pydantic models are rebuilt
@@ -770,6 +824,45 @@ def apply_section_update(camera_config, section: str, update: dict) -> Optional[
)
camera_config.objects = new_objects
+ elif section == "detect":
+ # apply detect first so frame_shape reflects the new resolution
+ # before we rebuild mask-dependent runtime configs below
+ merged = deep_merge(current.model_dump(), update, override=True)
+ camera_config.detect = current.__class__.model_validate(merged)
+
+ new_frame_shape = camera_config.frame_shape
+
+ # rebuild motion's rasterized_mask at the new frame_shape
+ if camera_config.motion is not None:
+ camera_config.motion = RuntimeMotionConfig(
+ frame_shape=new_frame_shape,
+ **camera_config.motion.model_dump(exclude_unset=True),
+ )
+
+ # rebuild per-object filter masks at the new frame_shape
+ for obj_name, filt in camera_config.objects.filters.items():
+ merged_mask = dict(filt.mask)
+ if camera_config.objects.mask:
+ for gid, gmask in camera_config.objects.mask.items():
+ merged_mask[f"global_{gid}"] = gmask
+
+ camera_config.objects.filters[obj_name] = RuntimeFilterConfig(
+ frame_shape=new_frame_shape,
+ mask=merged_mask,
+ **filt.model_dump(exclude_unset=True, exclude={"mask", "raw_mask"}),
+ )
+
+ # Regenerate zone contours and per-zone filter masks at the new
+ # frame_shape so zone outlines and membership stay relative
+ for zone in camera_config.zones.values():
+ if zone.filters:
+ for zone_obj_name, zone_filter in zone.filters.items():
+ zone.filters[zone_obj_name] = RuntimeFilterConfig(
+ frame_shape=new_frame_shape,
+ **zone_filter.model_dump(exclude_unset=True),
+ )
+ zone.generate_contour(new_frame_shape)
+
else:
merged = deep_merge(current.model_dump(), update, override=True)
setattr(camera_config, section, current.__class__.model_validate(merged))
diff --git a/frigate/util/downloader.py b/frigate/util/downloader.py
index ee80b38165..a8b593f159 100644
--- a/frigate/util/downloader.py
+++ b/frigate/util/downloader.py
@@ -1,8 +1,8 @@
import logging
import os
import threading
+from collections.abc import Callable
from pathlib import Path
-from typing import Callable, List
import requests
@@ -19,7 +19,7 @@ class ModelDownloader:
self,
model_name: str,
download_path: str,
- file_names: List[str],
+ file_names: list[str],
download_func: Callable[[str], None],
complete_func: Callable[[], None] | None = None,
silent: bool = False,
diff --git a/frigate/util/ffmpeg.py b/frigate/util/ffmpeg.py
index 9abacd4ed6..87601b91d6 100644
--- a/frigate/util/ffmpeg.py
+++ b/frigate/util/ffmpeg.py
@@ -2,8 +2,10 @@
import logging
import subprocess as sp
+from collections.abc import Callable
from typing import Any
+from frigate.const import PROCESS_PRIORITY_LOW
from frigate.log import LogPipe
@@ -46,3 +48,124 @@ def start_or_restart_ffmpeg(
start_new_session=True,
)
return process
+
+
+logger = logging.getLogger(__name__)
+
+
+def inject_progress_flags(cmd: list[str]) -> list[str]:
+ """Insert `-progress pipe:2 -nostats` immediately before the output path.
+
+ `-progress pipe:2` writes structured key=value lines to stderr;
+ `-nostats` suppresses the noisy default stats output. The output path
+ is conventionally the last token in an FFmpeg argv.
+ """
+ if not cmd:
+ return cmd
+ return cmd[:-1] + ["-progress", "pipe:2", "-nostats", cmd[-1]]
+
+
+def run_ffmpeg_with_progress(
+ cmd: list[str],
+ *,
+ expected_duration_seconds: float,
+ on_progress: Callable[[float], None] | None = None,
+ stdin_payload: str | None = None,
+ process_started: Callable[[sp.Popen], None] | None = None,
+ use_low_priority: bool = True,
+) -> tuple[int, str]:
+ """Run an ffmpeg command, streaming progress via `-progress pipe:2`.
+
+ Args:
+ cmd: ffmpeg argv. Output path must be the last token.
+ expected_duration_seconds: Duration of the expected output clip in
+ seconds. Used to convert ffmpeg's `out_time_us` into a percent.
+ on_progress: Optional callback invoked with a percent in [0, 100].
+ Called once with 0.0 at start, again on each `out_time_us=`
+ stderr line, and once with 100.0 on `progress=end`.
+ stdin_payload: Optional string written to ffmpeg stdin (used by
+ export for concat playlists).
+ process_started: Optional callback invoked with the live `Popen`
+ once spawned — lets callers store the ref for cancellation.
+ use_low_priority: When True, prepend `nice -n PROCESS_PRIORITY_LOW`
+ so concat doesn't starve detection.
+
+ Returns:
+ Tuple of `(returncode, captured_stderr)`. Stdout is left attached
+ to the parent process to avoid buffer-full deadlocks.
+ """
+ full_cmd = inject_progress_flags(cmd)
+ if use_low_priority:
+ full_cmd = ["nice", "-n", str(PROCESS_PRIORITY_LOW)] + full_cmd
+
+ def emit(percent: float) -> None:
+ if on_progress is None:
+ return
+ try:
+ on_progress(max(0.0, min(100.0, percent)))
+ except Exception:
+ logger.exception("FFmpeg progress callback failed")
+
+ emit(0.0)
+
+ proc = sp.Popen(
+ full_cmd,
+ stdin=sp.PIPE if stdin_payload is not None else None,
+ stderr=sp.PIPE,
+ text=True,
+ encoding="ascii",
+ errors="replace",
+ )
+ if process_started is not None:
+ try:
+ process_started(proc)
+ except Exception:
+ logger.exception("FFmpeg process_started callback failed")
+
+ if stdin_payload is not None and proc.stdin is not None:
+ try:
+ proc.stdin.write(stdin_payload)
+ except (BrokenPipeError, OSError):
+ pass
+ finally:
+ try:
+ proc.stdin.close()
+ except (BrokenPipeError, OSError):
+ pass
+
+ captured: list[str] = []
+ if proc.stderr is not None:
+ try:
+ for raw_line in proc.stderr:
+ captured.append(raw_line)
+ line = raw_line.strip()
+ if not line:
+ continue
+ if line.startswith("out_time_us="):
+ if expected_duration_seconds <= 0:
+ continue
+ try:
+ out_time_us = int(line.split("=", 1)[1])
+ except (ValueError, IndexError):
+ continue
+ if out_time_us < 0:
+ continue
+ out_seconds = out_time_us / 1_000_000.0
+ emit((out_seconds / expected_duration_seconds) * 100.0)
+ elif line == "progress=end":
+ emit(100.0)
+ break
+ except Exception:
+ logger.exception("Failed reading FFmpeg progress stream")
+
+ proc.wait()
+
+ if proc.stderr is not None:
+ try:
+ remaining = proc.stderr.read()
+ if remaining:
+ captured.append(remaining)
+ except Exception:
+ pass
+
+ return proc.returncode or 0, "".join(captured)
diff --git a/frigate/util/file.py b/frigate/util/file.py
index f858dca68e..e259d13456 100644
--- a/frigate/util/file.py
+++ b/frigate/util/file.py
@@ -7,7 +7,7 @@ import os
import time
from datetime import datetime
from pathlib import Path
-from typing import Any, Optional
+from typing import Any
import cv2
from numpy import ndarray
@@ -323,7 +323,7 @@ class FileLock:
self.timeout = timeout
self.poll_interval = poll_interval
self.stale_timeout = stale_timeout
- self._fd: Optional[int] = None
+ self._fd: int | None = None
self._acquired = False
if cleanup_stale_on_init:
@@ -367,7 +367,7 @@ class FileLock:
return False
- def acquire(self, timeout: Optional[int] = None) -> bool:
+ def acquire(self, timeout: int | None = None) -> bool:
"""
Acquire the file lock using fcntl.flock().
@@ -400,7 +400,7 @@ class FileLock:
self._acquired = True
logger.debug(f"Acquired lock: {self.lock_path}")
return True
- except (OSError, IOError):
+ except OSError:
# Lock is held by another process
if time.time() - start_time >= timeout:
logger.warning(f"Timeout waiting for lock: {self.lock_path}")
diff --git a/frigate/util/image.py b/frigate/util/image.py
index 2d2133c6b8..dde25e2425 100644
--- a/frigate/util/image.py
+++ b/frigate/util/image.py
@@ -8,7 +8,7 @@ from abc import ABC, abstractmethod
from multiprocessing import resource_tracker as _mprt
from multiprocessing import shared_memory as _mpshm
from string import printable
-from typing import Any, AnyStr, Optional
+from typing import Any, AnyStr
import cv2
import numpy as np
@@ -270,7 +270,7 @@ def draw_box_with_label(
)
-def get_image_quality_params(ext: str, quality: Optional[int]) -> list[int]:
+def get_image_quality_params(ext: str, quality: int | None) -> list[int]:
if ext in ("jpg", "jpeg"):
return [int(cv2.IMWRITE_JPEG_QUALITY), quality if quality is not None else 70]
@@ -921,7 +921,7 @@ def yuv_region_2_bgr(frame, region):
raise
-def intersection(box_a, box_b) -> Optional[list[int]]:
+def intersection(box_a, box_b) -> list[int] | None:
"""Return intersection box or None if boxes do not intersect."""
if (
box_a[2] < box_b[0]
@@ -994,7 +994,7 @@ class FrameManager(ABC):
pass
@abstractmethod
- def write(self, name: str) -> Optional[memoryview]:
+ def write(self, name: str) -> memoryview | None:
pass
@abstractmethod
@@ -1021,7 +1021,7 @@ class UntrackedSharedMemory(_mpshm.SharedMemory):
def __init__(
self,
- name: Optional[str] = None,
+ name: str | None = None,
create: bool = False,
size: int = 0,
*,
@@ -1075,7 +1075,7 @@ class SharedMemoryFrameManager(FrameManager):
self.shm_store[name] = shm
return shm.buf
- def write(self, name: str) -> Optional[memoryview]:
+ def write(self, name: str) -> memoryview | None:
try:
if name in self.shm_store:
shm = self.shm_store[name]
@@ -1087,12 +1087,27 @@ class SharedMemoryFrameManager(FrameManager):
logger.info(f"the file {name} not found")
return None
- def get(self, name: str, shape) -> Optional[np.ndarray]:
+ def get(self, name: str, shape) -> np.ndarray | None:
try:
- if name in self.shm_store:
- shm = self.shm_store[name]
- else:
+ required = int(np.prod(shape))
+ shm = self.shm_store.get(name)
+ if shm is not None and shm.size != required:
+ # stale cached ref from a same-name recreate — drop and reopen
+ try:
+ shm.close()
+ except Exception:
+ pass
+ self.shm_store.pop(name, None)
+ shm = None
+ if shm is None:
shm = UntrackedSharedMemory(name=name)
+ if shm.size != required:
+ # mid-recreate: OS segment doesn't match shape yet; skip
+ try:
+ shm.close()
+ except Exception:
+ pass
+ return None
self.shm_store[name] = shm
return np.ndarray(shape, dtype=np.uint8, buffer=shm.buf)
except FileNotFoundError:
@@ -1170,10 +1185,10 @@ def run_ffmpeg_snapshot(
ffmpeg,
input_path: str,
codec: str,
- seek_time: Optional[float] = None,
- height: Optional[int] = None,
- timeout: Optional[int] = None,
-) -> tuple[Optional[bytes], str]:
+ seek_time: float | None = None,
+ height: int | None = None,
+ timeout: int | None = None,
+) -> tuple[bytes | None, str]:
"""Run ffmpeg to extract a snapshot/image from a video source."""
ffmpeg_cmd = [
ffmpeg.ffmpeg_path,
@@ -1223,8 +1238,8 @@ def get_image_from_recording(
file_path: str,
relative_frame_time: float,
codec: str,
- height: Optional[int] = None,
-) -> Optional[Any]:
+ height: int | None = None,
+) -> Any | None:
"""retrieve a frame from given time in recording file."""
image_data, _ = run_ffmpeg_snapshot(
@@ -1246,7 +1261,7 @@ def get_histogram(image, x_min, y_min, x_max, y_max):
def create_thumbnail(
yuv_frame: np.ndarray, box: tuple[int, int, int, int], height=500
-) -> Optional[bytes]:
+) -> bytes | None:
"""Return jpg thumbnail of a region of the frame."""
frame = cv2.cvtColor(yuv_frame, cv2.COLOR_YUV2BGR_I420)
region = calculate_region(
diff --git a/frigate/util/media.py b/frigate/util/media.py
index 38f5698067..e4e84e9149 100644
--- a/frigate/util/media.py
+++ b/frigate/util/media.py
@@ -5,9 +5,9 @@ import errno
import logging
import os
import subprocess as sp
+from collections.abc import Iterable
from dataclasses import dataclass, field
from pathlib import Path
-from typing import Iterable
from peewee import DatabaseError, chunked
@@ -94,7 +94,7 @@ def remove_empty_directories(root: Path, paths: Iterable[Path]) -> None:
paths = parents
- logger.debug("Removed {count} empty directories")
+ logger.debug(f"Removed {count} empty directories")
def sync_recordings(
@@ -110,7 +110,7 @@ def sync_recordings(
# start checking on the hour 36 hours ago
check_point = datetime.datetime.now().replace(
minute=0, second=0, microsecond=0
- ).astimezone(datetime.timezone.utc) - datetime.timedelta(hours=36)
+ ).astimezone(datetime.UTC) - datetime.timedelta(hours=36)
# Gather DB recordings to inspect
if limited:
@@ -793,7 +793,7 @@ def write_orphan_report(
f.write("# Media Sync Orphan Report\n")
f.write(f"# Job: {job_id}\n")
f.write(
- f"# Date: {datetime.datetime.now().astimezone(datetime.timezone.utc).isoformat()}\n"
+ f"# Date: {datetime.datetime.now().astimezone(datetime.UTC).isoformat()}\n"
)
f.write(f"# Mode: dry_run={dry_run}\n\n")
diff --git a/frigate/util/object.py b/frigate/util/object.py
index 7c7edc10c5..b4ee72dc07 100644
--- a/frigate/util/object.py
+++ b/frigate/util/object.py
@@ -339,18 +339,13 @@ def reduce_boxes(boxes, iou_threshold=0.0):
def average_boxes(boxes: list[list[int, int, int, int]]) -> list[int, int, int, int]:
"""Return a box that is the average of a list of boxes."""
- x_mins = []
- y_mins = []
- x_max = []
- y_max = []
-
- for box in boxes:
- x_mins.append(box[0])
- y_mins.append(box[1])
- x_max.append(box[2])
- y_max.append(box[3])
-
- return [np.mean(x_mins), np.mean(y_mins), np.mean(x_max), np.mean(y_max)]
+ n = len(boxes)
+ return [
+ sum(box[0] for box in boxes) / n,
+ sum(box[1] for box in boxes) / n,
+ sum(box[2] for box in boxes) / n,
+ sum(box[3] for box in boxes) / n,
+ ]
def median_of_boxes(boxes: list[list[int, int, int, int]]) -> list[int, int, int, int]:
@@ -401,13 +396,13 @@ def get_cluster_candidates(frame_shape, min_region, boxes):
# determined by the max_region size minus half the box + 20%
# TODO: see if we can do this with numpy
cluster_candidates = []
- used_boxes = []
+ used_boxes = set()
# loop over each box
for current_index, b in enumerate(boxes):
if current_index in used_boxes:
continue
cluster = [current_index]
- used_boxes.append(current_index)
+ used_boxes.add(current_index)
cluster_boundary = get_cluster_boundary(b, min_region)
# find all other boxes that fit inside the boundary
for compare_index, compare_box in enumerate(boxes):
@@ -436,7 +431,7 @@ def get_cluster_candidates(frame_shape, min_region, boxes):
if should_cluster:
cluster.append(compare_index)
- used_boxes.append(compare_index)
+ used_boxes.add(compare_index)
cluster_candidates.append(cluster)
# return the unique clusters only
@@ -558,6 +553,7 @@ def reduce_detections(
current_detection = sorted_by_area[current_detection_idx]
current_label = current_detection[0]
current_box = current_detection[2]
+ current_area = area(current_box)
overlap = 0
for to_check_idx in range(
min(current_detection_idx + 1, len(sorted_by_area)),
@@ -568,14 +564,14 @@ def reduce_detections(
# if area of current detection / area of check < 5% they should not be compared
# this covers cases where a large car parked in a driveway doesn't block detections
# of cars in the street behind it
- if area(current_box) / area(to_check) < 0.05:
+ if current_area / area(to_check) < 0.05:
continue
intersect_box = intersection(current_box, to_check)
# if % of smaller detection is inside of another detection, consolidate
- if intersect_box is not None and area(intersect_box) / area(
- current_box
- ) > LABEL_CONSOLIDATION_MAP.get(
+ if intersect_box is not None and area(
+ intersect_box
+ ) / current_area > LABEL_CONSOLIDATION_MAP.get(
current_label, LABEL_CONSOLIDATION_DEFAULT
):
overlap = 1
diff --git a/frigate/util/process.py b/frigate/util/process.py
index 1613c1e431..060c39c7b1 100644
--- a/frigate/util/process.py
+++ b/frigate/util/process.py
@@ -6,9 +6,9 @@ import os
import pathlib
import subprocess
import threading
+from collections.abc import Callable
from logging.handlers import QueueHandler
from multiprocessing.synchronize import Event as MpEvent
-from typing import Callable, Optional
from setproctitle import setproctitle
@@ -23,11 +23,11 @@ class BaseProcess(mp.Process):
stop_event: MpEvent,
priority: int,
*,
- name: Optional[str] = None,
- target: Optional[Callable] = None,
+ name: str | None = None,
+ target: Callable | None = None,
args: tuple = (),
kwargs: dict = {},
- daemon: Optional[bool] = None,
+ daemon: bool | None = None,
):
self.priority = priority
self.stop_event = stop_event
@@ -121,7 +121,7 @@ class FrigateProcess(BaseProcess):
f"If process crashes, manually generate with: memray flamegraph {binary_file}"
)
except Exception as e:
- self.logger.error(f"Failed to setup memray profiling: {e}", exc_info=True)
+ self.logger.exception(f"Failed to setup memray profiling: {e}")
def _cleanup_memray(self, safe_name: str, binary_file: pathlib.Path) -> None:
"""Stop memray tracking and generate HTML report."""
@@ -156,4 +156,4 @@ class FrigateProcess(BaseProcess):
except subprocess.TimeoutExpired:
self.logger.error("Memray report generation timed out")
except Exception as e:
- self.logger.error(f"Failed to cleanup memray profiling: {e}", exc_info=True)
+ self.logger.exception(f"Failed to cleanup memray profiling: {e}")
diff --git a/frigate/util/rknn_converter.py b/frigate/util/rknn_converter.py
index 5660c7601d..ad387413d4 100644
--- a/frigate/util/rknn_converter.py
+++ b/frigate/util/rknn_converter.py
@@ -6,7 +6,6 @@ import subprocess
import sys
import time
from pathlib import Path
-from typing import Optional
from frigate.const import SUPPORTED_RK_SOCS
from frigate.util.file import FileLock
@@ -139,7 +138,7 @@ def ensure_rknn_toolkit() -> bool:
return False
-def get_soc_type() -> Optional[str]:
+def get_soc_type() -> str | None:
"""Get the SoC type from device tree."""
try:
with open("/proc/device-tree/compatible") as file:
@@ -160,7 +159,7 @@ def convert_onnx_to_rknn(
output_path: str,
model_type: str,
quantization: bool = False,
- soc: Optional[str] = None,
+ soc: str | None = None,
) -> bool:
"""
Convert ONNX model to RKNN format.
@@ -345,7 +344,7 @@ def wait_for_conversion_completion(
def auto_convert_model(
model_path: str, model_type: str | None = None, quantization: bool = False
-) -> Optional[str]:
+) -> str | None:
"""
Automatically convert a model to RKNN format if needed.
diff --git a/frigate/util/schema.py b/frigate/util/schema.py
index 5ba1bc0619..9af651ea07 100644
--- a/frigate/util/schema.py
+++ b/frigate/util/schema.py
@@ -1,11 +1,11 @@
"""JSON schema utilities for Frigate."""
-from typing import Any, Dict, Type
+from typing import Any
from pydantic import BaseModel, TypeAdapter
-def get_config_schema(config_class: Type[BaseModel]) -> Dict[str, Any]:
+def get_config_schema(config_class: type[BaseModel]) -> dict[str, Any]:
"""
Returns the JSON schema for FrigateConfig with polymorphic detectors.
diff --git a/frigate/util/services.py b/frigate/util/services.py
index f0bf2de1ed..62929d32df 100644
--- a/frigate/util/services.py
+++ b/frigate/util/services.py
@@ -12,7 +12,7 @@ import subprocess as sp
import time
import traceback
from datetime import datetime
-from typing import Any, List, Optional, Tuple
+from typing import Any
import cv2
import psutil
@@ -59,7 +59,7 @@ def get_cgroups_version() -> str:
return "unknown"
try:
- with open("/proc/mounts", "r") as f:
+ with open("/proc/mounts") as f:
mounts = f.readlines()
for mount in mounts:
@@ -89,7 +89,7 @@ def get_docker_memlimit_bytes() -> int:
memlimit_path = "/sys/fs/cgroup/memory.max"
try:
- with open(memlimit_path, "r") as f:
+ with open(memlimit_path) as f:
value = f.read().strip()
if value.isnumeric():
@@ -127,7 +127,7 @@ def get_cpu_stats() -> dict[str, dict]:
if not any(keyword in cmdline for keyword in keywords):
continue
- with open(f"/proc/{pid}/stat", "r") as f:
+ with open(f"/proc/{pid}/stat") as f:
stats = f.readline().split()
utime = int(stats[13])
stime = int(stats[14])
@@ -146,7 +146,7 @@ def get_cpu_stats() -> dict[str, dict]:
process_usage_sec = process_utime_sec + process_stime_sec
cpu_average_usage = process_usage_sec * 100 // process_elapsed_sec
- with open(f"/proc/{pid}/statm", "r") as f:
+ with open(f"/proc/{pid}/statm") as f:
mem_stats = f.readline().split()
mem_res = int(mem_stats[1]) * os.sysconf("SC_PAGE_SIZE") / 1024
@@ -171,7 +171,7 @@ def get_physical_interfaces(interfaces) -> list:
if not interfaces:
return []
- with open("/proc/net/dev", "r") as file:
+ with open("/proc/net/dev") as file:
lines = file.readlines()
physical_interfaces = []
@@ -238,7 +238,7 @@ def is_vaapi_amd_driver() -> bool:
return any("AMD Radeon Graphics" in line for line in output)
-def get_amd_gpu_stats() -> Optional[dict[str, str]]:
+def get_amd_gpu_stats() -> dict[str, str] | None:
"""Get stats using radeontop."""
radeontop_command = ["radeontop", "-d", "-", "-l", "1"]
@@ -264,164 +264,276 @@ def get_amd_gpu_stats() -> Optional[dict[str, str]]:
return results
-def get_intel_gpu_stats(intel_gpu_device: Optional[str]) -> Optional[dict[str, str]]:
- """Get stats using intel_gpu_top.
+_INTEL_FDINFO_SAMPLE_SECONDS = 1.0
- Returns overall GPU usage derived from rc6 residency (idle time),
- plus individual engine breakdowns:
- - enc: Render/3D engine (compute/shader encoder, used by QSV)
- - dec: Video engines (fixed-function codec, used by VAAPI)
+# Engines we track. Render/3D and Compute are pooled into "compute"; Video and
+# VideoEnhance into "dec" (VideoEnhance is the post-process engine that handles
+# VAAPI scaling/deinterlace/CSC, e.g. ffmpeg `-vf scale_vaapi=...`). The Copy
+# (DMA blitter) engine is intentionally ignored — it represents transparent
+# memory transfers, not user-visible GPU work.
+# i915 fdinfo keys (cumulative ns) → logical engine name.
+_I915_ENGINE_KEYS = {
+ "drm-engine-render": "render",
+ "drm-engine-video": "video",
+ "drm-engine-video-enhance": "video-enhance",
+ "drm-engine-compute": "compute",
+}
+# Xe fdinfo suffixes (cumulative cycles, paired with drm-total-cycles-*).
+_XE_ENGINE_KEYS = {
+ "rcs": "render",
+ "vcs": "video",
+ "vecs": "video-enhance",
+ "ccs": "compute",
+}
+
+
+def _resolve_intel_gpu_pdev(device: str | None) -> str | None:
+ """Map a configured GPU hint (/dev/dri/card1, renderD128, or a PCI bus
+ address) to its drm-pdev string so we can filter fdinfo entries to that
+ device. Returns None when no hint is supplied or it cannot be resolved."""
+ if not device:
+ return None
+
+ if re.match(r"^[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9a-fA-F]$", device):
+ return device
+
+ name = os.path.basename(device.rstrip("/"))
+ try:
+ return os.path.basename(os.path.realpath(f"/sys/class/drm/{name}/device"))
+ except OSError:
+ return None
+
+
+def _read_intel_drm_fdinfo(target_pdev: str | None) -> dict:
+ """Snapshot DRM fdinfo for every Intel client visible in /proc.
+
+ Returns a dict keyed by (pdev, drm-client-id, pid) so the same context
+ seen via multiple file descriptors on a single process collapses to one
+ entry.
"""
-
- def get_stats_manually(output: str) -> dict[str, str]:
- """Find global stats via regex when json fails to parse."""
- reading = "".join(output)
- results: dict[str, str] = {}
-
- # rc6 residency for overall GPU usage
- rc6_match = re.search(r'"rc6":\{"value":([\d.]+)', reading)
- if rc6_match:
- rc6_value = float(rc6_match.group(1))
- results["gpu"] = f"{round(100.0 - rc6_value, 2)}%"
- else:
- results["gpu"] = "-%"
-
- results["mem"] = "-%"
-
- # Render/3D is the compute/encode engine
- render = []
- for result in re.findall(r'"Render/3D/0":{[a-z":\d.,%]+}', reading):
- packet = json.loads(result[14:])
- single = packet.get("busy", 0.0)
- render.append(float(single))
-
- if render:
- results["compute"] = f"{round(sum(render) / len(render), 2)}%"
-
- # Video engines are the fixed-function decode engines
- video = []
- for result in re.findall(r'"Video/\d":{[a-z":\d.,%]+}', reading):
- packet = json.loads(result[10:])
- single = packet.get("busy", 0.0)
- video.append(float(single))
-
- if video:
- results["dec"] = f"{round(sum(video) / len(video), 2)}%"
-
- return results
-
- intel_gpu_top_command = [
- "timeout",
- "0.5s",
- "intel_gpu_top",
- "-J",
- "-o",
- "-",
- "-s",
- "1000", # Intel changed this from seconds to milliseconds in 2024+ versions
- ]
-
- if intel_gpu_device:
- intel_gpu_top_command += ["-d", intel_gpu_device]
+ snapshot: dict = {}
try:
- p = sp.run(
- intel_gpu_top_command,
- encoding="ascii",
- capture_output=True,
- )
- except UnicodeDecodeError:
- return None
+ proc_entries = os.listdir("/proc")
+ except OSError:
+ return snapshot
- # timeout has a non-zero returncode when timeout is reached
- if p.returncode != 124:
- logger.error(f"Unable to poll intel GPU stats: {p.stderr}")
- return None
- else:
- output = "".join(p.stdout.split())
+ for entry in proc_entries:
+ if not entry.isdigit():
+ continue
+ fdinfo_dir = f"/proc/{entry}/fdinfo"
try:
- data = json.loads(f"[{output}]")
- except json.JSONDecodeError:
- return get_stats_manually(output)
+ fds = os.listdir(fdinfo_dir)
+ except (FileNotFoundError, PermissionError, NotADirectoryError, OSError):
+ continue
- results: dict[str, str] = {}
- rc6_values = []
- render_global = []
- video_global = []
- # per-client: {pid: [total_busy_per_sample, ...]}
- client_usages: dict[str, list[float]] = {}
+ for fd in fds:
+ try:
+ with open(f"{fdinfo_dir}/{fd}") as f:
+ content = f.read()
+ except (FileNotFoundError, PermissionError, OSError):
+ continue
- for block in data:
- # rc6 residency: percentage of time GPU is idle
- rc6 = block.get("rc6", {}).get("value")
- if rc6 is not None:
- rc6_values.append(float(rc6))
+ if "drm-driver" not in content:
+ continue
- global_engine = block.get("engines")
+ fields: dict[str, str] = {}
+ for line in content.splitlines():
+ key, sep, value = line.partition(":")
+ if sep:
+ fields[key.strip()] = value.strip()
- if global_engine:
- render_frame = global_engine.get("Render/3D/0", {}).get("busy")
- video_frame = global_engine.get("Video/0", {}).get("busy")
+ driver = fields.get("drm-driver")
+ if driver not in ("i915", "xe"):
+ continue
- if render_frame is not None:
- render_global.append(float(render_frame))
+ pdev = fields.get("drm-pdev", "")
+ if target_pdev and pdev != target_pdev:
+ continue
- if video_frame is not None:
- video_global.append(float(video_frame))
+ client_id = fields.get("drm-client-id")
+ if not client_id:
+ continue
- clients = block.get("clients", {})
+ key = (pdev, client_id, entry)
+ if key in snapshot:
+ continue
- if clients:
- for client_block in clients.values():
- pid = client_block["pid"]
+ engines: dict[str, tuple[int, int, int]] = {}
- if pid not in client_usages:
- client_usages[pid] = []
+ if driver == "i915":
+ for fkey, engine in _I915_ENGINE_KEYS.items():
+ raw = fields.get(fkey)
+ if not raw:
+ continue
+ try:
+ engines[engine] = (int(raw.split()[0]), 0, 1)
+ except (ValueError, IndexError):
+ continue
+ else:
+ for suffix, engine in _XE_ENGINE_KEYS.items():
+ busy_raw = fields.get(f"drm-cycles-{suffix}")
+ total_raw = fields.get(f"drm-total-cycles-{suffix}")
- # Sum all engine-class busy values for this client
- total_busy = 0.0
- for engine in client_block.get("engine-classes", {}).values():
- busy = engine.get("busy")
- if busy is not None:
- total_busy += float(busy)
+ if not (busy_raw and total_raw):
+ continue
- client_usages[pid].append(total_busy)
+ # drm-cycles-* is summed across every instance of the engine
+ # class while drm-total-cycles-* tracks a single instance, so
+ # busy/total scales up to the capacity (e.g. Battlemage
+ # reports 2 for vcs/vecs). Capture it to divide back out;
+ # absent means a single engine, so default to 1.
+ capacity_raw = fields.get(f"drm-engine-capacity-{suffix}")
- # Overall GPU usage from rc6 (idle) residency
- if rc6_values:
- rc6_avg = sum(rc6_values) / len(rc6_values)
- results["gpu"] = f"{round(100.0 - rc6_avg, 2)}%"
+ try:
+ capacity = int(capacity_raw.split()[0]) if capacity_raw else 1
+ except (ValueError, IndexError):
+ capacity = 1
- results["mem"] = "-%"
+ try:
+ engines[engine] = (
+ int(busy_raw.split()[0]),
+ int(total_raw.split()[0]),
+ max(1, capacity),
+ )
+ except (ValueError, IndexError):
+ continue
- # Compute: Render/3D engine (compute/shader workloads and QSV encode)
- if render_global:
- results["compute"] = f"{round(sum(render_global) / len(render_global), 2)}%"
+ if not engines:
+ continue
- # Decoder: Video engine (fixed-function codec)
- if video_global:
- results["dec"] = f"{round(sum(video_global) / len(video_global), 2)}%"
+ snapshot[key] = {"driver": driver, "pid": entry, "engines": engines}
- # Per-client GPU usage (sum of all engines per process)
- if client_usages:
- results["clients"] = {}
-
- for pid, samples in client_usages.items():
- if samples:
- results["clients"][pid] = (
- f"{round(sum(samples) / len(samples), 2)}%"
- )
-
- return results
+ return snapshot
-def get_openvino_npu_stats() -> Optional[dict[str, str]]:
+def get_intel_gpu_stats(
+ intel_gpu_device: str | None,
+) -> dict[str, dict[str, Any]] | None:
+ """Get stats by reading DRM fdinfo files, bucketed per-pdev.
+
+ Each DRM client FD exposes monotonic per-engine busy counters via
+ /proc//fdinfo/ (i915 since kernel 5.19, Xe since first release).
+ We sample twice and divide busy-time deltas by wall-clock to derive
+ utilization. Render/3D and Compute are pooled into "compute"; Video and
+ VideoEnhance into "dec". Overall "gpu" is the sum of those pools (clamped
+ to 100%).
+
+ The return value is keyed by the GPU's drm-pdev string so multiple Intel
+ GPUs in the same system are reported separately. Each entry carries a
+ "name" populated from OpenVINO (falling back to the pdev) so callers can
+ surface a real device name in the UI.
+ """
+ from frigate.stats.intel_gpu_info import intel_gpu_name_resolver
+
+ target_pdev = _resolve_intel_gpu_pdev(intel_gpu_device)
+
+ snapshot_a = _read_intel_drm_fdinfo(target_pdev)
+ if not snapshot_a:
+ logger.warning(
+ "Unable to collect Intel GPU stats: no DRM fdinfo entries found"
+ "%s. Check that /proc is readable and the i915/xe driver is loaded",
+ f" for pdev {target_pdev}" if target_pdev else "",
+ )
+ return None
+
+ start = time.monotonic()
+ time.sleep(_INTEL_FDINFO_SAMPLE_SECONDS)
+ elapsed_ns = (time.monotonic() - start) * 1e9
+
+ snapshot_b = _read_intel_drm_fdinfo(target_pdev)
+ if not snapshot_b or elapsed_ns <= 0:
+ logger.warning(
+ "Unable to collect Intel GPU stats: second DRM fdinfo sample was empty"
+ )
+ return None
+
+ def _new_engine_pct() -> dict[str, float]:
+ return {"render": 0.0, "video": 0.0, "video-enhance": 0.0, "compute": 0.0}
+
+ per_pdev_engine_pct: dict[str, dict[str, float]] = {}
+ per_pdev_pid_pct: dict[str, dict[str, float]] = {}
+
+ for key, data_b in snapshot_b.items():
+ data_a = snapshot_a.get(key)
+ if not data_a or data_a["driver"] != data_b["driver"]:
+ continue
+
+ pdev = key[0]
+ engine_pct = per_pdev_engine_pct.setdefault(pdev, _new_engine_pct())
+ pid_pct = per_pdev_pid_pct.setdefault(pdev, {})
+
+ client_total = 0.0
+ for engine, (busy_b, total_b, capacity) in data_b["engines"].items():
+ if engine not in engine_pct:
+ continue
+
+ busy_a, total_a, _ = data_a["engines"].get(
+ engine, (busy_b, total_b, capacity)
+ )
+
+ if data_b["driver"] == "i915":
+ delta = max(0, busy_b - busy_a)
+ pct = min(100.0, delta / elapsed_ns * 100.0)
+ else:
+ delta_busy = max(0, busy_b - busy_a)
+ delta_total = total_b - total_a
+ if delta_total <= 0:
+ continue
+ # Normalize by capacity so a class with N engine instances
+ # (busy summed across all N) reports 0-100%, not 0-N*100%.
+ pct = min(100.0, delta_busy / (delta_total * capacity) * 100.0)
+
+ engine_pct[engine] += pct
+ client_total += pct
+
+ pid_pct[data_b["pid"]] = pid_pct.get(data_b["pid"], 0.0) + client_total
+
+ if not per_pdev_engine_pct:
+ logger.warning(
+ "Unable to collect Intel GPU stats: no per-engine counters available "
+ "(i915 requires kernel >= 5.19)"
+ )
+ return None
+
+ names = intel_gpu_name_resolver.get_names()
+ results: dict[str, dict[str, Any]] = {}
+
+ for pdev, engine_pct in per_pdev_engine_pct.items():
+ for engine in engine_pct:
+ engine_pct[engine] = min(100.0, engine_pct[engine])
+
+ compute_pct = min(100.0, engine_pct["render"] + engine_pct["compute"])
+ dec_pct = min(100.0, engine_pct["video"] + engine_pct["video-enhance"])
+ overall_pct = min(100.0, compute_pct + dec_pct)
+
+ entry: dict[str, Any] = {
+ "name": names.get(pdev) or "Intel iGPU",
+ "vendor": "intel",
+ "gpu": f"{round(overall_pct, 2)}%",
+ "mem": "-%",
+ "compute": f"{round(compute_pct, 2)}%",
+ "dec": f"{round(dec_pct, 2)}%",
+ }
+
+ pid_pct = per_pdev_pid_pct.get(pdev)
+ if pid_pct:
+ entry["clients"] = {
+ pid: f"{round(min(100.0, pct), 2)}%" for pid, pct in pid_pct.items()
+ }
+
+ results[pdev] = entry
+
+ return results
+
+
+def get_openvino_npu_stats() -> dict[str, str] | None:
"""Get NPU stats using openvino."""
NPU_RUNTIME_PATH = "/sys/devices/pci0000:00/0000:00:0b.0/power/runtime_active_time"
try:
- with open(NPU_RUNTIME_PATH, "r") as f:
+ with open(NPU_RUNTIME_PATH) as f:
initial_runtime = float(f.read().strip())
initial_time = time.time()
@@ -430,7 +542,7 @@ def get_openvino_npu_stats() -> Optional[dict[str, str]]:
time.sleep(1.0)
# Read runtime value again
- with open(NPU_RUNTIME_PATH, "r") as f:
+ with open(NPU_RUNTIME_PATH) as f:
current_runtime = float(f.read().strip())
current_time = time.time()
@@ -449,10 +561,10 @@ def get_openvino_npu_stats() -> Optional[dict[str, str]]:
return None
-def get_rockchip_gpu_stats() -> Optional[dict[str, str | float]]:
+def get_rockchip_gpu_stats() -> dict[str, str | float] | None:
"""Get GPU stats using rk."""
try:
- with open("/sys/kernel/debug/rkrga/load", "r") as f:
+ with open("/sys/kernel/debug/rkrga/load") as f:
content = f.read()
except FileNotFoundError:
return None
@@ -470,7 +582,7 @@ def get_rockchip_gpu_stats() -> Optional[dict[str, str | float]]:
stats: dict[str, str | float] = {"gpu": average_load, "mem": "-%"}
try:
- with open("/sys/class/thermal/thermal_zone5/temp", "r") as f:
+ with open("/sys/class/thermal/thermal_zone5/temp") as f:
line = f.readline().strip()
stats["temp"] = round(int(line) / 1000, 1)
except (FileNotFoundError, OSError, ValueError):
@@ -479,10 +591,10 @@ def get_rockchip_gpu_stats() -> Optional[dict[str, str | float]]:
return stats
-def get_rockchip_npu_stats() -> Optional[dict[str, float | str]]:
+def get_rockchip_npu_stats() -> dict[str, float | str] | None:
"""Get NPU stats using rk."""
try:
- with open("/sys/kernel/debug/rknpu/load", "r") as f:
+ with open("/sys/kernel/debug/rknpu/load") as f:
npu_output = f.read()
if "Core0:" in npu_output:
@@ -502,7 +614,7 @@ def get_rockchip_npu_stats() -> Optional[dict[str, float | str]]:
stats: dict[str, float | str] = {"npu": mean, "mem": "-%"}
try:
- with open("/sys/class/thermal/thermal_zone6/temp", "r") as f:
+ with open("/sys/class/thermal/thermal_zone6/temp") as f:
line = f.readline().strip()
stats["temp"] = round(int(line) / 1000, 1)
except (FileNotFoundError, OSError, ValueError):
@@ -511,7 +623,7 @@ def get_rockchip_npu_stats() -> Optional[dict[str, float | str]]:
return stats
-def get_axcl_npu_stats() -> Optional[dict[str, str | float]]:
+def get_axcl_npu_stats() -> dict[str, str | float] | None:
"""Get NPU stats using axcl."""
# Check if axcl-smi exists
axcl_smi_path = "/usr/bin/axcl/axcl-smi"
@@ -628,18 +740,18 @@ def get_nvidia_gpu_stats() -> dict[int, dict]:
return results
-def get_jetson_stats() -> Optional[dict[int, dict]]:
+def get_jetson_stats() -> dict[int, dict] | None:
results = {}
try:
results["mem"] = "-" # no discrete gpu memory
if os.path.exists("/sys/devices/gpu.0/load"):
- with open("/sys/devices/gpu.0/load", "r") as f:
+ with open("/sys/devices/gpu.0/load") as f:
gpuload = float(f.readline()) / 10
results["gpu"] = f"{gpuload}%"
elif os.path.exists("/sys/devices/platform/gpu.0/load"):
- with open("/sys/devices/platform/gpu.0/load", "r") as f:
+ with open("/sys/devices/platform/gpu.0/load") as f:
gpuload = float(f.readline()) / 10
results["gpu"] = f"{gpuload}%"
else:
@@ -697,6 +809,41 @@ def get_hailo_temps() -> dict[str, float]:
return temps
+def is_go2rtc_arbitrary_exec_allowed() -> bool:
+ """Read the GO2RTC_ALLOW_ARBITRARY_EXEC override from env, docker
+ secrets, or the Home Assistant add-on options file."""
+ raw: str | None = None
+ if "GO2RTC_ALLOW_ARBITRARY_EXEC" in os.environ:
+ raw = os.environ.get("GO2RTC_ALLOW_ARBITRARY_EXEC")
+ elif (
+ os.path.isdir("/run/secrets")
+ and os.access("/run/secrets", os.R_OK)
+ and "GO2RTC_ALLOW_ARBITRARY_EXEC" in os.listdir("/run/secrets")
+ ):
+ try:
+ with open("/run/secrets/GO2RTC_ALLOW_ARBITRARY_EXEC") as f:
+ raw = f.read().strip()
+ except OSError:
+ raw = None
+ elif os.path.isfile("/data/options.json"):
+ try:
+ with open("/data/options.json") as f:
+ options = json.loads(f.read())
+ raw = options.get("go2rtc_allow_arbitrary_exec")
+ except (OSError, json.JSONDecodeError):
+ raw = None
+
+ return raw is not None and str(raw).lower() in ("true", "1", "yes")
+
+
+def is_restricted_go2rtc_source(stream_source: str) -> bool:
+ """Check if a stream source is a restricted type (echo, expr, or exec)
+ and the GO2RTC_ALLOW_ARBITRARY_EXEC override is not set."""
+ if not stream_source.strip().startswith(("echo:", "expr:", "exec:")):
+ return False
+ return not is_go2rtc_arbitrary_exec_allowed()
+
+
def ffprobe_stream(ffmpeg, path: str, detailed: bool = False) -> sp.CompletedProcess:
"""Run ffprobe on stream."""
clean_path = escape_special_characters(path)
@@ -711,26 +858,172 @@ def ffprobe_stream(ffmpeg, path: str, detailed: bool = False) -> sp.CompletedPro
else:
format_entries = None
- ffprobe_cmd = [
+ def run(rtsp_transport: str | None = None) -> sp.CompletedProcess:
+ cmd = [ffmpeg.ffprobe_path]
+ if rtsp_transport:
+ cmd += ["-rtsp_transport", rtsp_transport]
+ cmd += [
+ "-timeout",
+ "1000000",
+ "-print_format",
+ "json",
+ "-show_entries",
+ f"stream={stream_entries}",
+ ]
+ if detailed and format_entries:
+ cmd.extend(["-show_entries", f"format={format_entries}"])
+ cmd.extend(["-loglevel", "error", clean_path])
+ try:
+ return sp.run(cmd, capture_output=True, timeout=6)
+ except sp.TimeoutExpired as e:
+ logger.info(
+ "ffprobe timed out while probing %s (transport=%s)",
+ clean_camera_user_pass(path),
+ rtsp_transport or "default",
+ )
+ return sp.CompletedProcess(
+ args=cmd,
+ returncode=1,
+ stdout=e.stdout or b"",
+ stderr=(e.stderr or b"") + b"\nffprobe timed out",
+ )
+
+ result = run()
+
+ # For RTSP: retry with explicit TCP transport if the first attempt failed
+ # (default UDP may be blocked)
+ if result.returncode != 0 and clean_path.startswith("rtsp://"):
+ result = run(rtsp_transport="tcp")
+
+ return result
+
+
+KEYFRAME_PROBE_WINDOW_SECONDS = 20
+KEYFRAME_GAP_WARNING_SECONDS = 4.0
+
+
+def parse_keyframe_packets(output: str) -> tuple[list[float], float | None]:
+ """Parse ffprobe CSV `pts_time,flags` output.
+
+ Returns the presentation timestamps of keyframes (flags containing "K")
+ and the maximum timestamp observed across all packets.
+ """
+ keyframe_pts: list[float] = []
+ max_pts: float | None = None
+
+ for line in output.splitlines():
+ parts = line.split(",")
+ if len(parts) < 2:
+ continue
+ try:
+ pts = float(parts[0])
+ except ValueError:
+ continue
+ if max_pts is None or pts > max_pts:
+ max_pts = pts
+ if "K" in parts[1]:
+ keyframe_pts.append(pts)
+
+ return keyframe_pts, max_pts
+
+
+def classify_keyframe_gaps(
+ keyframe_pts: list[float], segment_time: int
+) -> dict[str, Any]:
+ """Classify keyframe spacing for recording suitability.
+
+ A camera using a smart/+ codec or a long/variable GOP produces large or
+ irregular gaps between keyframes, which breaks time-based recording
+ segmentation. Severity:
+ - "unknown" when fewer than two keyframes were observed
+ - "error" when the longest gap exceeds the record segment length
+ - "warning" when the longest gap exceeds the warning threshold
+ - "ok" otherwise
+ """
+ thresholds = {
+ "warning": KEYFRAME_GAP_WARNING_SECONDS,
+ "error": segment_time,
+ }
+
+ if len(keyframe_pts) < 2:
+ return {
+ "keyframe_count": len(keyframe_pts),
+ "max_gap": None,
+ "mean_gap": None,
+ "min_gap": None,
+ "segment_time": segment_time,
+ "severity": "unknown",
+ "thresholds": thresholds,
+ }
+
+ gaps = [b - a for a, b in zip(keyframe_pts, keyframe_pts[1:])]
+ max_gap = max(gaps)
+
+ if max_gap > segment_time:
+ severity = "error"
+ elif max_gap > KEYFRAME_GAP_WARNING_SECONDS:
+ severity = "warning"
+ else:
+ severity = "ok"
+
+ return {
+ "keyframe_count": len(keyframe_pts),
+ "max_gap": round(max_gap, 2),
+ "mean_gap": round(sum(gaps) / len(gaps), 2),
+ "min_gap": round(min(gaps), 2),
+ "segment_time": segment_time,
+ "severity": severity,
+ "thresholds": thresholds,
+ }
+
+
+async def analyze_record_keyframes(
+ ffmpeg, url: str, segment_time: int, window: int = KEYFRAME_PROBE_WINDOW_SECONDS
+) -> dict[str, Any]:
+ """Probe a stream for ~`window` seconds and classify its keyframe spacing.
+
+ Reads video packet flags via ffprobe to find keyframes, then measures the
+ gaps between them. On timeout or failure returns an "unknown" result rather
+ than a false all-clear.
+ """
+ clean_url = escape_special_characters(url)
+ cmd = [
ffmpeg.ffprobe_path,
- "-timeout",
- "1000000",
- "-print_format",
- "json",
+ "-v",
+ "error",
+ "-select_streams",
+ "v:0",
+ "-read_intervals",
+ f"%+{window}",
"-show_entries",
- f"stream={stream_entries}",
+ "packet=pts_time,flags",
+ "-of",
+ "csv=p=0",
+ clean_url,
]
- # Add format entries for detailed mode
- if detailed and format_entries:
- ffprobe_cmd.extend(["-show_entries", f"format={format_entries}"])
+ try:
+ proc = await asyncio.create_subprocess_exec(
+ *cmd,
+ stdout=asyncio.subprocess.PIPE,
+ stderr=asyncio.subprocess.PIPE,
+ )
+ stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=window + 15)
+ except TimeoutError:
+ logger.warning("Keyframe probe timed out for record stream")
+ proc.kill()
+ return classify_keyframe_gaps([], segment_time)
+ except OSError as err:
+ logger.error("Keyframe probe failed: %s", err)
+ return classify_keyframe_gaps([], segment_time)
- ffprobe_cmd.extend(["-loglevel", "error", clean_path])
-
- return sp.run(ffprobe_cmd, capture_output=True)
+ keyframe_pts, max_pts = parse_keyframe_packets(stdout.decode("utf-8", "replace"))
+ result = classify_keyframe_gaps(keyframe_pts, segment_time)
+ result["duration_observed"] = round(max_pts, 2) if max_pts is not None else None
+ return result
-def vainfo_hwaccel(device_name: Optional[str] = None) -> sp.CompletedProcess:
+def vainfo_hwaccel(device_name: str | None = None) -> sp.CompletedProcess:
"""Run vainfo."""
if not device_name:
cmd = ["vainfo"]
@@ -807,10 +1100,15 @@ async def get_video_properties(
) -> dict[str, Any]:
async def probe_with_ffprobe(
url: str,
- ) -> tuple[bool, int, int, Optional[str], float]:
+ rtsp_transport: str | None = None,
+ ) -> tuple[bool, int, int, str | None, float]:
"""Fallback using ffprobe: returns (valid, width, height, codec, duration)."""
- cmd = [
- ffmpeg.ffprobe_path,
+ cmd = [ffmpeg.ffprobe_path]
+ if rtsp_transport:
+ cmd += ["-rtsp_transport", rtsp_transport]
+ cmd += [
+ "-rw_timeout",
+ "5000000",
"-v",
"quiet",
"-print_format",
@@ -819,11 +1117,23 @@ async def get_video_properties(
"-show_streams",
url,
]
+ proc = None
try:
proc = await asyncio.create_subprocess_exec(
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
)
- stdout, _ = await proc.communicate()
+ try:
+ stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=6)
+ except TimeoutError:
+ logger.info(
+ "ffprobe timed out while probing %s (transport=%s)",
+ clean_camera_user_pass(url),
+ rtsp_transport or "default",
+ )
+ proc.kill()
+ await proc.wait()
+ return False, 0, 0, None, -1
+
if proc.returncode != 0:
return False, 0, 0, None, -1
@@ -846,7 +1156,7 @@ async def get_video_properties(
except (json.JSONDecodeError, ValueError, KeyError, sp.SubprocessError):
return False, 0, 0, None, -1
- def probe_with_cv2(url: str) -> tuple[bool, int, int, Optional[str], float]:
+ def probe_with_cv2(url: str) -> tuple[bool, int, int, str | None, float]:
"""Primary attempt using cv2: returns (valid, width, height, fourcc, duration)."""
cap = cv2.VideoCapture(url)
if not cap.isOpened():
@@ -872,12 +1182,26 @@ async def get_video_properties(
cap.release()
return valid, width, height, fourcc, duration
- # try cv2 first
- has_video, width, height, fourcc, duration = probe_with_cv2(url)
+ is_rtsp = url.startswith("rtsp://")
- # fallback to ffprobe if needed
- if not has_video or (get_duration and duration < 0):
+ if is_rtsp:
+ # skip cv2 for RTSP: its FFmpeg backend has a hardcoded ~30s internal
+ # timeout that cannot be shortened per-call, and ffprobe bounded by
+ # -rw_timeout handles RTSP probing reliably
has_video, width, height, fourcc, duration = await probe_with_ffprobe(url)
+ else:
+ # try cv2 first for local files, HTTP, RTMP
+ has_video, width, height, fourcc, duration = probe_with_cv2(url)
+
+ # fallback to ffprobe if needed
+ if not has_video or (get_duration and duration < 0):
+ has_video, width, height, fourcc, duration = await probe_with_ffprobe(url)
+
+ # last resort for RTSP: try TCP transport, since default UDP may be blocked
+ if (not has_video or (get_duration and duration < 0)) and is_rtsp:
+ has_video, width, height, fourcc, duration = await probe_with_ffprobe(
+ url, rtsp_transport="tcp"
+ )
result: dict[str, Any] = {"has_valid_video": has_video}
if has_video:
@@ -892,10 +1216,10 @@ async def get_video_properties(
def process_logs(
contents: str,
- service: Optional[str] = None,
- start: Optional[int] = None,
- end: Optional[int] = None,
-) -> Tuple[int, List[str]]:
+ service: str | None = None,
+ start: int | None = None,
+ end: int | None = None,
+) -> tuple[int, list[str]]:
log_lines = []
last_message = None
last_timestamp = None
diff --git a/frigate/util/time.py b/frigate/util/time.py
index 1e7b49c243..861bec17f6 100644
--- a/frigate/util/time.py
+++ b/frigate/util/time.py
@@ -2,7 +2,6 @@
import datetime
import logging
-from typing import Tuple
from zoneinfo import ZoneInfoNotFoundError
import pytz
@@ -11,7 +10,7 @@ from tzlocal import get_localzone
logger = logging.getLogger(__name__)
-def get_tz_modifiers(tz_name: str) -> Tuple[str, str, float]:
+def get_tz_modifiers(tz_name: str) -> tuple[str, str, float]:
seconds_offset = (
datetime.datetime.now(pytz.timezone(tz_name)).utcoffset().total_seconds()
)
@@ -27,24 +26,18 @@ def get_tomorrow_at_time(hour: int) -> datetime.datetime:
try:
tomorrow = datetime.datetime.now(get_localzone()) + datetime.timedelta(days=1)
except ZoneInfoNotFoundError:
- tomorrow = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(
- days=1
- )
+ tomorrow = datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=1)
logger.warning(
"Using utc for maintenance due to missing or incorrect timezone set"
)
- return tomorrow.replace(hour=hour, minute=0, second=0).astimezone(
- datetime.timezone.utc
- )
+ return tomorrow.replace(hour=hour, minute=0, second=0).astimezone(datetime.UTC)
def is_current_hour(timestamp: int) -> bool:
"""Returns if timestamp is in the current UTC hour."""
start_of_next_hour = (
- datetime.datetime.now(datetime.timezone.utc).replace(
- minute=0, second=0, microsecond=0
- )
+ datetime.datetime.now(datetime.UTC).replace(minute=0, second=0, microsecond=0)
+ datetime.timedelta(hours=1)
).timestamp()
return timestamp < start_of_next_hour
diff --git a/frigate/video/detect.py b/frigate/video/detect.py
index 339b11e534..8c5a31e8df 100644
--- a/frigate/video/detect.py
+++ b/frigate/video/detect.py
@@ -3,7 +3,7 @@
import logging
import queue
import time
-from datetime import datetime, timezone
+from datetime import UTC, datetime
from multiprocessing import Queue
from multiprocessing.synchronize import Event as MpEvent
from typing import Any
@@ -273,7 +273,7 @@ def process_frames(
time.sleep(0.1)
continue
- if datetime.now().astimezone(timezone.utc) > next_region_update:
+ if datetime.now().astimezone(UTC) > next_region_update:
region_grid = requestor.send_data(REQUEST_REGION_GRID, camera_config.name)
next_region_update = get_tomorrow_at_time(2)
@@ -438,34 +438,32 @@ def process_frames(
else:
object_tracker.update_frame_times(frame_name, frame_time)
- # group the attribute detections based on what label they apply to
- attribute_detections: dict[str, list[TrackedObjectAttribute]] = {}
- for label, attribute_labels in attributes_map.items():
- attribute_detections[label] = [
- TrackedObjectAttribute(d)
- for d in consolidated_detections
- if d[0] in attribute_labels
- ]
-
# build detections
detections = {}
for obj in object_tracker.tracked_objects.values():
detections[obj["id"]] = {**obj, "attributes": []}
- # find the best object for each attribute to be assigned to
+ # assign each detected attribute to the best matching object.
+ # iterate consolidated_detections once so attributes that appear under
+ # multiple parent labels in attributes_map (e.g. license_plate is in
+ # both "car" and "motorcycle") are not appended more than once
all_objects: list[dict[str, Any]] = object_tracker.tracked_objects.values()
- for attributes in attribute_detections.values():
- for attribute in attributes:
- filtered_objects = filter(
- lambda o: attribute.label in attributes_map.get(o["label"], []),
- all_objects,
- )
- selected_object_id = attribute.find_best_object(filtered_objects)
+ detected_attributes = [
+ TrackedObjectAttribute(d)
+ for d in consolidated_detections
+ if d[0] in all_attributes
+ ]
+ for attribute in detected_attributes:
+ filtered_objects = filter(
+ lambda o: attribute.label in attributes_map.get(o["label"], []),
+ all_objects,
+ )
+ selected_object_id = attribute.find_best_object(filtered_objects)
- if selected_object_id is not None:
- detections[selected_object_id]["attributes"].append(
- attribute.get_tracking_data()
- )
+ if selected_object_id is not None:
+ detections[selected_object_id]["attributes"].append(
+ attribute.get_tracking_data()
+ )
# debug object tracking
if False:
diff --git a/frigate/video/ffmpeg.py b/frigate/video/ffmpeg.py
index d30dc3b188..24b7805333 100644
--- a/frigate/video/ffmpeg.py
+++ b/frigate/video/ffmpeg.py
@@ -6,7 +6,7 @@ import subprocess as sp
import threading
import time
from collections import deque
-from datetime import datetime, timedelta, timezone
+from datetime import UTC, datetime, timedelta
from multiprocessing import Queue, Value
from multiprocessing.synchronize import Event as MpEvent
from typing import Any
@@ -24,7 +24,7 @@ from frigate.config.camera.updater import (
)
from frigate.const import PROCESS_PRIORITY_HIGH
from frigate.log import LogPipe
-from frigate.util.builtin import EventsPerSecond
+from frigate.util.builtin import EventsPerSecond, get_record_segment_time
from frigate.util.ffmpeg import start_or_restart_ffmpeg, stop_ffmpeg
from frigate.util.image import (
FrameManager,
@@ -157,6 +157,7 @@ class CameraWatchdog(threading.Thread):
)
self.requestor = InterProcessRequestor()
self.was_enabled = self.config.enabled
+ self.was_record_enabled_in_config = self.config.record.enabled_in_config
self.segment_subscriber = RecordingsDataSubscriber(RecordingsDataTypeEnum.all)
self.latest_valid_segment_time: float = 0
@@ -164,6 +165,12 @@ class CameraWatchdog(threading.Thread):
self.latest_cache_segment_time: float = 0
self.record_enable_time: datetime | None = None
+ # `valid` segments are published with the segment's start time, so the
+ # gap between consecutive publishes can reach 2 * segment_time. Pad the
+ # staleness threshold so it's never tighter than that worst case.
+ segment_time = get_record_segment_time(self.config)
+ self.record_stale_threshold = max(120, 2 * segment_time + 30)
+
# Stall tracking (based on last processed frame)
self._stall_timestamps: deque[float] = deque()
self._stall_active: bool = False
@@ -253,7 +260,7 @@ class CameraWatchdog(threading.Thread):
self.start_all_ffmpeg()
# If recording is enabled at startup, set the grace period timer
if self.config.record.enabled:
- self.record_enable_time = datetime.now().astimezone(timezone.utc)
+ self.record_enable_time = datetime.now().astimezone(UTC)
time.sleep(self.sleeptime)
last_restart_time = datetime.now().timestamp()
@@ -273,7 +280,7 @@ class CameraWatchdog(threading.Thread):
self.latest_valid_segment_time = 0
self.latest_invalid_segment_time = 0
self.latest_cache_segment_time = 0
- self.record_enable_time = datetime.now().astimezone(timezone.utc)
+ self.record_enable_time = datetime.now().astimezone(UTC)
last_restart_time = datetime.now().timestamp()
continue
@@ -287,7 +294,7 @@ class CameraWatchdog(threading.Thread):
self.latest_valid_segment_time = 0
self.latest_invalid_segment_time = 0
self.latest_cache_segment_time = 0
- self.record_enable_time = datetime.now().astimezone(timezone.utc)
+ self.record_enable_time = datetime.now().astimezone(UTC)
else:
self.logger.debug(f"Disabling camera {self.config.name}")
self.stop_all_ffmpeg()
@@ -300,6 +307,22 @@ class CameraWatchdog(threading.Thread):
self.was_enabled = enabled
continue
+ record_enabled_in_config = self.config.record.enabled_in_config
+ if record_enabled_in_config != self.was_record_enabled_in_config:
+ if record_enabled_in_config and enabled:
+ self.logger.debug(
+ f"Record enabled in config for {self.config.name}, restarting ffmpeg"
+ )
+ self.stop_all_ffmpeg()
+ self.start_all_ffmpeg()
+ self.latest_valid_segment_time = 0
+ self.latest_invalid_segment_time = 0
+ self.latest_cache_segment_time = 0
+ self.record_enable_time = datetime.now().astimezone(UTC)
+ last_restart_time = datetime.now().timestamp()
+ self.was_record_enabled_in_config = record_enabled_in_config
+ continue
+
if not enabled:
continue
@@ -317,16 +340,16 @@ class CameraWatchdog(threading.Thread):
if camera != self.config.name:
continue
- if topic.endswith(RecordingsDataTypeEnum.valid.value):
- self.logger.debug(
- f"Latest valid recording segment time on {camera}: {segment_time}"
- )
- self.latest_valid_segment_time = segment_time
- elif topic.endswith(RecordingsDataTypeEnum.invalid.value):
+ if topic.endswith(RecordingsDataTypeEnum.invalid.value):
self.logger.warning(
f"Invalid recording segment detected for {camera} at {segment_time}"
)
self.latest_invalid_segment_time = segment_time
+ elif topic.endswith(RecordingsDataTypeEnum.valid.value):
+ self.logger.debug(
+ f"Latest valid recording segment time on {camera}: {segment_time}"
+ )
+ self.latest_valid_segment_time = segment_time
elif topic.endswith(RecordingsDataTypeEnum.latest.value):
if segment_time is not None:
self.latest_cache_segment_time = segment_time
@@ -379,7 +402,7 @@ class CameraWatchdog(threading.Thread):
poll = p["process"].poll()
if self.config.record.enabled and "record" in p["roles"]:
- now_utc = datetime.now().astimezone(timezone.utc)
+ now_utc = datetime.now().astimezone(UTC)
# Check if we're within the grace period after enabling recording
# Grace period: 90 seconds allows time for ffmpeg to start and create first segment
@@ -388,41 +411,36 @@ class CameraWatchdog(threading.Thread):
) < timedelta(seconds=90)
latest_cache_dt = (
- datetime.fromtimestamp(
- self.latest_cache_segment_time, tz=timezone.utc
- )
+ datetime.fromtimestamp(self.latest_cache_segment_time, tz=UTC)
if self.latest_cache_segment_time > 0
else now_utc - timedelta(seconds=1)
)
latest_valid_dt = (
- datetime.fromtimestamp(
- self.latest_valid_segment_time, tz=timezone.utc
- )
+ datetime.fromtimestamp(self.latest_valid_segment_time, tz=UTC)
if self.latest_valid_segment_time > 0
else now_utc - timedelta(seconds=1)
)
latest_invalid_dt = (
- datetime.fromtimestamp(
- self.latest_invalid_segment_time, tz=timezone.utc
- )
+ datetime.fromtimestamp(self.latest_invalid_segment_time, tz=UTC)
if self.latest_invalid_segment_time > 0
else now_utc - timedelta(seconds=1)
)
# ensure segments are still being created and that they have valid video data
# Skip checks during grace period to allow segments to start being created
+ stale_window = timedelta(seconds=self.record_stale_threshold)
cache_stale = not in_grace_period and now_utc > (
- latest_cache_dt + timedelta(seconds=120)
+ latest_cache_dt + stale_window
)
valid_stale = not in_grace_period and now_utc > (
- latest_valid_dt + timedelta(seconds=120)
+ latest_valid_dt + stale_window
)
invalid_stale_condition = (
self.latest_invalid_segment_time > 0
and not in_grace_period
- and now_utc > (latest_invalid_dt + timedelta(seconds=120))
+ and now_utc > (latest_invalid_dt + stale_window)
and self.latest_valid_segment_time
<= self.latest_invalid_segment_time
)
@@ -439,7 +457,7 @@ class CameraWatchdog(threading.Thread):
)
self.logger.error(
- f"{reason} for {self.config.name} in the last 120s. Restarting the ffmpeg record process..."
+ f"{reason} for {self.config.name} in the last {self.record_stale_threshold}s. Restarting the ffmpeg record process..."
)
p["process"] = start_or_restart_ffmpeg(
p["cmd"],
diff --git a/frigate/watchdog.py b/frigate/watchdog.py
index 63fd166298..88023d5122 100644
--- a/frigate/watchdog.py
+++ b/frigate/watchdog.py
@@ -3,9 +3,9 @@ import logging
import threading
import time
from collections import deque
+from collections.abc import Callable
from dataclasses import dataclass, field
from multiprocessing.synchronize import Event as MpEvent
-from typing import Callable
from frigate.object_detection.base import ObjectDetectProcess
from frigate.util.process import FrigateProcess
@@ -28,6 +28,7 @@ class MonitoredProcess:
restart_timestamps: deque[float] = field(
default_factory=lambda: deque(maxlen=MAX_RESTARTS)
)
+ clean_exit_logged: bool = False
def is_restarting_too_fast(self, now: float) -> bool:
while (
@@ -72,7 +73,9 @@ class FrigateWatchdog(threading.Thread):
exitcode = entry.process.exitcode
if exitcode == 0:
- logger.info("Process %s exited cleanly, not restarting", entry.name)
+ if not entry.clean_exit_logged:
+ logger.info("Process %s exited cleanly, not restarting", entry.name)
+ entry.clean_exit_logged = True
return
logger.warning(
diff --git a/generate_api_auth_spec.py b/generate_api_auth_spec.py
new file mode 100644
index 0000000000..5768a6db64
--- /dev/null
+++ b/generate_api_auth_spec.py
@@ -0,0 +1,606 @@
+"""Generate the OpenAPI spec from the app, annotated with auth requirements.
+
+This generator builds the FastAPI application, exports its OpenAPI document via
+``app.openapi()``, and enriches every operation with authentication metadata:
+
+ * a ``components.securitySchemes`` block,
+ * a per-operation ``security`` requirement (so the docs render a lock badge),
+ * an ``x-required-role`` extension for machine readers, and
+ * a short bold ``Access:`` note prepended to each operation description.
+
+The committed docs/static/frigate-api.yaml is the output of this script. It is
+generated rather than hand-maintained so it stays complete and current; the docs
+build (docusaurus-plugin-openapi-docs) consumes it as-is.
+
+The access level for an endpoint is determined by BOTH its route-level
+dependency (``require_role``/``allow_any_authenticated``/``allow_public``/
+``require_camera_access``) AND the global "secure by default" admin dependency,
+which is bypassed only for the paths listed in ``require_admin_by_default``.
+Those exempt lists are read directly from the function's closure so this script
+stays in lockstep with ``frigate/api/auth.py`` instead of duplicating them.
+
+Many handlers enforce per-camera access by calling ``require_camera_access``
+inside the handler body rather than as a route dependency, which dependency
+introspection cannot see. We recover those from the handler's bytecode (see
+``_handler_enforces_camera``) and promote an otherwise "any authenticated"
+operation to camera-scoped.
+
+Usage (from the repository root):
+
+ python3 generate_api_auth_spec.py # write the spec
+ python3 generate_api_auth_spec.py --check # CI guard: fail if stale
+
+The process exits non-zero if the generated document fails structural
+validation, or (in --check mode) if the committed spec is out of date.
+"""
+
+import argparse
+import difflib
+import inspect
+import io
+import logging
+import sys
+from pathlib import Path
+
+from fastapi import FastAPI
+from fastapi.routing import APIRoute
+from ruamel.yaml import YAML
+from ruamel.yaml.scalarstring import LiteralScalarString
+
+from frigate.api import app as main_app
+from frigate.api import (
+ auth,
+ camera,
+ chat,
+ classification,
+ debug_replay,
+ event,
+ export,
+ media,
+ motion_search,
+ notification,
+ preview,
+ record,
+ review,
+)
+from frigate.api.auth import require_admin_by_default
+
+logging.basicConfig(level=logging.INFO, format="%(message)s")
+logger = logging.getLogger("generate_api_auth_spec")
+
+REPO_ROOT = Path(__file__).resolve().parent
+OUTPUT_SPEC = REPO_ROOT / "docs" / "static" / "frigate-api.yaml"
+
+HTTP_METHODS = {"get", "post", "put", "delete", "patch"}
+
+# Banner written at the top of the generated spec.
+HEADER = (
+ "# Generated by generate_api_auth_spec.py — do not edit by hand.\n"
+ "# Regenerate with: python3 generate_api_auth_spec.py\n"
+ "# The empty info.title is intentional: a docusaurus-openapi-docs convention\n"
+ "# that suppresses the generated API introduction page.\n"
+)
+
+# Post-processing applied on top of the raw app.openapi() export. These live
+# only in the published spec, not in the app, so they are reproduced here.
+SPEC_TITLE = ""
+SPEC_SERVERS = [
+ {"url": "https://demo.frigate.video/api"},
+ {"url": "http://localhost:5001/api"},
+]
+
+# Access levels, ordered from least to most privileged. The string values are
+# also what we emit as ``x-required-role``.
+PUBLIC = "public"
+AUTHENTICATED = "any"
+CAMERA = "camera"
+ADMIN = "admin"
+
+ADMIN_SCHEME = "frigateAdminAuth"
+USER_SCHEME = "frigateUserAuth"
+
+SECURITY_SCHEMES = {
+ ADMIN_SCHEME: {
+ "type": "apiKey",
+ "in": "cookie",
+ "name": "frigate_token",
+ "description": (
+ "Authenticated session whose resolved role is 'admin'. The session "
+ "is established via the JWT cookie issued by POST /login, or via "
+ "proxy auth headers (remote-user / remote-role) when Frigate runs "
+ "behind an authenticating reverse proxy."
+ ),
+ },
+ USER_SCHEME: {
+ "type": "apiKey",
+ "in": "cookie",
+ "name": "frigate_token",
+ "description": (
+ "Any authenticated session (role 'viewer' or higher), established "
+ "via the JWT cookie issued by POST /login, or via proxy auth "
+ "headers when Frigate runs behind an authenticating reverse proxy."
+ ),
+ },
+}
+
+# How each access level maps to a rendered note.
+ACCESS_NOTES = {
+ PUBLIC: "**Access:** Public — no authentication required.",
+ AUTHENTICATED: "**Access:** Any authenticated user.",
+ CAMERA: "**Access:** Authenticated user with access to the referenced camera.",
+ ADMIN: "**Access:** Admin role required.",
+}
+
+
+def build_app() -> FastAPI:
+ """Build a bare app with every router mounted.
+
+ This mirrors the router set wired up in frigate.api.fastapi_app. It omits
+ the global admin dependency and all runtime state; the OpenAPI route table
+ and the per-route dependencies are all we need to export and classify.
+ """
+ app = FastAPI()
+ routers = [
+ auth.router,
+ camera.router,
+ chat.router,
+ classification.router,
+ review.router,
+ main_app.router,
+ preview.router,
+ notification.router,
+ export.router,
+ event.router,
+ media.router,
+ motion_search.router,
+ record.router,
+ debug_replay.router,
+ ]
+ for router in routers:
+ app.include_router(router)
+ return app
+
+
+def read_exempt_rules() -> tuple[set[str], tuple[str, ...]]:
+ """Read the admin-exemption lists straight from the auth dependency closure.
+
+ Reading them here (rather than copying) keeps this generator in sync with
+ frigate/api/auth.py automatically.
+ """
+ closure = inspect.getclosurevars(require_admin_by_default()).nonlocals
+ exempt_paths = set(closure["EXEMPT_PATHS"])
+ exempt_prefixes = tuple(closure["EXEMPT_PREFIXES"])
+ return exempt_paths, exempt_prefixes
+
+
+def _first_segment(path: str) -> str:
+ return path.split("/", 2)[1] if path.startswith("/") and len(path) > 1 else ""
+
+
+def _route_markers(route: APIRoute) -> tuple[set[str], list[str] | None]:
+ """Return the set of recognized auth markers on a route's dependencies."""
+ markers: set[str] = set()
+ admin_roles: list[str] | None = None
+
+ for dep in route.dependant.dependencies:
+ call = dep.call
+ qualname = getattr(call, "__qualname__", "") or ""
+ name = getattr(call, "__name__", "") or ""
+
+ if "role_checker" in qualname:
+ markers.add(ADMIN)
+ try:
+ roles = inspect.getclosurevars(call).nonlocals.get("required_roles")
+ if roles:
+ admin_roles = list(roles)
+ except (TypeError, ValueError):
+ pass
+ elif name in ("require_camera_access", "require_go2rtc_stream_access"):
+ markers.add(CAMERA)
+ elif "auth_checker" in qualname:
+ markers.add(AUTHENTICATED)
+ elif "public_checker" in qualname:
+ markers.add(PUBLIC)
+
+ return markers, admin_roles
+
+
+def _handler_enforces_camera(route: APIRoute) -> bool:
+ """True if the route handler calls require_camera_access in its body.
+
+ Such calls are invisible to dependency introspection. We detect them from
+ the handler's compiled bytecode: a global name referenced anywhere in the
+ function appears in ``__code__.co_names``. This catches direct calls (all of
+ them, currently); a call hidden behind a helper function would be missed.
+ """
+ code = getattr(route.endpoint, "__code__", None)
+ return bool(code and "require_camera_access" in code.co_names)
+
+
+def classify_route(
+ route: APIRoute,
+ exempt_paths: set[str],
+ exempt_prefixes: tuple[str, ...],
+) -> tuple[str, list[str] | None, str | None]:
+ """Resolve the effective access level for a route.
+
+ Returns (access_level, roles, flag). ``flag`` is a human-readable note when
+ the result needed inference or revealed a possible inconsistency.
+ """
+ level, roles, flag = _classify_base(route, exempt_paths, exempt_prefixes)
+
+ # In-body require_camera_access enforcement is invisible to dependency
+ # introspection. When the effective access would otherwise be "any
+ # authenticated", the handler's per-camera check is the real constraint, so
+ # promote it to camera-scoped. Admin/public are left alone: for admin the
+ # role is the binding requirement and the camera check is only defensive.
+ if level == AUTHENTICATED and _handler_enforces_camera(route):
+ return CAMERA, None, None
+
+ return level, roles, flag
+
+
+def _classify_base(
+ route: APIRoute,
+ exempt_paths: set[str],
+ exempt_prefixes: tuple[str, ...],
+) -> tuple[str, list[str] | None, str | None]:
+ """Resolve the access level from route-level dependencies and exempt rules."""
+ markers, admin_roles = _route_markers(route)
+ path = route.path
+ is_camera_path = _first_segment(path) == "{camera_name}"
+ exempt = path in exempt_paths or path.startswith(exempt_prefixes) or is_camera_path
+
+ # Explicit route-level markers win, in order of specificity.
+ if ADMIN in markers:
+ return ADMIN, admin_roles or ["admin"], None
+ if CAMERA in markers:
+ return CAMERA, None, None
+ if AUTHENTICATED in markers:
+ if exempt:
+ return AUTHENTICATED, None, None
+ # The route opts in to any-authenticated, but the global admin check is
+ # not bypassed for this path, so admin is what actually gets enforced.
+ return (
+ ADMIN,
+ ["admin"],
+ (
+ "route declares allow_any_authenticated but path is not exempt from "
+ "the global admin check; admin is effectively enforced"
+ ),
+ )
+ if PUBLIC in markers:
+ if exempt:
+ return PUBLIC, None, None
+ return (
+ ADMIN,
+ ["admin"],
+ (
+ "route declares allow_public but path is not exempt from the global "
+ "admin check; admin is effectively enforced"
+ ),
+ )
+
+ # No explicit auth marker: governed purely by the global default.
+ if not exempt:
+ return ADMIN, ["admin"], None
+
+ # Exempt with no route dependency: the global admin check is bypassed and
+ # there is no route-level gate, so authorization (if any) happens inside the
+ # handler. Infer from the path shape and flag for confirmation.
+ if is_camera_path:
+ return (
+ CAMERA,
+ None,
+ (
+ "no route-level dependency; camera-scoped path, authorization "
+ "assumed to be enforced in the handler"
+ ),
+ )
+ return (
+ AUTHENTICATED,
+ None,
+ (
+ "path is exempt from the global admin check but has no route-level "
+ "dependency; confirm authorization is enforced in the handler"
+ ),
+ )
+
+
+def build_access_map(
+ app: FastAPI,
+ exempt_paths: set[str],
+ exempt_prefixes: tuple[str, ...],
+) -> dict[tuple[str, str], dict]:
+ """Map (path, lowercase method) -> classification details."""
+ access_map: dict[tuple[str, str], dict] = {}
+ for route in app.routes:
+ if not isinstance(route, APIRoute):
+ continue
+ level, roles, flag = classify_route(route, exempt_paths, exempt_prefixes)
+ for method in route.methods:
+ if method in ("HEAD", "OPTIONS"):
+ continue
+ access_map[(route.path, method.lower())] = {
+ "level": level,
+ "roles": roles,
+ "flag": flag,
+ "path": route.path,
+ "method": method,
+ }
+ return access_map
+
+
+def security_for(level: str) -> list:
+ """Build the OpenAPI ``security`` value for an access level."""
+ if level == PUBLIC:
+ return []
+ if level == ADMIN:
+ return [{ADMIN_SCHEME: []}]
+ # AUTHENTICATED and CAMERA both require any authenticated session; the
+ # camera-specific scoping is conveyed in the note and x-required-role.
+ return [{USER_SCHEME: []}]
+
+
+def required_role_value(level: str, roles: list[str] | None):
+ if level == ADMIN and roles and roles != ["admin"]:
+ return roles
+ return level
+
+
+def annotate_description(operation: dict, note: str) -> None:
+ existing = operation.get("description")
+ if not existing:
+ operation["description"] = note
+ return
+ operation["description"] = LiteralScalarString(
+ f"{note}\n\n{str(existing).rstrip()}"
+ )
+
+
+def base_document(raw: dict) -> dict:
+ """Apply the docs pipeline post-processing with a stable top-level order."""
+ info = dict(raw.get("info", {}))
+ info["title"] = SPEC_TITLE
+ return {
+ "openapi": raw["openapi"],
+ "info": info,
+ "servers": [dict(server) for server in SPEC_SERVERS],
+ "paths": raw["paths"],
+ "components": raw.get("components", {}),
+ }
+
+
+def enrich(spec: dict, access_map: dict) -> tuple[dict, list, list]:
+ """Add security schemes and per-operation auth metadata in place."""
+ components = spec.setdefault("components", {})
+ components["securitySchemes"] = dict(SECURITY_SCHEMES)
+
+ counts: dict[str, int] = {}
+ flagged: list[dict] = []
+ unmatched: list[tuple[str, str]] = []
+
+ for path, path_item in spec["paths"].items():
+ for method, operation in path_item.items():
+ if method.lower() not in HTTP_METHODS:
+ continue
+ details = access_map.get((path, method.lower()))
+ if details is None:
+ unmatched.append((method.upper(), path))
+ continue
+
+ level = details["level"]
+ counts[level] = counts.get(level, 0) + 1
+ operation["security"] = security_for(level)
+ operation["x-required-role"] = required_role_value(level, details["roles"])
+ annotate_description(operation, ACCESS_NOTES[level])
+
+ if details["flag"]:
+ flagged.append(details)
+
+ return counts, flagged, unmatched
+
+
+# Numeric defaults at or above this magnitude are treated as live Unix
+# timestamps baked into the schema at import time (e.g. the /{camera_name}
+# /recordings after/before params default to datetime.now()). They make the
+# export non-deterministic and document a meaningless frozen epoch, so they are
+# stripped. The proper fix is to default those route params to None and resolve
+# "now" inside the handler.
+VOLATILE_DEFAULT_THRESHOLD = 1_000_000_000
+
+
+def strip_volatile_defaults(node, trail: str = "") -> list[tuple[str, float]]:
+ """Remove epoch-like numeric ``default`` values so the export is stable.
+
+ Returns the (location, value) pairs that were removed, for reporting.
+ """
+ removed: list[tuple[str, float]] = []
+ if isinstance(node, dict):
+ default = node.get("default")
+ if (
+ isinstance(default, (int, float))
+ and not isinstance(default, bool)
+ and default >= VOLATILE_DEFAULT_THRESHOLD
+ ):
+ removed.append((trail, default))
+ del node["default"]
+ for key, value in node.items():
+ removed.extend(strip_volatile_defaults(value, f"{trail}/{key}"))
+ elif isinstance(node, list):
+ for index, value in enumerate(node):
+ removed.extend(strip_volatile_defaults(value, f"{trail}[{index}]"))
+ return removed
+
+
+def to_block_scalars(node):
+ """Recursively render multi-line strings as literal block scalars.
+
+ Produces readable, deterministic YAML (``|-`` blocks) instead of long
+ double-quoted lines with escaped newlines.
+ """
+ if isinstance(node, dict):
+ return {key: to_block_scalars(value) for key, value in node.items()}
+ if isinstance(node, list):
+ return [to_block_scalars(value) for value in node]
+ if isinstance(node, str) and "\n" in node:
+ return LiteralScalarString(node)
+ return node
+
+
+def _iter_refs(node):
+ if isinstance(node, dict):
+ for key, value in node.items():
+ if key == "$ref" and isinstance(value, str):
+ yield value
+ else:
+ yield from _iter_refs(value)
+ elif isinstance(node, list):
+ for value in node:
+ yield from _iter_refs(value)
+
+
+def validate(spec: dict) -> list[str]:
+ """Structural sanity checks on the generated document."""
+ problems: list[str] = []
+ schemas = set(spec.get("components", {}).get("schemas", {}))
+ defined_schemes = set(spec.get("components", {}).get("securitySchemes", {}))
+
+ for ref in _iter_refs(spec):
+ if ref.startswith("#/components/schemas/"):
+ name = ref.rsplit("/", 1)[-1]
+ if name not in schemas:
+ problems.append(f"dangling $ref: {ref}")
+
+ for path, path_item in spec.get("paths", {}).items():
+ for method, operation in path_item.items():
+ if method.lower() not in HTTP_METHODS or not isinstance(operation, dict):
+ continue
+ location = f"{method.upper()} {path}"
+ if "x-required-role" not in operation:
+ problems.append(f"missing x-required-role: {location}")
+ if "security" not in operation:
+ problems.append(f"missing security: {location}")
+ continue
+ for requirement in operation["security"]:
+ for scheme in requirement:
+ if scheme not in defined_schemes:
+ problems.append(
+ f"undefined security scheme {scheme}: {location}"
+ )
+
+ return sorted(set(problems))
+
+
+def render(spec: dict) -> str:
+ """Serialize the spec to the canonical YAML string (with the header)."""
+ yaml = YAML()
+ yaml.width = 80
+ yaml.indent(mapping=2, sequence=4, offset=2)
+ stream = io.StringIO()
+ yaml.dump(spec, stream)
+ return HEADER + stream.getvalue()
+
+
+def build_spec() -> tuple[dict, dict, list, list, list]:
+ app = build_app()
+ exempt_paths, exempt_prefixes = read_exempt_rules()
+ access_map = build_access_map(app, exempt_paths, exempt_prefixes)
+
+ spec = base_document(app.openapi())
+ normalized = strip_volatile_defaults(spec)
+ counts, flagged, unmatched = enrich(spec, access_map)
+ spec = to_block_scalars(spec)
+ return spec, counts, flagged, unmatched, normalized
+
+
+def main(argv: list[str] | None = None) -> int:
+ parser = argparse.ArgumentParser(description="Generate the annotated OpenAPI spec.")
+ parser.add_argument(
+ "--check",
+ action="store_true",
+ help="verify the committed spec is up to date without writing; "
+ "exit non-zero if it would change",
+ )
+ args = parser.parse_args(argv)
+
+ spec, counts, flagged, unmatched, normalized = build_spec()
+ problems = validate(spec)
+ rendered = render(spec)
+
+ if args.check:
+ return _check(rendered, problems)
+
+ if problems:
+ logger.error("Refusing to write — generated spec failed validation:")
+ for problem in problems:
+ logger.error(" %s", problem)
+ return 1
+
+ OUTPUT_SPEC.write_text(rendered)
+ _report(counts, flagged, unmatched, normalized)
+ logger.info("\nWrote %s", OUTPUT_SPEC.relative_to(REPO_ROOT))
+ return 0
+
+
+def _check(rendered: str, problems: list[str]) -> int:
+ name = OUTPUT_SPEC.relative_to(REPO_ROOT)
+ if problems:
+ logger.error("Generated spec failed validation:")
+ for problem in problems:
+ logger.error(" %s", problem)
+ return 1
+
+ current = OUTPUT_SPEC.read_text() if OUTPUT_SPEC.exists() else ""
+ if current == rendered:
+ logger.info("%s is up to date", name)
+ return 0
+
+ logger.error(
+ "%s is out of date. Regenerate with: python3 %s",
+ name,
+ Path(__file__).name,
+ )
+ diff = difflib.unified_diff(
+ current.splitlines(),
+ rendered.splitlines(),
+ fromfile=f"{name} (committed)",
+ tofile=f"{name} (generated)",
+ lineterm="",
+ n=2,
+ )
+ for shown, line in enumerate(diff):
+ if shown >= 60:
+ logger.error(" ... (diff truncated)")
+ break
+ logger.error(" %s", line)
+ return 1
+
+
+def _report(counts, flagged, unmatched, normalized) -> None:
+ logger.info("Access levels applied:")
+ for level in (PUBLIC, AUTHENTICATED, CAMERA, ADMIN):
+ logger.info(" %-14s %d", level, counts.get(level, 0))
+ logger.info(" %-14s %d", "total", sum(counts.values()))
+
+ if normalized:
+ logger.info("\nStripped volatile timestamp defaults (%d):", len(normalized))
+ for location, value in normalized:
+ logger.info(" %s = %s", location.lstrip("/"), value)
+
+ if flagged:
+ logger.info("\nFlagged for manual confirmation (%d):", len(flagged))
+ for item in flagged:
+ logger.info(" %-6s %s", item["method"], item["path"])
+ logger.info(" -> %s (%s)", item["level"], item["flag"])
+
+ if unmatched:
+ logger.info(
+ "\nOperations with no classification (%d) [unexpected]:", len(unmatched)
+ )
+ for method, path in unmatched:
+ logger.info(" %-6s %s", method, path)
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/generate_config_translations.py b/generate_config_translations.py
index 032edb2327..aa115e44c3 100644
--- a/generate_config_translations.py
+++ b/generate_config_translations.py
@@ -10,7 +10,7 @@ import json
import logging
import sys
from pathlib import Path
-from typing import Any, Dict, get_args, get_origin
+from typing import Any, get_args, get_origin
from frigate.config.config import FrigateConfig
from frigate.util.schema import get_config_schema
@@ -19,7 +19,7 @@ logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
-def get_field_translations(field_info) -> Dict[str, str]:
+def get_field_translations(field_info) -> dict[str, str]:
"""Extract title and description from a Pydantic field."""
translations = {}
@@ -33,8 +33,8 @@ def get_field_translations(field_info) -> Dict[str, str]:
def extract_translations_from_schema(
- schema: Dict[str, Any], defs: Dict[str, Any] = None
-) -> Dict[str, Any]:
+ schema: dict[str, Any], defs: dict[str, Any] = None
+) -> dict[str, Any]:
"""
Recursively extract translations (titles and descriptions) from a JSON schema.
@@ -150,29 +150,51 @@ def extract_translations_from_schema(
# Handle anyOf cases
elif "anyOf" in field_schema:
for item in field_schema["anyOf"]:
+ nested = None
+ if item.get("type") == "null":
+ continue
if "properties" in item:
nested = extract_translations_from_schema(item, defs=defs)
+ elif "$ref" in item:
+ ref_path = item["$ref"]
+ if ref_path.startswith("#/$defs/"):
+ ref_name = ref_path.split("/")[-1]
+ if ref_name in defs:
+ nested = extract_translations_from_schema(
+ defs[ref_name], defs=defs
+ )
+ elif (
+ "additionalProperties" in item
+ and isinstance(item["additionalProperties"], dict)
+ and "$ref" in item["additionalProperties"]
+ ):
+ ref_path = item["additionalProperties"]["$ref"]
+ if ref_path.startswith("#/$defs/"):
+ ref_name = ref_path.split("/")[-1]
+ if ref_name in defs:
+ nested = extract_translations_from_schema(
+ defs[ref_name], defs=defs
+ )
+ elif (
+ "items" in item
+ and isinstance(item["items"], dict)
+ and ("$ref" in item["items"])
+ ):
+ ref_path = item["items"]["$ref"]
+ if ref_path.startswith("#/$defs/"):
+ ref_name = ref_path.split("/")[-1]
+ if ref_name in defs:
+ nested = extract_translations_from_schema(
+ defs[ref_name], defs=defs
+ )
+
+ if nested:
nested_without_root = {
k: v
for k, v in nested.items()
if k not in ("label", "description")
}
field_translations.update(nested_without_root)
- elif "$ref" in item:
- ref_path = item["$ref"]
- if ref_path.startswith("#/$defs/"):
- ref_name = ref_path.split("/")[-1]
- if ref_name in defs:
- ref_schema = defs[ref_name]
- nested = extract_translations_from_schema(
- ref_schema, defs=defs
- )
- nested_without_root = {
- k: v
- for k, v in nested.items()
- if k not in ("label", "description")
- }
- field_translations.update(nested_without_root)
if field_translations:
translations[field_name] = field_translations
@@ -180,7 +202,7 @@ def extract_translations_from_schema(
return translations
-def generate_section_translation(config_class: type) -> Dict[str, Any]:
+def generate_section_translation(config_class: type) -> dict[str, Any]:
"""
Generate translation structure for a config section using its JSON schema.
"""
@@ -189,8 +211,8 @@ def generate_section_translation(config_class: type) -> Dict[str, Any]:
def get_detector_translations(
- config_schema: Dict[str, Any],
-) -> tuple[Dict[str, Any], Dict[str, Any], set[str]]:
+ config_schema: dict[str, Any],
+) -> tuple[dict[str, Any], dict[str, Any], set[str]]:
"""Build detector type translations with nested fields based on schema definitions.
Returns a tuple of (type_translations, shared_fields, nested_field_keys).
@@ -203,8 +225,8 @@ def get_detector_translations(
mapping = discriminator.get("mapping", {})
# First pass: collect all nested fields per detector type
- all_nested: Dict[str, Dict[str, Any]] = {}
- type_meta: Dict[str, Dict[str, str]] = {}
+ all_nested: dict[str, dict[str, Any]] = {}
+ type_meta: dict[str, dict[str, str]] = {}
for detector_type, ref in mapping.items():
if not isinstance(ref, str) or not ref.startswith("#/$defs/"):
@@ -215,7 +237,7 @@ def get_detector_translations(
if not ref_schema:
continue
- meta: Dict[str, str] = {}
+ meta: dict[str, str] = {}
title = ref_schema.get("title")
description = ref_schema.get("description")
if title:
@@ -230,7 +252,7 @@ def get_detector_translations(
}
# Find fields that are identical across all types that have them
- shared_fields: Dict[str, Any] = {}
+ shared_fields: dict[str, Any] = {}
if all_nested:
# Collect all field keys across all types
all_keys: set[str] = set()
@@ -243,10 +265,10 @@ def get_detector_translations(
shared_fields[key] = values[0]
# Build per-type translations with only unique (non-shared) fields
- type_translations: Dict[str, Any] = {}
+ type_translations: dict[str, Any] = {}
nested_field_keys: set[str] = set()
for detector_type, nested in all_nested.items():
- type_entry: Dict[str, Any] = {}
+ type_entry: dict[str, Any] = {}
type_entry.update(type_meta.get(detector_type, {}))
unique_fields = {k: v for k, v in nested.items() if k not in shared_fields}
@@ -342,6 +364,64 @@ def main():
continue
section_data.pop(key, None)
+ if field_name == "objects":
+ # Produce a parallel `filters_attribute` block alongside `filters`,
+ # with object-wording rewritten for attribute filters (face,
+ # license_plate, courier logos). The frontend's
+ # buildTranslationPath routes `filters..` lookups to
+ # `filters_attribute.` when `` is in
+ # `model.all_attributes`. Keep this rewrite list explicit rather
+ # than running a blanket s/object/attribute/ so unrelated
+ # descriptions (e.g. "JSON object") never accidentally flip.
+ filters_block = section_data.get("filters")
+ if isinstance(filters_block, dict):
+ attribute_rewrites = [
+ ("Object filters", "Attribute filters"),
+ ("detected objects", "detected attributes"),
+ ("object area", "attribute area"),
+ ("object type", "attribute"),
+ ("the object", "the attribute"),
+ ]
+
+ # Per-field overrides for cases where the generic rewrite
+ # doesn't capture the attribute-specific semantics. Keys
+ # match the FilterConfig field name; values are partial
+ # overrides applied AFTER the generic rewrites.
+ attribute_field_overrides: dict[str, dict[str, str]] = {
+ "min_score": {
+ "description": (
+ "Minimum single-frame detection confidence required "
+ "to associate this attribute with its parent object."
+ ),
+ },
+ }
+
+ def rewrite(text: str) -> str:
+ for source, replacement in attribute_rewrites:
+ text = text.replace(source, replacement)
+ return text
+
+ attribute_variant: dict[str, Any] = {}
+ for key, value in filters_block.items():
+ if key in ("label", "description"):
+ if isinstance(value, str):
+ attribute_variant[key] = rewrite(value)
+ continue
+ if not isinstance(value, dict):
+ continue
+ field_trans: dict[str, str] = {}
+ if isinstance(value.get("label"), str):
+ field_trans["label"] = rewrite(value["label"])
+ if isinstance(value.get("description"), str):
+ field_trans["description"] = rewrite(value["description"])
+ overrides = attribute_field_overrides.get(key)
+ if overrides:
+ field_trans.update(overrides)
+ if field_trans:
+ attribute_variant[key] = field_trans
+ if attribute_variant:
+ section_data["filters_attribute"] = attribute_variant
+
if not section_data:
logger.warning(f"No translations found for section: {field_name}")
continue
diff --git a/notebooks/YOLO_NAS_Pretrained_Export.ipynb b/notebooks/YOLO_NAS_Pretrained_Export.ipynb
index e9ee223149..23c55d1dcb 100644
--- a/notebooks/YOLO_NAS_Pretrained_Export.ipynb
+++ b/notebooks/YOLO_NAS_Pretrained_Export.ipynb
@@ -1,88 +1,95 @@
{
- "cells": [
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "id": "rmuF9iKWTbdk"
- },
- "outputs": [],
- "source": [
- "! pip install -q git+https://github.com/Deci-AI/super-gradients.git"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "id": "NiRCt917KKcL"
- },
- "outputs": [],
- "source": [
- "! sed -i 's/sghub.deci.ai/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"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "id": "dTB0jy_NNSFz"
- },
- "outputs": [],
- "source": [
- "from super_gradients.common.object_names import Models\n",
- "from super_gradients.conversion import DetectionOutputFormatMode\n",
- "from super_gradients.training import models\n",
- "\n",
- "model = models.get(Models.YOLO_NAS_S, pretrained_weights=\"coco\")"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "id": "GymUghyCNXem"
- },
- "outputs": [],
- "source": [
- "# export the model for compatibility with Frigate\n",
- "\n",
- "model.export(\"yolo_nas_s.onnx\",\n",
- " output_predictions_format=DetectionOutputFormatMode.FLAT_FORMAT,\n",
- " max_predictions_per_image=20,\n",
- " num_pre_nms_predictions=300,\n",
- " confidence_threshold=0.4,\n",
- " input_image_shape=(320,320),\n",
- " )"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "id": "uBhXV5g4Nh42"
- },
- "outputs": [],
- "source": [
- "from google.colab import files\n",
- "\n",
- "files.download('yolo_nas_s.onnx')"
- ]
- }
- ],
- "metadata": {
- "colab": {
- "provenance": []
- },
- "kernelspec": {
- "display_name": "Python 3",
- "name": "python3"
- },
- "language_info": {
- "name": "python"
- }
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "runtime-notice"
+ },
+ "source": [
+ "**Before running:** go to **Runtime → Change runtime type → Fallback runtime version: 2025.07** (Python 3.11). The current Colab default (Python 3.12+) is incompatible with `super-gradients`."
+ ]
},
- "nbformat": 4,
- "nbformat_minor": 0
-}
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "rmuF9iKWTbdk"
+ },
+ "outputs": [],
+ "source": [
+ "! pip install -q \"jedi>=0.16\"\n",
+ "! pip install -q git+https://github.com/Deci-AI/super-gradients.git"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "NiRCt917KKcL"
+ },
+ "outputs": [],
+ "source": "! sed -i 's/sghub\\.deci\\.ai/d2gjn4b69gu75n.cloudfront.net/g; s/sg-hub-nv\\.s3\\.amazonaws\\.com/d2gjn4b69gu75n.cloudfront.net/g' /usr/local/lib/python*/dist-packages/super_gradients/training/pretrained_models.py\n! sed -i 's/sghub\\.deci\\.ai/d2gjn4b69gu75n.cloudfront.net/g; s/sg-hub-nv\\.s3\\.amazonaws\\.com/d2gjn4b69gu75n.cloudfront.net/g' /usr/local/lib/python*/dist-packages/super_gradients/training/utils/checkpoint_utils.py"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "dTB0jy_NNSFz"
+ },
+ "outputs": [],
+ "source": [
+ "from super_gradients.common.object_names import Models\n",
+ "from super_gradients.conversion import DetectionOutputFormatMode\n",
+ "from super_gradients.training import models\n",
+ "\n",
+ "model = models.get(Models.YOLO_NAS_S, pretrained_weights=\"coco\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "GymUghyCNXem"
+ },
+ "outputs": [],
+ "source": [
+ "# export the model for compatibility with Frigate\n",
+ "\n",
+ "model.export(\"yolo_nas_s.onnx\",\n",
+ " output_predictions_format=DetectionOutputFormatMode.FLAT_FORMAT,\n",
+ " max_predictions_per_image=20,\n",
+ " num_pre_nms_predictions=300,\n",
+ " confidence_threshold=0.4,\n",
+ " input_image_shape=(320,320),\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "uBhXV5g4Nh42"
+ },
+ "outputs": [],
+ "source": [
+ "from google.colab import files\n",
+ "\n",
+ "files.download('yolo_nas_s.onnx')"
+ ]
+ }
+ ],
+ "metadata": {
+ "colab": {
+ "provenance": []
+ },
+ "kernelspec": {
+ "display_name": "Python 3",
+ "name": "python3"
+ },
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index d17a60e726..775db6026c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,6 @@
+[tool.ruff]
+target-version = "py311"
+
[tool.ruff.lint]
-ignore = ["E501","E711","E712"]
-extend-select = ["I"]
+ignore = ["E501","E711","E712","UP031","UP032","UP042","G004"]
+extend-select = ["I", "UP", "G", "ASYNC210", "B904"]
diff --git a/testing-scripts/analyze_recording_keyframes.py b/testing-scripts/analyze_recording_keyframes.py
new file mode 100644
index 0000000000..982cac82f3
--- /dev/null
+++ b/testing-scripts/analyze_recording_keyframes.py
@@ -0,0 +1,376 @@
+#!/usr/bin/env python3
+"""Analyze keyframe and timestamp structure of Frigate recording segments.
+
+This is a diagnostic tool for investigating seek precision / GOP behavior on
+recorded segments. It does not modify anything.
+
+ffprobe is only available inside the Frigate container, at
+ /usr/lib/ffmpeg/$DEFAULT_FFMPEG_VERSION/bin/ffprobe
+This script auto-resolves that path from the DEFAULT_FFMPEG_VERSION env var
+(or falls back to scanning /usr/lib/ffmpeg/*/bin/ffprobe). Pass --ffprobe to
+override if needed.
+
+All recording segments on the filesystem are in UTC. The --timestamp flag
+expects a UTC Unix timestamp.
+
+Typical use:
+ # Inside the Frigate container (or wherever recordings are mounted)
+ python3 analyze_recording_keyframes.py
+
+ # Analyze 10 most recent segments
+ python3 analyze_recording_keyframes.py --count 10
+
+ # Locate the segment that contains a specific UTC Unix timestamp and
+ # show it plus surrounding segments
+ python3 analyze_recording_keyframes.py --timestamp 1713471234.567
+
+ # Custom recordings directory
+ python3 analyze_recording_keyframes.py --recordings-dir /media/frigate/recordings
+
+ # Override the ffprobe path explicitly
+ python3 analyze_recording_keyframes.py --ffprobe /usr/lib/ffmpeg/7.0/bin/ffprobe
+"""
+
+import argparse
+import datetime
+import json
+import os
+import subprocess
+import sys
+from pathlib import Path
+from statistics import mean, median, stdev
+
+
+def resolve_ffprobe_path(override: str | None) -> str:
+ """Resolve the ffprobe binary path.
+
+ Inside the Frigate container, ffprobe lives at
+ /usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffprobe — the exact version
+ depends on the image build and is exposed as an env var.
+ """
+ if override:
+ return override
+ version = os.environ.get("DEFAULT_FFMPEG_VERSION", "")
+ if version:
+ path = f"/usr/lib/ffmpeg/{version}/bin/ffprobe"
+ if Path(path).is_file():
+ return path
+ # Fall back to scanning the Frigate ffmpeg install root.
+ for candidate in sorted(Path("/usr/lib/ffmpeg").glob("*/bin/ffprobe")):
+ if candidate.is_file():
+ return str(candidate)
+ print(
+ "Could not locate ffprobe. Pass --ffprobe or set "
+ "DEFAULT_FFMPEG_VERSION.",
+ file=sys.stderr,
+ )
+ sys.exit(1)
+
+
+def find_recent_segments(recordings_dir: Path, camera: str, count: int) -> list[Path]:
+ """Return the N most recent .mp4 segments for the given camera.
+
+ Expected layout: ////..mp4
+ """
+ pattern = f"*/*/{camera}/*.mp4"
+ segments = sorted(recordings_dir.glob(pattern))
+ return segments[-count:]
+
+
+def find_segments_near_timestamp(
+ recordings_dir: Path, camera: str, target_ts: float, count: int
+) -> tuple[list[Path], Path | None]:
+ """Return `count` segments centered on the one containing `target_ts`.
+
+ Also returns the specific segment that should contain the timestamp, so
+ callers can highlight it in output.
+ """
+ pattern = f"*/*/{camera}/*.mp4"
+ with_ts: list[tuple[float, Path]] = []
+ for seg in sorted(recordings_dir.glob(pattern)):
+ ts = filename_to_timestamp(seg)
+ if ts is not None:
+ with_ts.append((ts, seg))
+
+ if not with_ts:
+ return [], None
+
+ # Largest filename_ts that is <= target_ts — that's the segment that
+ # should contain the timestamp (Frigate catalogs segments by filename).
+ target_idx = -1
+ for i, (ts, _) in enumerate(with_ts):
+ if ts <= target_ts:
+ target_idx = i
+ else:
+ break
+
+ if target_idx < 0:
+ # target_ts is before the earliest segment we have — just return the
+ # first `count` segments so the user can see what's available.
+ window = with_ts[:count]
+ return [seg for _, seg in window], None
+
+ half = count // 2
+ start = max(0, target_idx - half)
+ end = min(len(with_ts), start + count)
+ start = max(0, end - count)
+
+ window = with_ts[start:end]
+ return [seg for _, seg in window], with_ts[target_idx][1]
+
+
+def filename_to_timestamp(segment: Path) -> float | None:
+ """Parse the wall-clock time from Frigate's segment path layout."""
+ try:
+ date = segment.parent.parent.parent.name # YYYY-MM-DD
+ hour = segment.parent.parent.name # HH
+ mm_ss = segment.stem # MM.SS
+ minute, second = mm_ss.split(".")
+ dt = datetime.datetime.strptime(
+ f"{date} {hour}:{minute}:{second}",
+ "%Y-%m-%d %H:%M:%S",
+ ).replace(tzinfo=datetime.timezone.utc)
+ return dt.timestamp()
+ except (ValueError, IndexError):
+ return None
+
+
+def run_ffprobe(ffprobe: str, args: list[str]) -> dict:
+ """Run ffprobe and return parsed JSON, or empty dict on failure."""
+ result = subprocess.run(
+ [ffprobe, "-v", "error", *args, "-of", "json"],
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+ if result.returncode != 0:
+ print(f" ffprobe error: {result.stderr.strip()}", file=sys.stderr)
+ return {}
+ try:
+ return json.loads(result.stdout)
+ except json.JSONDecodeError:
+ return {}
+
+
+def get_format_info(ffprobe: str, segment: Path) -> tuple[dict, dict]:
+ """Return (format_dict, stream_dict) for the first video stream."""
+ data = run_ffprobe(
+ ffprobe,
+ [
+ "-show_entries",
+ "format=duration,start_time",
+ "-show_entries",
+ "stream=codec_name,profile,r_frame_rate,width,height",
+ "-select_streams",
+ "v:0",
+ str(segment),
+ ],
+ )
+ fmt = data.get("format", {})
+ streams = data.get("streams") or [{}]
+ return fmt, streams[0]
+
+
+def get_video_packets(ffprobe: str, segment: Path) -> list[dict]:
+ """Return video packets with pts_time and flags."""
+ data = run_ffprobe(
+ ffprobe,
+ [
+ "-select_streams",
+ "v",
+ "-show_entries",
+ "packet=pts_time,dts_time,flags",
+ str(segment),
+ ],
+ )
+ return data.get("packets", [])
+
+
+def analyze(ffprobe: str, segment: Path, highlight: bool = False) -> None:
+ marker = " <-- contains target timestamp" if highlight else ""
+ print(f"\n=== {segment} ==={marker}")
+
+ fmt, stream = get_format_info(ffprobe, segment)
+ duration = float(fmt.get("duration", 0) or 0)
+ start_time = float(fmt.get("start_time", 0) or 0)
+ codec = stream.get("codec_name", "?")
+ profile = stream.get("profile", "?")
+ width = stream.get("width", "?")
+ height = stream.get("height", "?")
+ fps = stream.get("r_frame_rate", "?/1")
+
+ filename_ts = filename_to_timestamp(segment)
+ filename_iso = (
+ datetime.datetime.fromtimestamp(
+ filename_ts, tz=datetime.timezone.utc
+ ).isoformat()
+ if filename_ts is not None
+ else "?"
+ )
+
+ print(f" Codec: {codec} ({profile}) {width}x{height} {fps}")
+ print(f" Filename time: {filename_ts} ({filename_iso})")
+ print(f" Format duration: {duration:.3f}s")
+ print(f" Format start: {start_time:.3f}s (PTS offset of first packet)")
+
+ packets = get_video_packets(ffprobe, segment)
+ if not packets:
+ print(" (no video packets)")
+ return
+
+ keyframe_times: list[float] = []
+ first_pts: float | None = None
+ last_pts: float | None = None
+
+ for pkt in packets:
+ pts_str = pkt.get("pts_time")
+ if pts_str is None or pts_str == "N/A":
+ continue
+ pts = float(pts_str)
+ if first_pts is None:
+ first_pts = pts
+ last_pts = pts
+ if "K" in pkt.get("flags", ""):
+ keyframe_times.append(pts)
+
+ total_packets = len(packets)
+ kf_count = len(keyframe_times)
+
+ print(f" Video packets: {total_packets}")
+ print(f" Keyframes: {kf_count}")
+ if first_pts is not None and last_pts is not None:
+ print(
+ f" Packet PTS: first={first_pts:.3f}s last={last_pts:.3f}s "
+ f"span={last_pts - first_pts:.3f}s"
+ )
+
+ if keyframe_times:
+ print(
+ f" Keyframe PTS: first={keyframe_times[0]:.3f}s "
+ f"last={keyframe_times[-1]:.3f}s"
+ )
+ formatted = ", ".join(f"{t:.3f}" for t in keyframe_times)
+ print(f" Keyframe times: [{formatted}]")
+
+ if len(keyframe_times) >= 2:
+ gaps = [b - a for a, b in zip(keyframe_times, keyframe_times[1:])]
+ avg_fps_estimate = (
+ total_packets / (last_pts - first_pts)
+ if last_pts and first_pts is not None and last_pts > first_pts
+ else 0
+ )
+ print(
+ f" GOP gaps (s): min={min(gaps):.3f} max={max(gaps):.3f} "
+ f"mean={mean(gaps):.3f} median={median(gaps):.3f}"
+ )
+ if len(gaps) > 1:
+ print(f" stdev={stdev(gaps):.3f}")
+ print(
+ f" Est. mean GOP: ~{mean(gaps) * avg_fps_estimate:.1f} frames"
+ if avg_fps_estimate
+ else ""
+ )
+ if max(gaps) > 5:
+ print(
+ " !! Max GOP > 5s — consistent with adaptive/smart codec "
+ "(even if 'Smart Codec' is off in the UI, some cameras still "
+ "produce irregular GOPs under specific encoder profiles)"
+ )
+ elif kf_count == 1:
+ print(" !! Only one keyframe in segment — very long GOP")
+
+ # Report how well filename time aligns with first-packet PTS.
+ # (Filename time is what Frigate uses as recording.start_time in the DB.)
+ if filename_ts is not None and first_pts is not None:
+ print(
+ f" Notes: first packet PTS is {first_pts:.3f}s into the file; "
+ f"Frigate treats filename time as PTS=0 for seek math."
+ )
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(
+ description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ parser.add_argument("camera", help="Camera name (matches the recordings subfolder)")
+ parser.add_argument(
+ "--count",
+ type=int,
+ default=5,
+ help="Number of most recent segments to analyze (default: 5)",
+ )
+ parser.add_argument(
+ "--recordings-dir",
+ default="/media/frigate/recordings",
+ help="Path to the recordings directory (default: /media/frigate/recordings)",
+ )
+ parser.add_argument(
+ "--ffprobe",
+ default=None,
+ help=(
+ "Full path to the ffprobe binary. Defaults to the Frigate-bundled "
+ "binary at /usr/lib/ffmpeg/$DEFAULT_FFMPEG_VERSION/bin/ffprobe."
+ ),
+ )
+ parser.add_argument(
+ "--timestamp",
+ type=float,
+ default=None,
+ help=(
+ "Unix timestamp (UTC seconds, decimals allowed) to locate. The "
+ "script finds the segment that should contain this time and "
+ "analyzes it plus surrounding segments (count controls the "
+ "window). All on-disk segments are stored in UTC, so pass a UTC "
+ "Unix timestamp."
+ ),
+ )
+ args = parser.parse_args()
+
+ ffprobe = resolve_ffprobe_path(args.ffprobe)
+
+ recordings_dir = Path(args.recordings_dir)
+ if not recordings_dir.is_dir():
+ print(
+ f"Recordings directory not found: {recordings_dir}",
+ file=sys.stderr,
+ )
+ sys.exit(1)
+
+ target_segment: Path | None = None
+ if args.timestamp is not None:
+ segments, target_segment = find_segments_near_timestamp(
+ recordings_dir, args.camera, args.timestamp, args.count
+ )
+ target_iso = datetime.datetime.fromtimestamp(
+ args.timestamp, tz=datetime.timezone.utc
+ ).isoformat()
+ mode = f"around timestamp {args.timestamp} ({target_iso})"
+ else:
+ segments = find_recent_segments(recordings_dir, args.camera, args.count)
+ mode = "most recent"
+
+ if not segments:
+ print(
+ f"No segments found for camera '{args.camera}' under {recordings_dir}",
+ file=sys.stderr,
+ )
+ sys.exit(1)
+
+ if args.timestamp is not None and target_segment is None:
+ print(
+ f"!! Target timestamp {args.timestamp} is before the earliest "
+ f"segment on disk; showing the earliest available segments instead.",
+ file=sys.stderr,
+ )
+
+ print(
+ f"Analyzing {len(segments)} {mode} segment(s) for camera "
+ f"'{args.camera}' under {recordings_dir} (ffprobe: {ffprobe})"
+ )
+ for segment in segments:
+ analyze(ffprobe, segment, highlight=(segment == target_segment))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/benchmark.py b/testing-scripts/benchmark.py
similarity index 100%
rename from benchmark.py
rename to testing-scripts/benchmark.py
diff --git a/benchmark_motion.py b/testing-scripts/benchmark_motion.py
similarity index 100%
rename from benchmark_motion.py
rename to testing-scripts/benchmark_motion.py
diff --git a/testing-scripts/face_dataset.py b/testing-scripts/face_dataset.py
new file mode 100644
index 0000000000..0c9e451d1b
--- /dev/null
+++ b/testing-scripts/face_dataset.py
@@ -0,0 +1,783 @@
+"""
+Face recognition investigation script.
+
+Standalone replica of Frigate's ArcFace pipeline (see
+frigate/data_processing/common/face/model.py and
+frigate/embeddings/onnx/face_embedding.py) for analyzing a face collection
+outside the running service. Useful for:
+
+ - Diagnosing why a person's collection produces false positives
+ - Finding outlier/contaminating training images
+ - Inspecting the effect of the shipped vector-wise outlier filter
+
+Layout:
+ - Core pipeline: LandmarkAligner, ArcFaceEmbedder, arcface_preprocess,
+ similarity_to_confidence, blur_reduction — all mirroring the production
+ code exactly
+ - Default run: summarize positive and negative sets against a baseline
+ trim_mean class representation
+ - Optional diagnostics (flags): vector-outlier filter behavior, degenerate
+ "tiny crop" embedding clustering, and multi-identity contamination
+
+Usage:
+ python3 face_investigate.py \\
+ --positive \\
+ --negative \\
+ [--model-cache /path/to/model_cache] \\
+ [--vector-outlier] [--degenerate] [--contamination]
+
+The positive folder should contain training images for a single identity
+(same layout as FACE_DIR//*.webp). The negative folder should contain
+runtime crops to test against — a mix of true matches and misfires.
+"""
+
+from __future__ import annotations
+
+import argparse
+import os
+import sys
+from dataclasses import dataclass
+from typing import Iterable
+
+import cv2
+import numpy as np
+import onnxruntime as ort
+from PIL import Image
+from scipy import stats
+
+ARCFACE_INPUT_SIZE = 112
+
+
+# ---------------------------------------------------------------------------
+# Replicated Frigate pipeline
+# ---------------------------------------------------------------------------
+
+
+def _process_image_frigate(image: np.ndarray) -> Image.Image:
+ """Mirror BaseEmbedding._process_image for an ndarray input.
+
+ NOTE: Frigate passes the output of `cv2.imread` (BGR) directly in. PIL's
+ `Image.fromarray` does NOT reorder channels, so the embedder effectively
+ receives a BGR-ordered tensor. We replicate that faithfully here. (Tested
+ — swapping to RGB produces near-identical embeddings; this model is
+ robust to channel order.)
+ """
+ return Image.fromarray(image)
+
+
+def arcface_preprocess(image_bgr: np.ndarray) -> np.ndarray:
+ """Mirror ArcfaceEmbedding._preprocess_inputs."""
+ pil = _process_image_frigate(image_bgr)
+
+ width, height = pil.size
+ if width != ARCFACE_INPUT_SIZE or height != ARCFACE_INPUT_SIZE:
+ if width > height:
+ new_height = int(((height / width) * ARCFACE_INPUT_SIZE) // 4 * 4)
+ pil = pil.resize((ARCFACE_INPUT_SIZE, new_height))
+ else:
+ new_width = int(((width / height) * ARCFACE_INPUT_SIZE) // 4 * 4)
+ pil = pil.resize((new_width, ARCFACE_INPUT_SIZE))
+
+ og = np.array(pil).astype(np.float32)
+ og_h, og_w, channels = og.shape
+
+ frame = np.zeros(
+ (ARCFACE_INPUT_SIZE, ARCFACE_INPUT_SIZE, channels), dtype=np.float32
+ )
+ x_center = (ARCFACE_INPUT_SIZE - og_w) // 2
+ y_center = (ARCFACE_INPUT_SIZE - og_h) // 2
+ frame[y_center : y_center + og_h, x_center : x_center + og_w] = og
+
+ frame = (frame / 127.5) - 1.0
+ frame = np.transpose(frame, (2, 0, 1))
+ frame = np.expand_dims(frame, axis=0)
+ return frame
+
+
+class LandmarkAligner:
+ """Mirror FaceRecognizer.align_face."""
+
+ def __init__(self, landmark_model_path: str):
+ if not os.path.exists(landmark_model_path):
+ raise FileNotFoundError(landmark_model_path)
+ self.detector = cv2.face.createFacemarkLBF()
+ self.detector.loadModel(landmark_model_path)
+
+ def align(
+ self, image: np.ndarray, out_w: int, out_h: int
+ ) -> tuple[np.ndarray, dict]:
+ land_image = (
+ cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) if image.ndim == 3 else image
+ )
+ _, lands = self.detector.fit(
+ land_image, np.array([(0, 0, land_image.shape[1], land_image.shape[0])])
+ )
+ landmarks = lands[0][0]
+
+ leftEyePts = landmarks[42:48]
+ rightEyePts = landmarks[36:42]
+ leftEyeCenter = leftEyePts.mean(axis=0).astype("int")
+ rightEyeCenter = rightEyePts.mean(axis=0).astype("int")
+
+ dY = rightEyeCenter[1] - leftEyeCenter[1]
+ dX = rightEyeCenter[0] - leftEyeCenter[0]
+ angle = np.degrees(np.arctan2(dY, dX)) - 180
+ dist = float(np.sqrt((dX**2) + (dY**2)))
+
+ desiredRightEyeX = 1.0 - 0.35
+ desiredDist = (desiredRightEyeX - 0.35) * out_w
+ scale = desiredDist / dist if dist > 0 else 1.0
+
+ eyesCenter = (
+ int((leftEyeCenter[0] + rightEyeCenter[0]) // 2),
+ int((leftEyeCenter[1] + rightEyeCenter[1]) // 2),
+ )
+ M = cv2.getRotationMatrix2D(eyesCenter, angle, scale)
+ tX = out_w * 0.5
+ tY = out_h * 0.35
+ M[0, 2] += tX - eyesCenter[0]
+ M[1, 2] += tY - eyesCenter[1]
+
+ aligned = cv2.warpAffine(
+ image, M, (out_w, out_h), flags=cv2.INTER_CUBIC
+ )
+ info = dict(
+ angle=float(angle),
+ eye_dist_px=dist,
+ scale=float(scale),
+ landmarks=landmarks,
+ )
+ return aligned, info
+
+
+class ArcFaceEmbedder:
+ def __init__(self, model_path: str):
+ self.session = ort.InferenceSession(
+ model_path, providers=["CPUExecutionProvider"]
+ )
+ self.input_name = self.session.get_inputs()[0].name
+
+ def embed(self, image_bgr: np.ndarray) -> np.ndarray:
+ tensor = arcface_preprocess(image_bgr)
+ out = self.session.run(None, {self.input_name: tensor})[0]
+ return out.squeeze()
+
+
+def similarity_to_confidence(
+ cos_sim: float,
+ median: float = 0.3,
+ range_width: float = 0.6,
+ slope_factor: float = 12,
+) -> float:
+ slope = slope_factor / range_width
+ return float(1.0 / (1.0 + np.exp(-slope * (cos_sim - median))))
+
+
+def laplacian_variance(image: np.ndarray) -> float:
+ return float(cv2.Laplacian(image, cv2.CV_64F).var())
+
+
+def blur_reduction(variance: float) -> float:
+ if variance < 120:
+ return 0.06
+ elif variance < 160:
+ return 0.04
+ elif variance < 200:
+ return 0.02
+ elif variance < 250:
+ return 0.01
+ return 0.0
+
+
+def cosine(a: np.ndarray, b: np.ndarray) -> float:
+ denom = np.linalg.norm(a) * np.linalg.norm(b)
+ if denom == 0:
+ return 0.0
+ return float(np.dot(a, b) / denom)
+
+
+def l2(v: np.ndarray) -> np.ndarray:
+ return v / (np.linalg.norm(v) + 1e-9)
+
+
+# ---------------------------------------------------------------------------
+# Sample loading
+# ---------------------------------------------------------------------------
+
+
+@dataclass
+class FaceSample:
+ path: str
+ shape: tuple[int, int]
+ embedding: np.ndarray
+ blur_var: float
+ align_info: dict
+
+
+def load_folder(
+ folder: str, aligner: LandmarkAligner, embedder: ArcFaceEmbedder
+) -> list[FaceSample]:
+ samples: list[FaceSample] = []
+ names = sorted(os.listdir(folder))
+ for name in names:
+ if name.startswith("."):
+ continue
+ path = os.path.join(folder, name)
+ if not os.path.isfile(path):
+ continue
+ img = cv2.imread(path)
+ if img is None:
+ print(f" [skip unreadable] {name}")
+ continue
+ aligned, info = aligner.align(img, img.shape[1], img.shape[0])
+ emb = embedder.embed(aligned)
+ samples.append(
+ FaceSample(
+ path=path,
+ shape=(img.shape[1], img.shape[0]),
+ embedding=emb,
+ blur_var=laplacian_variance(img),
+ align_info=info,
+ )
+ )
+ return samples
+
+
+def trimmed_mean(embs: Iterable[np.ndarray], trim: float = 0.15) -> np.ndarray:
+ arr = np.stack(list(embs), axis=0)
+ return stats.trim_mean(arr, trim, axis=0)
+
+
+# ---------------------------------------------------------------------------
+# Baseline analyses (always run)
+# ---------------------------------------------------------------------------
+
+
+def summarize_positive(samples: list[FaceSample], mean_emb: np.ndarray) -> None:
+ """Summary of training set: per-sample cos to class mean, intra-class stats.
+
+ Outliers with cos far below the rest are likely degrading the mean —
+ they'd be the first candidates the shipped vector-outlier filter drops.
+ """
+ print("\n" + "=" * 78)
+ print(f"POSITIVE SET ANALYSIS ({len(samples)} images)")
+ print("=" * 78)
+
+ rows = []
+ for s in samples:
+ cs = cosine(s.embedding, mean_emb)
+ conf = similarity_to_confidence(cs)
+ red = blur_reduction(s.blur_var)
+ rows.append(
+ dict(
+ name=os.path.basename(s.path),
+ shape=f"{s.shape[0]}x{s.shape[1]}",
+ eye_px=s.align_info["eye_dist_px"],
+ angle=s.align_info["angle"] + 180,
+ blur=s.blur_var,
+ cos=cs,
+ conf=conf,
+ red=red,
+ adj_conf=max(0.0, conf - red),
+ )
+ )
+
+ rows.sort(key=lambda r: r["cos"])
+ sims = np.array([r["cos"] for r in rows])
+ print(
+ f"\nCosine-to-trimmed-mean: mean={sims.mean():.3f} std={sims.std():.3f} "
+ f"min={sims.min():.3f} max={sims.max():.3f}"
+ )
+
+ print("\n-- Worst matches (bottom 10, most likely hurting the mean) --")
+ print(
+ f"{'cos':>6} {'conf':>6} {'blur':>7} {'eyes':>6} "
+ f"{'angle':>6} {'shape':>9} name"
+ )
+ for r in rows[:10]:
+ print(
+ f"{r['cos']:6.3f} {r['conf']:6.3f} {r['blur']:7.1f} "
+ f"{r['eye_px']:6.1f} {r['angle']:6.1f} {r['shape']:>9} {r['name']}"
+ )
+
+ print("\n-- Best matches (top 5) --")
+ for r in rows[-5:][::-1]:
+ print(
+ f"{r['cos']:6.3f} {r['conf']:6.3f} {r['blur']:7.1f} "
+ f"{r['eye_px']:6.1f} {r['angle']:6.1f} {r['shape']:>9} {r['name']}"
+ )
+
+ # Pairwise analysis — flags embeddings poorly correlated with the rest
+ print("\n-- Pairwise intra-class similarity (mean cos vs. other positives) --")
+ embs = np.stack([s.embedding for s in samples], axis=0)
+ norms = embs / (np.linalg.norm(embs, axis=1, keepdims=True) + 1e-9)
+ sim_matrix = norms @ norms.T
+ np.fill_diagonal(sim_matrix, np.nan)
+ mean_pairwise = np.nanmean(sim_matrix, axis=1)
+ names = [os.path.basename(s.path) for s in samples]
+ ordered = sorted(zip(names, mean_pairwise), key=lambda t: t[1])
+ print(f"{'mean_cos':>9} name")
+ for nm, mp in ordered[:10]:
+ print(f"{mp:9.3f} {nm}")
+ print(f"\n overall mean pairwise cos: {np.nanmean(sim_matrix):.3f}")
+ print(f" median pairwise cos: {np.nanmedian(sim_matrix):.3f}")
+
+
+def summarize_negative(
+ neg_samples: list[FaceSample],
+ mean_emb: np.ndarray,
+ pos_samples: list[FaceSample],
+) -> None:
+ """Score each negative against the class mean, then show its top-3
+ nearest positives. High-scoring negatives that match specific outlier
+ positives hint at training-set contamination.
+ """
+ print("\n" + "=" * 78)
+ print(f"NEGATIVE SET ANALYSIS ({len(neg_samples)} images)")
+ print("=" * 78)
+ print(
+ f"\n{'cos':>6} {'conf':>6} {'red':>5} {'adj':>5} "
+ f"{'blur':>7} {'eyes':>6} {'shape':>9} name"
+ )
+ for s in neg_samples:
+ cs = cosine(s.embedding, mean_emb)
+ conf = similarity_to_confidence(cs)
+ red = blur_reduction(s.blur_var)
+ print(
+ f"{cs:6.3f} {conf:6.3f} {red:5.2f} {max(0, conf - red):5.2f} "
+ f"{s.blur_var:7.1f} {s.align_info['eye_dist_px']:6.1f} "
+ f"{s.shape[0]}x{s.shape[1]:<5} {os.path.basename(s.path)}"
+ )
+
+ print("\n-- For each negative, top-3 most similar positives --")
+ pos_embs = np.stack([p.embedding for p in pos_samples])
+ pos_norm = pos_embs / (np.linalg.norm(pos_embs, axis=1, keepdims=True) + 1e-9)
+ for s in neg_samples:
+ v = s.embedding / (np.linalg.norm(s.embedding) + 1e-9)
+ sims = pos_norm @ v
+ idx = np.argsort(-sims)[:3]
+ print(f"\n {os.path.basename(s.path)}:")
+ for i in idx:
+ print(
+ f" {sims[i]:6.3f} {os.path.basename(pos_samples[i].path)} "
+ f"blur={pos_samples[i].blur_var:.1f} "
+ f"eyes={pos_samples[i].align_info['eye_dist_px']:.1f}"
+ )
+
+
+# ---------------------------------------------------------------------------
+# Optional diagnostics
+# ---------------------------------------------------------------------------
+
+
+def vector_outlier_test(
+ pos: list[FaceSample], neg: list[FaceSample], base_trim: float = 0.15
+) -> None:
+ """Measure the shipped vector-wise outlier filter at various thresholds.
+
+ The production filter at `build_class_mean` in
+ frigate/data_processing/common/face/model.py uses T=0.30. This test
+ sweeps T so you can see which images would be dropped on a new collection
+ and how that affects the negative scores.
+
+ Algorithm: iteratively recompute trim_mean on the kept set, drop any
+ embedding with cos < T to that mean, repeat until converged. Floor at
+ 50% of the collection to avoid collapse.
+ """
+ print("\n" + "=" * 78)
+ print("VECTOR-WISE OUTLIER PRE-FILTER — layered on trim_mean(0.15)")
+ print("=" * 78)
+
+ all_embs = np.stack([s.embedding for s in pos])
+
+ def iterative_mean(
+ embs: np.ndarray,
+ threshold: float,
+ iters: int = 3,
+ min_keep_frac: float = 0.5,
+ ) -> tuple[np.ndarray, np.ndarray]:
+ keep = np.ones(len(embs), dtype=bool)
+ floor = max(5, int(np.ceil(min_keep_frac * len(embs))))
+ for _ in range(iters):
+ m = stats.trim_mean(embs[keep], base_trim, axis=0)
+ m_norm = m / (np.linalg.norm(m) + 1e-9)
+ e_norms = embs / (np.linalg.norm(embs, axis=1, keepdims=True) + 1e-9)
+ cos_to_mean = e_norms @ m_norm
+ new_keep = cos_to_mean >= threshold
+ if new_keep.sum() < floor:
+ top_idx = np.argsort(-cos_to_mean)[:floor]
+ new_keep = np.zeros_like(new_keep)
+ new_keep[top_idx] = True
+ if np.array_equal(new_keep, keep):
+ break
+ keep = new_keep
+ final = stats.trim_mean(embs[keep], base_trim, axis=0)
+ return final, keep
+
+ provisional = stats.trim_mean(all_embs, base_trim, axis=0)
+ p_norm = provisional / (np.linalg.norm(provisional) + 1e-9)
+ e_norms_all = all_embs / (np.linalg.norm(all_embs, axis=1, keepdims=True) + 1e-9)
+ cos_to_prov = e_norms_all @ p_norm
+ print("\nDistribution of cos(positive, provisional trim_mean):")
+ print(
+ f" min={cos_to_prov.min():.3f} p10={np.percentile(cos_to_prov, 10):.3f} "
+ f"p25={np.percentile(cos_to_prov, 25):.3f} "
+ f"median={np.median(cos_to_prov):.3f} "
+ f"p75={np.percentile(cos_to_prov, 75):.3f} max={cos_to_prov.max():.3f}"
+ )
+
+ baseline_mean = stats.trim_mean(all_embs, base_trim, axis=0)
+ baseline_pos = np.array([cosine(p.embedding, baseline_mean) for p in pos])
+ baseline_neg = (
+ np.array([cosine(n.embedding, baseline_mean) for n in neg])
+ if neg
+ else np.array([])
+ )
+ baseline_conf_neg = np.array(
+ [similarity_to_confidence(c) for c in baseline_neg]
+ )
+
+ print(
+ f"\nBaseline (trim_mean only, {len(pos)} images):"
+ f"\n pos cos min={baseline_pos.min():.3f} "
+ f"mean={baseline_pos.mean():.3f} max={baseline_pos.max():.3f}"
+ )
+ if len(neg):
+ print(
+ f" neg cos min={baseline_neg.min():.3f} "
+ f"mean={baseline_neg.mean():.3f} max={baseline_neg.max():.3f}"
+ )
+ print(
+ f" neg conf min={baseline_conf_neg.min():.3f} "
+ f"mean={baseline_conf_neg.mean():.3f} max={baseline_conf_neg.max():.3f}"
+ )
+ print(
+ f" margin (pos.min - neg.max): "
+ f"{baseline_pos.min() - baseline_neg.max():+.3f}"
+ )
+
+ print("\nIterative (refine mean → drop vectors with cos5} {'kept':>6} {'pos min':>7} {'pos mean':>8} "
+ f"{'neg max':>7} {'neg mean':>8} {'neg conf.max':>12} {'margin':>7}"
+ )
+ for T in [0.15, 0.20, 0.25, 0.28, 0.30, 0.33, 0.36, 0.40]:
+ mean, keep = iterative_mean(all_embs, T)
+ pos_sims = np.array([cosine(p.embedding, mean) for p in pos])
+ neg_sims = (
+ np.array([cosine(n.embedding, mean) for n in neg])
+ if neg
+ else np.array([])
+ )
+ neg_conf = np.array([similarity_to_confidence(c) for c in neg_sims])
+ margin = pos_sims.min() - (neg_sims.max() if len(neg_sims) else 0)
+ print(
+ f"{T:5.2f} {int(keep.sum()):>3}/{len(pos):<2} "
+ f"{pos_sims.min():7.3f} {pos_sims.mean():8.3f} "
+ f"{neg_sims.max() if len(neg_sims) else float('nan'):7.3f} "
+ f"{neg_sims.mean() if len(neg_sims) else float('nan'):8.3f} "
+ f"{neg_conf.max() if len(neg_conf) else float('nan'):12.3f} "
+ f"{margin:+7.3f}"
+ )
+
+ # Show which images get dropped at the shipped threshold + neighbors
+ for T_show in (0.25, 0.30, 0.33):
+ _, keep = iterative_mean(all_embs, T_show)
+ print(
+ f"\nAt T={T_show}, the {int((~keep).sum())} dropped positives are:"
+ )
+ final_mean = stats.trim_mean(all_embs[keep], base_trim, axis=0)
+ m_n = final_mean / (np.linalg.norm(final_mean) + 1e-9)
+ for i, (p, k) in enumerate(zip(pos, keep)):
+ if not k:
+ e_n = p.embedding / (np.linalg.norm(p.embedding) + 1e-9)
+ cos_final = float(e_n @ m_n)
+ print(
+ f" cos_to_clean_mean={cos_final:6.3f} "
+ f"shape={p.shape[0]}x{p.shape[1]} "
+ f"eyes={p.align_info['eye_dist_px']:6.1f} "
+ f"blur={p.blur_var:7.1f} "
+ f"{os.path.basename(p.path)}"
+ )
+
+
+def degenerate_embedding_test(
+ pos: list[FaceSample], neg: list[FaceSample]
+) -> None:
+ """Detect whether negatives and low-quality positives share a degenerate
+ 'tiny/noisy face' region of the embedding space.
+
+ Signal: if neg-to-neg cos is higher than pos-to-pos cos, the negatives
+ aren't really per-identity embeddings — they're dominated by upsample /
+ low-resolution artifacts that all map to a similar corner of embedding
+ space regardless of who the face belongs to.
+
+ Also rebuilds the mean using only high-intra-similarity positives to
+ show whether a cleaner training set separates the negatives.
+ """
+ print("\n" + "=" * 78)
+ print("DEGENERATE-EMBEDDING TEST")
+ print("=" * 78)
+
+ pos_embs = np.stack([l2(s.embedding) for s in pos])
+ neg_embs = np.stack([l2(s.embedding) for s in neg])
+
+ nn = neg_embs @ neg_embs.T
+ np.fill_diagonal(nn, np.nan)
+ pp = pos_embs @ pos_embs.T
+ np.fill_diagonal(pp, np.nan)
+ pn = pos_embs @ neg_embs.T
+
+ print(
+ f"\n neg<->neg mean cos : {np.nanmean(nn):.3f} "
+ f"(how tightly negatives cluster together)"
+ )
+ print(
+ f" pos<->pos mean cos : {np.nanmean(pp):.3f} "
+ f"(how tightly positives cluster)"
+ )
+ print(
+ f" pos<->neg mean cos : {pn.mean():.3f} "
+ f"(cross-class — should be low for a clean class)"
+ )
+ if np.nanmean(nn) > np.nanmean(pp):
+ print(
+ "\n >> neg<->neg > pos<->pos: negatives cluster more tightly than\n"
+ " positives. This is the degenerate-embedding signature —\n"
+ " upsampled tiny crops share a common 'face-like blob' region\n"
+ " regardless of identity."
+ )
+
+ mean_intra = np.nanmean(pp, axis=1)
+ for thresh in (0.30, 0.33, 0.36):
+ keep = mean_intra >= thresh
+ if keep.sum() < 5:
+ continue
+ clean_embs = [pos[i].embedding for i in range(len(pos)) if keep[i]]
+ clean_mean = stats.trim_mean(np.stack(clean_embs), 0.15, axis=0)
+ neg_scores = np.array([cosine(n.embedding, clean_mean) for n in neg])
+ neg_confs = np.array([similarity_to_confidence(c) for c in neg_scores])
+ pos_scores = np.array(
+ [
+ cosine(pos[i].embedding, clean_mean)
+ for i in range(len(pos))
+ if keep[i]
+ ]
+ )
+ print(
+ f"\n mean_intra >= {thresh}: keeping {int(keep.sum())}/{len(pos)} positives"
+ )
+ print(
+ f" pos cos vs mean : min={pos_scores.min():.3f} "
+ f"mean={pos_scores.mean():.3f} max={pos_scores.max():.3f}"
+ )
+ print(
+ f" neg cos vs mean : min={neg_scores.min():.3f} "
+ f"mean={neg_scores.mean():.3f} max={neg_scores.max():.3f}"
+ )
+ print(
+ f" neg conf : min={neg_confs.min():.3f} "
+ f"mean={neg_confs.mean():.3f} max={neg_confs.max():.3f}"
+ )
+ print(
+ f" margin (pos.min - neg.max): "
+ f"{pos_scores.min() - neg_scores.max():+.3f}"
+ )
+
+
+def contamination_analysis(
+ pos: list[FaceSample], neg: list[FaceSample]
+) -> None:
+ """Check whether the positive collection contains a second identity.
+
+ Two signals:
+ (a) Per-positive: if an image is closer to at least one negative than
+ to the rest of the positive class, it's likely a mislabeled face.
+ (b) 2-means split of the positive embeddings: if one cluster center
+ lands close to the negative mean, that cluster is a contaminating
+ sub-identity that's pulling the class mean toward the negatives.
+ """
+ print("\n" + "=" * 78)
+ print("CONTAMINATION ANALYSIS")
+ print("=" * 78)
+
+ pos_embs = np.stack([l2(s.embedding) for s in pos])
+ neg_embs = np.stack([l2(s.embedding) for s in neg])
+ pos_names = [os.path.basename(s.path) for s in pos]
+
+ pos_pos = pos_embs @ pos_embs.T
+ np.fill_diagonal(pos_pos, np.nan)
+ pos_neg = pos_embs @ neg_embs.T
+
+ mean_intra = np.nanmean(pos_pos, axis=1)
+ max_to_neg = pos_neg.max(axis=1)
+ mean_to_neg = pos_neg.mean(axis=1)
+
+ print(
+ "\nPositives closer to a negative than to their own class avg"
+ "\n(these are candidates for mislabeled images):"
+ )
+ print(
+ f"\n{'max_neg':>7} {'mean_neg':>8} {'mean_intra':>10} "
+ f"{'delta':>6} name"
+ )
+ rows = list(zip(pos_names, max_to_neg, mean_to_neg, mean_intra))
+ rows.sort(key=lambda r: -(r[1] - r[3]))
+ for nm, mxn, mnn, mi in rows[:15]:
+ delta = mxn - mi
+ marker = " <<" if delta > 0 else ""
+ print(f"{mxn:7.3f} {mnn:8.3f} {mi:10.3f} {delta:6.3f} {nm}{marker}")
+
+ # 2-means in cosine space (no sklearn dependency).
+ print("\n2-means split of positive embeddings (cosine space):")
+ rng = np.random.default_rng(0)
+ best = None
+ for _ in range(5):
+ idx = rng.choice(len(pos_embs), 2, replace=False)
+ centers = pos_embs[idx].copy()
+ for _ in range(50):
+ sims = pos_embs @ centers.T
+ labels = np.argmax(sims, axis=1)
+ new_centers = np.stack(
+ [
+ l2(pos_embs[labels == k].mean(axis=0))
+ if np.any(labels == k)
+ else centers[k]
+ for k in range(2)
+ ]
+ )
+ if np.allclose(new_centers, centers):
+ break
+ centers = new_centers
+ tight = float(np.mean([sims[i, labels[i]] for i in range(len(labels))]))
+ if best is None or tight > best[0]:
+ best = (tight, labels.copy(), centers.copy())
+
+ _, labels, centers = best
+ sizes = [int((labels == k).sum()) for k in range(2)]
+ neg_mean = l2(neg_embs.mean(axis=0))
+ print(
+ f" cluster 0: size={sizes[0]:>2} "
+ f"center<->other_center_cos={float(centers[0] @ centers[1]):.3f} "
+ f"center<->neg_mean_cos={float(centers[0] @ neg_mean):.3f}"
+ )
+ print(
+ f" cluster 1: size={sizes[1]:>2} "
+ f"center<->neg_mean_cos={float(centers[1] @ neg_mean):.3f}"
+ )
+
+ neg_aligned = 0 if centers[0] @ neg_mean > centers[1] @ neg_mean else 1
+ print(
+ f"\n cluster {neg_aligned} is more similar to the negatives — "
+ f"its members are the contamination candidates:"
+ )
+ for i, lbl in enumerate(labels):
+ if lbl == neg_aligned:
+ print(
+ f" max_to_neg={max_to_neg[i]:.3f} "
+ f"mean_intra={mean_intra[i]:.3f} {pos_names[i]}"
+ )
+
+ keep_mask = labels != neg_aligned
+ if keep_mask.sum() >= 3:
+ clean_embs = [pos[i].embedding for i in range(len(pos)) if keep_mask[i]]
+ clean_mean = stats.trim_mean(np.stack(clean_embs), 0.15, axis=0)
+ print(
+ f"\n Rebuilding class mean from the OTHER cluster "
+ f"({keep_mask.sum()} images):"
+ )
+ print(f" {'cos':>6} {'conf':>6} name")
+ for n in neg:
+ cs = cosine(n.embedding, clean_mean)
+ cf = similarity_to_confidence(cs)
+ print(f" {cs:6.3f} {cf:6.3f} {os.path.basename(n.path)}")
+
+
+# ---------------------------------------------------------------------------
+# main
+# ---------------------------------------------------------------------------
+
+
+def main() -> int:
+ ap = argparse.ArgumentParser(
+ description="Analyze a face recognition collection outside Frigate.",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog=__doc__,
+ )
+ ap.add_argument("--positive", required=True, help="Training folder for one identity")
+ ap.add_argument(
+ "--negative",
+ default=None,
+ help="Runtime-crop folder to score against (optional)",
+ )
+ ap.add_argument(
+ "--model-cache",
+ default="/config/model_cache",
+ help="Directory containing facedet/arcface.onnx and facedet/landmarkdet.yaml",
+ )
+ ap.add_argument(
+ "--trim",
+ type=float,
+ default=0.15,
+ help="trim_mean proportion (Frigate uses 0.15)",
+ )
+ ap.add_argument(
+ "--vector-outlier",
+ action="store_true",
+ help="Sweep the vector-wise outlier filter threshold",
+ )
+ ap.add_argument(
+ "--degenerate",
+ action="store_true",
+ help="Test whether negatives share a degenerate embedding region",
+ )
+ ap.add_argument(
+ "--contamination",
+ action="store_true",
+ help="Check whether the positive folder contains a second identity",
+ )
+ args = ap.parse_args()
+
+ arcface_path = os.path.join(args.model_cache, "facedet", "arcface.onnx")
+ landmark_path = os.path.join(args.model_cache, "facedet", "landmarkdet.yaml")
+ for p in (arcface_path, landmark_path):
+ if not os.path.exists(p):
+ print(f"ERROR: model file not found: {p}")
+ return 1
+
+ print(f"Loading ArcFace from {arcface_path}")
+ embedder = ArcFaceEmbedder(arcface_path)
+ print(f"Loading landmark model from {landmark_path}")
+ aligner = LandmarkAligner(landmark_path)
+
+ print(f"\nLoading positives from {args.positive} ...")
+ pos = load_folder(args.positive, aligner, embedder)
+ print(f" {len(pos)} positives loaded")
+
+ neg: list[FaceSample] = []
+ if args.negative:
+ print(f"\nLoading negatives from {args.negative} ...")
+ neg = load_folder(args.negative, aligner, embedder)
+ print(f" {len(neg)} negatives loaded")
+
+ if not pos:
+ print("no positive samples — aborting")
+ return 1
+
+ mean_emb = trimmed_mean([s.embedding for s in pos], trim=args.trim)
+ summarize_positive(pos, mean_emb)
+ if neg:
+ summarize_negative(neg, mean_emb, pos)
+
+ if args.vector_outlier:
+ vector_outlier_test(pos, neg, args.trim)
+ if args.degenerate and neg:
+ degenerate_embedding_test(pos, neg)
+ if args.contamination and neg:
+ contamination_analysis(pos, neg)
+
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/testing-scripts/object_dataset.py b/testing-scripts/object_dataset.py
new file mode 100644
index 0000000000..dd4071c441
--- /dev/null
+++ b/testing-scripts/object_dataset.py
@@ -0,0 +1,1022 @@
+"""
+Object classification investigation script.
+
+Standalone replica of Frigate's custom object classification inference pipeline
+(see frigate/data_processing/real_time/custom_classification.py and
+frigate/util/classification.py) for analyzing a training dataset outside the
+running service. Useful for:
+
+ - Diagnosing why a class produces false positives / misidentifications
+ - Finding the training images that the deployed model itself misclassifies
+ (these are the worst offenders — usually mislabeled or low-quality crops)
+ - Inspecting borderline-correct images that sit near the decision boundary
+ - Spotting class-pair confusion (which classes get mixed up)
+
+Layout:
+ - Core pipeline: load_tflite, preprocess_for_inference, classify_image —
+ all mirroring CustomObjectClassificationProcessor exactly
+ - Default run: scan the dataset, classify every image with the deployed
+ model.tflite, report misclassified + borderline images per class, and
+ print a confusion matrix
+ - Optional diagnostics (flags): image-quality breakdown, scoring an
+ unlabeled "negative" folder, cross-class contamination analysis (find
+ training images in class A that visually look like class B and pull
+ inference toward A), and copying worst offenders out for review
+
+Recommended workflow when troubleshooting misclassifications:
+
+ 1. Run the basic scan first (no extra flags). Read top-down:
+ - Class balance ratio. If > 3x, balance counts before anything else.
+ The dominant class will absorb borderline predictions otherwise.
+ - Per-class accuracy. Any class < 50% needs attention.
+ - Confusion matrix. If multiple classes all over-predict the same
+ class (e.g. Buddy->Rex, Bailey->Rex, none->Rex), you have
+ feature collapse, not "a few bad photos." Don't bother with
+ contamination analysis yet — fix the collapse first.
+
+ 2. Check for "degenerate blob" upsampling. Look at the SHAPE column on
+ worst-offender rows. If most misclassified crops are < 80x80, the
+ small originals are being stretched 3-7x to fit the 224x224 model
+ input. Upsampled crops collapse to a similar region of feature space
+ regardless of identity — the model can't tell them apart and defaults
+ them to whichever class has the most of them.
+
+ Fix: quarantine every image where min(w, h) < 80 (or 100 for a
+ stricter cut) and retrain. This works when the named class has
+ plenty of non-small examples to fall back on AND the small crops
+ are mostly degenerate blobs (target unrecognizable at that size).
+
+ CAVEAT — sometimes small crops ARE the signal, not the noise: if
+ your target naturally appears small at the camera distance (cats
+ indoors, distant subjects, wide-FOV setups), the small crops in
+ the named class ARE the typical inference-time input. Removing them
+ leaves the model unable to recognize the target at its natural
+ detection size, and accuracy on the named class collapses after
+ retraining. If that happens — named-class accuracy drops sharply
+ after size cut + retrain — restore the quarantine and switch to
+ visual review of just the misclassified small crops instead of
+ bulk size filtering. The size threshold is a tool for "tons of
+ accidental tiny blobs polluting a class with otherwise large
+ examples," not a universal cleanup.
+
+ 3. Verify the "none" class exists and is healthy. Without a strong
+ "none" class, every unknown crop at inference gets forced into one of
+ your real classes — the model has no "I don't know" option. Aim for:
+ - Count similar to your other classes (don't let it be the smallest)
+ - Images >= 100x100, well-framed
+ - Visual variety: other dogs/objects, partial views, empty scenes,
+ not just one type of negative
+
+ 4. Look for cross-class duplicates from the same Frigate event. If the
+ same timestamp prefix appears across multiple class folders (e.g.
+ "1772052999.x" present in Buddy AND Bailey AND Rex AND none), those
+ crops came from one moment in time. Either they were extracted from a
+ multi-object frame and labeled inconsistently, or they're near-
+ duplicates of one scene cropped slightly differently. Inspect them as
+ a group and decide together.
+
+ 5. Only after (1)-(4) are clean, run --confuses : for
+ targeted contamination analysis. The "ringleaders" section at the
+ bottom is the actionable part: a short list of images appearing
+ repeatedly as nearest neighbors of the wrong class. Those are the
+ few photos doing most of the damage.
+
+ 6. Stop deleting when the contamination delta column shows ALL negative
+ values for the source class. That means dataset images in
+ are already visually distinct from in fixed-backbone
+ embedding space — the trained model just hasn't learned to use that
+ separation. The fix from that point is to ADD more training data for
+ the underperforming class, not delete more. Aim for at least 20 well-
+ framed images per class.
+
+The dataset must be the same layout Frigate trains from:
+ //dataset//*.{webp,png,jpg,jpeg}
+
+The model must already be trained:
+ //model.tflite
+ //labelmap.txt
+
+Command-line examples (mirror the workflow steps above):
+
+ One-time setup — download the ImageNet-pretrained MobileNetV2 backbone
+ that --confuses uses for model-independent embeddings:
+
+ curl -L -o /config/model_cache/mobilenetv2-7.onnx \\
+ https://github.com/onnx/models/raw/main/validated/vision/classification/mobilenet/model/mobilenetv2-7.onnx
+
+ Step 1 — Basic scan. Always start here. Reads class balance, accuracy,
+ confusion matrix, and per-class worst offenders:
+
+ python3 object_dataset.py --name "" --top-n 25
+
+ Step 2 — Same scan plus image-quality stats (blur, brightness, aspect
+ distortion) for correct vs misclassified rows. Use when you suspect
+ systematic quality issues are driving the misses:
+
+ python3 object_dataset.py --name "" --top-n 25 --quality
+
+ Step 2 (cleanup) — Quarantine crops below 80x80 (the upsampling-blob
+ fix). Mirrors the class folder structure so individual images can be
+ restored. Change `threshold = 80` to 64 (looser) or 100 (stricter):
+
+ python3 - <<'EOF'
+ import cv2
+ from pathlib import Path
+ dataset = Path("/media/frigate/clips//dataset")
+ quarantine = Path("/media/frigate/clips//quarantine_small")
+ threshold = 80
+ moved = 0
+ for cls_dir in sorted(dataset.iterdir()):
+ if not cls_dir.is_dir():
+ continue
+ for img_path in sorted(cls_dir.iterdir()):
+ if img_path.suffix.lower() not in (".png", ".jpg", ".jpeg", ".webp"):
+ continue
+ img = cv2.imread(str(img_path))
+ if img is None:
+ continue
+ h, w = img.shape[:2]
+ if min(h, w) < threshold:
+ dest_dir = quarantine / cls_dir.name
+ dest_dir.mkdir(parents=True, exist_ok=True)
+ img_path.rename(dest_dir / img_path.name)
+ moved += 1
+ print(f"moved {moved} images")
+ EOF
+
+ Revert any quarantine directory (puts everything back into dataset/):
+
+ python3 - <<'EOF'
+ from pathlib import Path
+ quarantine = Path("/media/frigate/clips//quarantine_small")
+ dataset = Path("/media/frigate/clips//dataset")
+ for cls_dir in sorted(quarantine.iterdir()):
+ if not cls_dir.is_dir():
+ continue
+ target = dataset / cls_dir.name
+ target.mkdir(parents=True, exist_ok=True)
+ for img_path in sorted(cls_dir.iterdir()):
+ img_path.rename(target / img_path.name)
+ EOF
+
+ Step 4 — Inspect a same-timestamp cluster across all classes (replace
+ TIMESTAMP with the prefix you saw in worst-offenders, e.g. "1772052999"):
+
+ mkdir -p /tmp/timestamp_cluster
+ cd "/media/frigate/clips//dataset"
+ for f in */*TIMESTAMP*; do
+ cls=$(dirname "$f"); fn=$(basename "$f")
+ cp "$f" "/tmp/timestamp_cluster/${cls}__${fn}"
+ done
+
+ Step 5 — Cross-class contamination. Lists specific images that
+ look like , plus a ringleader summary of the few worst offenders.
+ Also copies all misclassified images into a flat browse-able folder
+ bucketed by (true_class)__as__(predicted_class):
+
+ python3 object_dataset.py --name "" \\
+ --embedding-model /config/model_cache/mobilenetv2-7.onnx \\
+ --confuses Rex:Buddy --top-n 15 \\
+ --save-misclassified /tmp/_offenders
+
+ Or let the script pick the worst-confused class pair from the matrix:
+
+ python3 object_dataset.py --name "" \\
+ --embedding-model /config/model_cache/mobilenetv2-7.onnx \\
+ --confuses auto
+
+ Score an unlabeled folder of runtime crops against the trained model —
+ useful for analyzing why specific inference-time misfires happened.
+ Prints full per-class probability vectors and threshold-pass status:
+
+ python3 object_dataset.py --name "" \\
+ --negative /path/to/runtime_misfires --threshold 0.8
+
+Full flag reference:
+ python3 object_dataset.py \\
+ --name \\
+ [--clips-dir /media/frigate/clips] \\
+ [--model-cache /config/model_cache] \\
+ [--threshold 0.8] [--top-n 15] \\
+ [--quality] [--negative ] [--save-misclassified ] \\
+ [--confuses :] [--embedding-model ]
+"""
+
+from __future__ import annotations
+
+import argparse
+import os
+import shutil
+import sys
+from dataclasses import dataclass
+
+import cv2
+import numpy as np
+
+try:
+ from tflite_runtime.interpreter import Interpreter
+except ModuleNotFoundError:
+ from ai_edge_litert.interpreter import Interpreter
+
+CLASSIFIER_INPUT_SIZE = 224
+IMAGE_EXTS = (".webp", ".png", ".jpg", ".jpeg")
+
+
+# ---------------------------------------------------------------------------
+# Replicated Frigate pipeline
+# ---------------------------------------------------------------------------
+
+
+def load_tflite(model_path: str) -> tuple[Interpreter, list[dict], list[dict]]:
+ """Mirror CustomObjectClassificationProcessor.__build_detector."""
+ interpreter = Interpreter(model_path=model_path, num_threads=2)
+ interpreter.allocate_tensors()
+ return (
+ interpreter,
+ interpreter.get_input_details(),
+ interpreter.get_output_details(),
+ )
+
+
+def load_labelmap(path: str) -> dict[int, str]:
+ """Mirror util.builtin.load_labels(prefill=0, indexed=False)."""
+ with open(path, "r", encoding="utf-8") as f:
+ lines = [line.strip() for line in f.readlines() if line.strip()]
+ return {idx: line for idx, line in enumerate(lines)}
+
+
+def preprocess_for_inference(image_bgr: np.ndarray) -> np.ndarray:
+ """Mirror the inference preprocessing in process_frame.
+
+ Frigate decodes the camera frame YUV->RGB, crops, then cv2.resize to
+ 224x224, and passes the uint8 array directly to the int8-quantized
+ interpreter. On disk we read BGR via cv2.imread, so we must convert
+ to RGB to match the channel order the model was trained on.
+ """
+ rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
+ resized = cv2.resize(rgb, (CLASSIFIER_INPUT_SIZE, CLASSIFIER_INPUT_SIZE))
+ return resized
+
+
+class MobileNetEmbedder:
+ """ImageNet-pretrained MobileNetV2 backbone via cv2.dnn.
+
+ Used as a model-independent visual embedder for cross-class contamination
+ analysis. The user's trained classifier may have memorized contaminating
+ training images and place them inside the right class in its own embedding
+ space — a fixed external backbone keeps the analysis honest.
+
+ Expects the standard ONNX Model Zoo MobileNetV2-7 file (PyTorch-style
+ preprocessing: ImageNet mean/std on /255 input). Output is 1000-d ImageNet
+ logits; L2-normalized for cosine-similarity comparisons.
+ """
+
+ IMAGENET_MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32)
+ IMAGENET_STD = np.array([0.229, 0.224, 0.225], dtype=np.float32)
+
+ def __init__(self, model_path: str):
+ if not os.path.exists(model_path):
+ raise FileNotFoundError(model_path)
+ self.net = cv2.dnn.readNetFromONNX(model_path)
+
+ def embed(self, image_bgr: np.ndarray) -> np.ndarray:
+ rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
+ resized = cv2.resize(rgb, (224, 224)).astype(np.float32) / 255.0
+ normalized = (resized - self.IMAGENET_MEAN) / self.IMAGENET_STD
+ # NHWC -> NCHW
+ blob = np.transpose(normalized, (2, 0, 1))[np.newaxis, :, :, :]
+ self.net.setInput(blob)
+ out = self.net.forward().squeeze().astype(np.float32)
+ norm = float(np.linalg.norm(out))
+ return out / norm if norm > 0 else out
+
+
+def classify_image(
+ interpreter: Interpreter,
+ input_details: list[dict],
+ output_details: list[dict],
+ image_bgr: np.ndarray,
+) -> np.ndarray:
+ """Mirror _classify_object's tensor flow.
+
+ Returns the per-class probability vector (length = num_classes) after
+ the exact `probs = res / res.sum(axis=0)` renormalization Frigate uses
+ on the int8-quantized output.
+ """
+ resized = preprocess_for_inference(image_bgr)
+ tensor = np.expand_dims(resized, axis=0)
+ interpreter.set_tensor(input_details[0]["index"], tensor)
+ interpreter.invoke()
+ res = interpreter.get_tensor(output_details[0]["index"])[0].astype(np.float32)
+ total = res.sum(axis=0)
+ if total <= 0:
+ # Defensive: all zeros from a degenerate quantization step.
+ return np.full_like(res, 1.0 / len(res))
+ return res / total
+
+
+# ---------------------------------------------------------------------------
+# Sample loading
+# ---------------------------------------------------------------------------
+
+
+@dataclass
+class ImageSample:
+ path: str
+ true_label: str | None # None for the unlabeled negative folder
+ shape: tuple[int, int]
+ probs: np.ndarray
+ pred_idx: int
+ pred_label: str
+ pred_score: float
+ true_idx: int | None
+ true_score: float | None
+
+
+def laplacian_variance(image_bgr: np.ndarray) -> float:
+ gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
+ return float(cv2.Laplacian(gray, cv2.CV_64F).var())
+
+
+def mean_brightness(image_bgr: np.ndarray) -> float:
+ return float(cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY).mean())
+
+
+def aspect_distortion(shape: tuple[int, int]) -> float:
+ """How far the crop is from square; |1 - max(w,h)/min(w,h)|.
+
+ A wide or tall crop gets squashed to 224x224 by the inference resize,
+ which can be a hidden source of misclassification.
+ """
+ w, h = shape
+ if w <= 0 or h <= 0:
+ return float("inf")
+ return float(max(w, h) / min(w, h) - 1.0)
+
+
+def iter_dataset(dataset_dir: str) -> list[tuple[str, str]]:
+ """Yield (class_name, image_path) pairs from the dataset directory."""
+ pairs: list[tuple[str, str]] = []
+ if not os.path.isdir(dataset_dir):
+ return pairs
+ for cls in sorted(os.listdir(dataset_dir)):
+ cls_dir = os.path.join(dataset_dir, cls)
+ if not os.path.isdir(cls_dir) or cls.startswith("."):
+ continue
+ for name in sorted(os.listdir(cls_dir)):
+ if name.startswith("."):
+ continue
+ if not name.lower().endswith(IMAGE_EXTS):
+ continue
+ pairs.append((cls, os.path.join(cls_dir, name)))
+ return pairs
+
+
+def classify_folder(
+ folder: str,
+ interpreter: Interpreter,
+ input_details: list[dict],
+ output_details: list[dict],
+ labelmap: dict[int, str],
+ label_to_idx: dict[str, int],
+ true_label: str | None = None,
+) -> list[ImageSample]:
+ """Classify every image directly under `folder`. Used for the negative set."""
+ samples: list[ImageSample] = []
+ if not os.path.isdir(folder):
+ return samples
+ for name in sorted(os.listdir(folder)):
+ if name.startswith(".") or not name.lower().endswith(IMAGE_EXTS):
+ continue
+ path = os.path.join(folder, name)
+ img = cv2.imread(path)
+ if img is None:
+ print(f" [skip unreadable] {name}")
+ continue
+ probs = classify_image(interpreter, input_details, output_details, img)
+ pred_idx = int(np.argmax(probs))
+ true_idx = label_to_idx.get(true_label) if true_label is not None else None
+ true_score = float(probs[true_idx]) if true_idx is not None else None
+ samples.append(
+ ImageSample(
+ path=path,
+ true_label=true_label,
+ shape=(img.shape[1], img.shape[0]),
+ probs=probs,
+ pred_idx=pred_idx,
+ pred_label=labelmap[pred_idx],
+ pred_score=float(probs[pred_idx]),
+ true_idx=true_idx,
+ true_score=true_score,
+ )
+ )
+ return samples
+
+
+def classify_dataset(
+ dataset_dir: str,
+ interpreter: Interpreter,
+ input_details: list[dict],
+ output_details: list[dict],
+ labelmap: dict[int, str],
+ label_to_idx: dict[str, int],
+) -> list[ImageSample]:
+ samples: list[ImageSample] = []
+ pairs = iter_dataset(dataset_dir)
+ for cls, path in pairs:
+ img = cv2.imread(path)
+ if img is None:
+ print(f" [skip unreadable] {cls}/{os.path.basename(path)}")
+ continue
+ probs = classify_image(interpreter, input_details, output_details, img)
+ pred_idx = int(np.argmax(probs))
+ true_idx = label_to_idx.get(cls)
+ true_score = float(probs[true_idx]) if true_idx is not None else None
+ samples.append(
+ ImageSample(
+ path=path,
+ true_label=cls,
+ shape=(img.shape[1], img.shape[0]),
+ probs=probs,
+ pred_idx=pred_idx,
+ pred_label=labelmap[pred_idx],
+ pred_score=float(probs[pred_idx]),
+ true_idx=true_idx,
+ true_score=true_score,
+ )
+ )
+ return samples
+
+
+# ---------------------------------------------------------------------------
+# Baseline analyses (always run)
+# ---------------------------------------------------------------------------
+
+
+def summarize_dataset(samples: list[ImageSample], labelmap: dict[int, str]) -> None:
+ """Per-class counts, accuracy, mean confidence on the true class."""
+ print("\n" + "=" * 78)
+ print(f"DATASET OVERVIEW ({len(samples)} images)")
+ print("=" * 78)
+
+ by_class: dict[str, list[ImageSample]] = {}
+ for s in samples:
+ by_class.setdefault(s.true_label or "", []).append(s)
+
+ print(
+ f"\n{'class':<20} {'count':>6} {'acc':>6} {'mean_p_true':>12} "
+ f"{'min_p_true':>10} {'mislabeled':>11}"
+ )
+ for cls in sorted(by_class):
+ rows = by_class[cls]
+ correct = sum(1 for r in rows if r.pred_label == cls)
+ mean_pt = (
+ np.mean([r.true_score for r in rows if r.true_score is not None])
+ if any(r.true_score is not None for r in rows)
+ else float("nan")
+ )
+ min_pt = (
+ np.min([r.true_score for r in rows if r.true_score is not None])
+ if any(r.true_score is not None for r in rows)
+ else float("nan")
+ )
+ acc = correct / len(rows) if rows else 0.0
+ bad = len(rows) - correct
+ print(
+ f"{cls:<20} {len(rows):>6} {acc:>6.2%} {mean_pt:>12.3f} "
+ f"{min_pt:>10.3f} {bad:>11}"
+ )
+
+ # Class balance — large skew can hide poor minority-class accuracy in the totals.
+ counts = [len(by_class[c]) for c in by_class]
+ if counts:
+ print(
+ f"\nClass balance: min={min(counts)} max={max(counts)} "
+ f"ratio={max(counts) / max(1, min(counts)):.1f}x"
+ )
+
+
+def confusion_matrix(samples: list[ImageSample], labelmap: dict[int, str]) -> None:
+ print("\n" + "=" * 78)
+ print("CONFUSION MATRIX (rows = true class, cols = predicted class)")
+ print("=" * 78)
+
+ classes = [labelmap[i] for i in sorted(labelmap)]
+ idx = {c: i for i, c in enumerate(classes)}
+ mat = np.zeros((len(classes), len(classes)), dtype=int)
+ for s in samples:
+ if s.true_label is None or s.true_label not in idx:
+ continue
+ mat[idx[s.true_label], s.pred_idx] += 1
+
+ col_w = max(8, max(len(c) for c in classes) + 1)
+ header = " " * (col_w + 2) + "".join(f"{c[: col_w - 1]:>{col_w}}" for c in classes)
+ print("\n" + header)
+ for i, cls in enumerate(classes):
+ row = "".join(f"{mat[i, j]:>{col_w}}" for j in range(len(classes)))
+ print(f" {cls[: col_w - 1]:<{col_w}}{row}")
+
+ # Top class-pair confusions, in both directions.
+ pairs: list[tuple[str, str, int]] = []
+ for i, src in enumerate(classes):
+ for j, dst in enumerate(classes):
+ if i != j and mat[i, j] > 0:
+ pairs.append((src, dst, int(mat[i, j])))
+ pairs.sort(key=lambda r: -r[2])
+ if pairs:
+ print("\nTop class-pair confusions:")
+ for src, dst, n in pairs[:10]:
+ print(f" {n:>4} {src} -> {dst}")
+
+
+def worst_offenders(
+ samples: list[ImageSample],
+ labelmap: dict[int, str],
+ top_n: int,
+ quality: bool,
+) -> list[ImageSample]:
+ """Print the worst-offender images grouped by class.
+
+ Two buckets per class:
+ (a) Misclassified — predicted label differs from folder. Sorted by the
+ confidence in the WRONG class (highest first). These are the most
+ confidently wrong images, the strongest candidates for relabeling
+ or deletion.
+ (b) Borderline-correct — predicted label matches but p_true is low.
+ These sit near the decision boundary; they're not actively wrong
+ but they make the class harder to learn cleanly.
+
+ Returns the union of (a) lists across classes, for optional copying.
+ """
+ print("\n" + "=" * 78)
+ print(f"WORST OFFENDERS (top {top_n} per class)")
+ print("=" * 78)
+
+ by_class: dict[str, list[ImageSample]] = {}
+ for s in samples:
+ if s.true_label is None:
+ continue
+ by_class.setdefault(s.true_label, []).append(s)
+
+ all_misclassified: list[ImageSample] = []
+ for cls in sorted(by_class):
+ rows = by_class[cls]
+ miscls = [r for r in rows if r.pred_label != cls]
+ miscls.sort(key=lambda r: -r.pred_score)
+ all_misclassified.extend(miscls[:top_n])
+
+ print(f"\n-- class '{cls}': {len(miscls)}/{len(rows)} misclassified --")
+ if miscls:
+ print(
+ f"{'p_pred':>7} {'pred':<18} {'p_true':>7} {'shape':>11}"
+ + (" blur bright aspect " if quality else " ")
+ + "name"
+ )
+ for r in miscls[:top_n]:
+ shape = f"{r.shape[0]}x{r.shape[1]}"
+ extra = ""
+ if quality:
+ img = cv2.imread(r.path)
+ blur = laplacian_variance(img) if img is not None else float("nan")
+ bright = mean_brightness(img) if img is not None else float("nan")
+ aspect = aspect_distortion(r.shape)
+ extra = f" {blur:5.0f} {bright:6.1f} {aspect:6.2f} "
+ pt = r.true_score if r.true_score is not None else float("nan")
+ print(
+ f"{r.pred_score:7.3f} {r.pred_label:<18} "
+ f"{pt:7.3f} {shape:>11}{extra}{os.path.basename(r.path)}"
+ )
+
+ # Borderline-correct: labeled right but the model isn't confident.
+ correct = [r for r in rows if r.pred_label == cls and r.true_score is not None]
+ correct.sort(key=lambda r: r.true_score or 0.0)
+ borderline = correct[: max(5, top_n // 3)]
+ if borderline:
+ print("\n borderline-correct (lowest p_true while still labeled right):")
+ for r in borderline:
+ # Second-best class names the neighbor that's pulling on this image.
+ if len(r.probs) > 1:
+ second = int(np.argsort(-r.probs)[1])
+ second_lbl = labelmap[second]
+ second_p = float(r.probs[second])
+ else:
+ second_lbl = "-"
+ second_p = 0.0
+ print(
+ f" p_true={r.true_score:.3f} "
+ f"p_2nd={second_p:.3f} ({second_lbl}) "
+ f"{os.path.basename(r.path)}"
+ )
+
+ return all_misclassified
+
+
+# ---------------------------------------------------------------------------
+# Optional diagnostics
+# ---------------------------------------------------------------------------
+
+
+def quality_summary(samples: list[ImageSample]) -> None:
+ """Compare image-quality stats for correct vs misclassified images.
+
+ Helps answer: are the worst offenders systematically blurrier / darker /
+ more squashed than the rest of the class? If so, the fix is to tighten
+ the data-collection criteria, not just delete individual images.
+ """
+ print("\n" + "=" * 78)
+ print("IMAGE QUALITY — correct vs misclassified")
+ print("=" * 78)
+
+ rows: list[tuple[str, bool, float, float, float]] = []
+ for s in samples:
+ if s.true_label is None:
+ continue
+ img = cv2.imread(s.path)
+ if img is None:
+ continue
+ blur = laplacian_variance(img)
+ bright = mean_brightness(img)
+ aspect = aspect_distortion(s.shape)
+ rows.append((s.true_label, s.pred_label == s.true_label, blur, bright, aspect))
+
+ if not rows:
+ print(" (no readable images)")
+ return
+
+ correct = [r for r in rows if r[1]]
+ wrong = [r for r in rows if not r[1]]
+
+ def stats(name: str, getter, group: list) -> None:
+ if not group:
+ print(f" {name:<14} (no samples)")
+ return
+ vals = np.array([getter(r) for r in group])
+ print(
+ f" {name:<14} n={len(vals):>4} "
+ f"mean={vals.mean():8.2f} median={np.median(vals):8.2f} "
+ f"p10={np.percentile(vals, 10):8.2f} p90={np.percentile(vals, 90):8.2f}"
+ )
+
+ print("\nBlur (laplacian variance — higher = sharper):")
+ stats("correct", lambda r: r[2], correct)
+ stats("misclassified", lambda r: r[2], wrong)
+ print("\nBrightness (0..255):")
+ stats("correct", lambda r: r[3], correct)
+ stats("misclassified", lambda r: r[3], wrong)
+ print("\nAspect distortion (0 = square; higher = more squashed by 224x224):")
+ stats("correct", lambda r: r[4], correct)
+ stats("misclassified", lambda r: r[4], wrong)
+
+
+def summarize_negative(
+ neg_samples: list[ImageSample],
+ threshold: float,
+ labelmap: dict[int, str],
+) -> None:
+ """Score an unlabeled folder of runtime crops against the model.
+
+ Equivalent to face_dataset.py's negative-set analysis: each image is
+ classified, and we print its full probability vector plus whether it
+ would clear the configured threshold. High-confidence predictions on
+ crops the user knows are wrong indicate the training set is leaking
+ a representative image into the wrong class.
+ """
+ print("\n" + "=" * 78)
+ print(f"NEGATIVE SET ANALYSIS ({len(neg_samples)} images, threshold={threshold})")
+ print("=" * 78)
+
+ classes = [labelmap[i] for i in sorted(labelmap)]
+ print(f"\n{'pass':>4} {'score':>6} {'pred':<18} full prob vector / name")
+ for s in neg_samples:
+ passes = "yes" if s.pred_score >= threshold else "no"
+ full = " ".join(f"{c}={float(s.probs[i]):.2f}" for i, c in enumerate(classes))
+ print(
+ f"{passes:>4} {s.pred_score:6.3f} {s.pred_label:<18} "
+ f"{full} :: {os.path.basename(s.path)}"
+ )
+
+
+def pick_worst_confusion_pair(
+ samples: list[ImageSample],
+ labelmap: dict[int, str],
+) -> tuple[int, str | None, str | None]:
+ """Return (count, source, target) for the most-confused class pair."""
+ classes = [labelmap[i] for i in sorted(labelmap)]
+ pairs: list[tuple[int, str, str]] = []
+ for src in classes:
+ for tgt in classes:
+ if src == tgt:
+ continue
+ n = sum(1 for s in samples if s.true_label == src and s.pred_label == tgt)
+ if n > 0:
+ pairs.append((n, src, tgt))
+ pairs.sort(reverse=True)
+ return pairs[0] if pairs else (0, None, None)
+
+
+def cross_class_contamination(
+ samples: list[ImageSample],
+ source_class: str,
+ target_class: str,
+ label_to_idx: dict[str, int],
+ embedder: MobileNetEmbedder,
+ top_n: int,
+) -> None:
+ """Find training images in source_class that visually look like target_class.
+
+ Generalizes face_dataset.py's contamination_analysis to N classes. Uses a
+ fixed ImageNet backbone (NOT the user's trained classifier) so that
+ contaminators which the trained model has memorized into the source class
+ still surface — the trained model's own embedding would hide them.
+
+ Three sections:
+ 1. Source-image culprits ranked by `cos(img, target_centroid) -
+ cos(img, source_centroid)`. Positive delta = the image looks more
+ like the target class than its own class — prime relabeling
+ candidates.
+ 2. For each target image, the top-3 nearest source training images.
+ Shows the visual chain of confusion image-by-image.
+ 3. Ringleader summary: source images that appear most often as a top-3
+ neighbor across the target set. These few photos are responsible
+ for the bulk of the confusion.
+ """
+ src = [s for s in samples if s.true_label == source_class]
+ tgt = [s for s in samples if s.true_label == target_class]
+
+ if not src or not tgt:
+ print(
+ f"\nERROR: need both classes populated; got {len(src)} "
+ f"'{source_class}' and {len(tgt)} '{target_class}'"
+ )
+ return
+
+ print("\n" + "=" * 78)
+ print(
+ f"CROSS-CLASS CONTAMINATION '{source_class}' leaning toward '{target_class}'"
+ )
+ print(" (model-independent embeddings via ImageNet MobileNetV2)")
+ print("=" * 78)
+
+ target_idx = label_to_idx.get(target_class)
+ source_idx = label_to_idx.get(source_class)
+
+ print(f"\nEmbedding {len(src) + len(tgt)} images...")
+ src_embs = np.stack([embedder.embed(cv2.imread(s.path)) for s in src])
+ tgt_embs = np.stack([embedder.embed(cv2.imread(s.path)) for s in tgt])
+
+ src_centroid = src_embs.mean(axis=0)
+ src_centroid /= np.linalg.norm(src_centroid) + 1e-9
+ tgt_centroid = tgt_embs.mean(axis=0)
+ tgt_centroid /= np.linalg.norm(tgt_centroid) + 1e-9
+
+ src_to_src = src_embs @ src_centroid
+ src_to_tgt = src_embs @ tgt_centroid
+ delta = src_to_tgt - src_to_src
+
+ print(f"\n-- '{source_class}' images sorted by '{target_class}'-likeness --")
+ print(f" positive delta = visually closer to '{target_class}' centroid")
+ print(
+ f" p_{target_class} = trained model's probability for '{target_class}' "
+ f"on this image\n"
+ )
+ delta_label = "delta"
+ tgt_cos_label = f"cos_{target_class}"[:12]
+ src_cos_label = f"cos_{source_class}"[:12]
+ p_tgt_label = f"p_{target_class}"[:10]
+ print(
+ f" {delta_label:>7} {tgt_cos_label:>12} {src_cos_label:>12} "
+ f"{p_tgt_label:>10} name"
+ )
+ order = np.argsort(-delta)
+ for i in order[:top_n]:
+ s = src[i]
+ p_tgt = float(s.probs[target_idx]) if target_idx is not None else float("nan")
+ print(
+ f" {delta[i]:+7.3f} {src_to_tgt[i]:12.3f} {src_to_src[i]:12.3f} "
+ f"{p_tgt:10.3f} {os.path.basename(s.path)}"
+ )
+
+ print(f"\n-- nearest '{source_class}' neighbors for each '{target_class}' image --")
+ neighbor_counts: dict[str, int] = {}
+ src_paths = [os.path.basename(s.path) for s in src]
+ for i, t in enumerate(tgt):
+ sims = src_embs @ tgt_embs[i]
+ top3 = np.argsort(-sims)[:3]
+ p_src = float(t.probs[source_idx]) if source_idx is not None else float("nan")
+ marker = " <5} name")
+ ranked = sorted(neighbor_counts.items(), key=lambda r: -r[1])
+ for name, count in ranked[:top_n]:
+ print(f" {count:>5} {name}")
+
+
+def save_misclassified(samples: list[ImageSample], out_dir: str) -> None:
+ """Copy misclassified images to /__as__/.
+
+ Lets you browse the worst offenders in a file manager and bulk-delete or
+ relabel them without poking through the original dataset tree.
+ """
+ print("\n" + "=" * 78)
+ print(f"SAVING MISCLASSIFIED IMAGES -> {out_dir}")
+ print("=" * 78)
+ count = 0
+ for s in samples:
+ if s.true_label is None or s.pred_label == s.true_label:
+ continue
+ bucket = os.path.join(out_dir, f"{s.true_label}__as__{s.pred_label}")
+ os.makedirs(bucket, exist_ok=True)
+ score_tag = f"{int(round(s.pred_score * 100)):03d}"
+ dest = os.path.join(bucket, f"{score_tag}_{os.path.basename(s.path)}")
+ try:
+ shutil.copy2(s.path, dest)
+ count += 1
+ except OSError as err:
+ print(f" [copy failed] {s.path}: {err}")
+ print(f" copied {count} images into {out_dir}")
+
+
+# ---------------------------------------------------------------------------
+# main
+# ---------------------------------------------------------------------------
+
+
+def main() -> int:
+ ap = argparse.ArgumentParser(
+ description=(
+ "Analyze a Frigate object-classification training dataset against its "
+ "deployed TFLite model."
+ ),
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog=__doc__,
+ )
+ ap.add_argument(
+ "--name",
+ required=True,
+ help="Classification model name (matches the key in classification.custom.)",
+ )
+ ap.add_argument(
+ "--clips-dir",
+ default="/media/frigate/clips",
+ help="Frigate clips directory; dataset is read from //dataset",
+ )
+ ap.add_argument(
+ "--model-cache",
+ default="/config/model_cache",
+ help="Frigate model_cache; model is read from //model.tflite",
+ )
+ ap.add_argument(
+ "--threshold",
+ type=float,
+ default=0.8,
+ help="Score threshold (matches model_config.threshold; default 0.8)",
+ )
+ ap.add_argument(
+ "--top-n",
+ type=int,
+ default=15,
+ help="Worst-offender images to show per class",
+ )
+ ap.add_argument(
+ "--quality",
+ action="store_true",
+ help="Include blur/brightness/aspect stats for correct vs misclassified",
+ )
+ ap.add_argument(
+ "--negative",
+ default=None,
+ help="Score an unlabeled folder of crops against the model",
+ )
+ ap.add_argument(
+ "--save-misclassified",
+ default=None,
+ help="Copy every misclassified image into this directory for review",
+ )
+ ap.add_argument(
+ "--confuses",
+ default=None,
+ help=(
+ "Cross-class contamination analysis. Format ':', "
+ "e.g. 'rex:buddy' to find Rex training images that look like "
+ "Buddy. Use 'auto' to pick the worst pair from the confusion matrix. "
+ "Requires --embedding-model."
+ ),
+ )
+ ap.add_argument(
+ "--embedding-model",
+ default=None,
+ help=(
+ "Path to ONNX MobileNetV2 file for model-independent embeddings "
+ "(required by --confuses). Download once with: curl -L -o "
+ "/config/model_cache/mobilenetv2-7.onnx https://github.com/onnx/"
+ "models/raw/main/validated/vision/classification/mobilenet/model/"
+ "mobilenetv2-7.onnx"
+ ),
+ )
+ args = ap.parse_args()
+
+ dataset_dir = os.path.join(args.clips_dir, args.name, "dataset")
+ model_path = os.path.join(args.model_cache, args.name, "model.tflite")
+ labelmap_path = os.path.join(args.model_cache, args.name, "labelmap.txt")
+
+ for required in (dataset_dir, model_path, labelmap_path):
+ if not os.path.exists(required):
+ print(f"ERROR: required path not found: {required}")
+ return 1
+
+ print(f"Loading model from {model_path}")
+ interpreter, input_details, output_details = load_tflite(model_path)
+ labelmap = load_labelmap(labelmap_path)
+ label_to_idx = {v: k for k, v in labelmap.items()}
+ print(f" labels: {sorted(labelmap.values())}")
+
+ print(f"\nScanning dataset at {dataset_dir} ...")
+ samples = classify_dataset(
+ dataset_dir, interpreter, input_details, output_details, labelmap, label_to_idx
+ )
+ if not samples:
+ print("no images found — aborting")
+ return 1
+ print(f" classified {len(samples)} images")
+
+ summarize_dataset(samples, labelmap)
+ confusion_matrix(samples, labelmap)
+ misclassified = worst_offenders(samples, labelmap, args.top_n, args.quality)
+
+ if args.quality:
+ quality_summary(samples)
+
+ if args.negative:
+ print(f"\nLoading negatives from {args.negative} ...")
+ neg = classify_folder(
+ args.negative,
+ interpreter,
+ input_details,
+ output_details,
+ labelmap,
+ label_to_idx,
+ true_label=None,
+ )
+ if neg:
+ summarize_negative(neg, args.threshold, labelmap)
+
+ if args.confuses:
+ if not args.embedding_model:
+ print(
+ "\nERROR: --confuses requires --embedding-model (path to ONNX "
+ "MobileNetV2). See --help for the download command."
+ )
+ return 1
+ try:
+ embedder = MobileNetEmbedder(args.embedding_model)
+ except (FileNotFoundError, cv2.error) as err:
+ print(f"\nERROR: failed to load embedding model: {err}")
+ return 1
+
+ if args.confuses == "auto":
+ n, src, tgt = pick_worst_confusion_pair(samples, labelmap)
+ if src is None:
+ print(
+ "\nNo misclassifications in dataset — "
+ "nothing to investigate via --confuses auto"
+ )
+ else:
+ print(f"\nAuto-picked worst confusion: {src} -> {tgt} ({n} cases)")
+ cross_class_contamination(
+ samples, src, tgt, label_to_idx, embedder, args.top_n
+ )
+ else:
+ if ":" not in args.confuses:
+ print("\nERROR: --confuses expects ':' or 'auto'")
+ return 1
+ src, tgt = args.confuses.split(":", 1)
+ if src not in label_to_idx or tgt not in label_to_idx:
+ print(
+ f"\nERROR: class names must be in the labelmap "
+ f"({sorted(label_to_idx)})"
+ )
+ return 1
+ cross_class_contamination(
+ samples, src, tgt, label_to_idx, embedder, args.top_n
+ )
+
+ if args.save_misclassified:
+ save_misclassified(misclassified, args.save_misclassified)
+
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/process_clip.py b/testing-scripts/process_clip.py
similarity index 100%
rename from process_clip.py
rename to testing-scripts/process_clip.py
diff --git a/web/.gitignore b/web/.gitignore
index 1cac5597ea..ca98c7b96a 100644
--- a/web/.gitignore
+++ b/web/.gitignore
@@ -12,6 +12,10 @@ dist
dist-ssr
*.local
+# Playwright
+playwright-report
+test-results
+
# Editor directories and files
.vscode/*
!.vscode/extensions.json
diff --git a/web/e2e/fixtures/mock-data/cases.json b/web/e2e/fixtures/mock-data/cases.json
index 5d0c96b8c1..6174cdebf3 100644
--- a/web/e2e/fixtures/mock-data/cases.json
+++ b/web/e2e/fixtures/mock-data/cases.json
@@ -1 +1 @@
-[{"id": "case-001", "name": "Package Theft Investigation", "description": "Review of suspicious activity near the front porch", "created_at": 1775407931.3863528, "updated_at": 1775483531.3863528}]
\ No newline at end of file
+[{"id": "case-001", "name": "Package Theft Investigation", "description": "Review of suspicious activity near the front porch", "created_at": 1780597809.365581, "updated_at": 1780673409.365581}]
\ No newline at end of file
diff --git a/web/e2e/fixtures/mock-data/config-schema.json b/web/e2e/fixtures/mock-data/config-schema.json
new file mode 100644
index 0000000000..04d72b1e5a
--- /dev/null
+++ b/web/e2e/fixtures/mock-data/config-schema.json
@@ -0,0 +1 @@
+{"$defs": {"AlertsConfig": {"additionalProperties": false, "description": "Configure alerts", "properties": {"enabled": {"default": true, "description": "Enable or disable alert generation for all cameras; can be overridden per-camera.", "title": "Enable alerts", "type": "boolean"}, "labels": {"default": ["person", "car"], "description": "List of object labels that qualify as alerts (for example: car, person).", "items": {"type": "string"}, "title": "Alert labels", "type": "array"}, "required_zones": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "Zones that an object must enter to be considered an alert; leave empty to allow any zone.", "title": "Required zones"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Tracks whether alerts were originally enabled in the static configuration.", "title": "Original alerts state"}, "cutoff_time": {"default": 40, "description": "Seconds to wait after no alert-causing activity before cutting off an alert.", "title": "Alerts cutoff time", "type": "integer"}}, "title": "AlertsConfig", "type": "object"}, "AudioConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable audio event detection for all cameras; can be overridden per-camera.", "title": "Enable audio detection", "type": "boolean"}, "max_not_heard": {"default": 30, "description": "Amount of seconds without the configured audio type before the audio event is ended.", "title": "End timeout", "type": "integer"}, "min_volume": {"default": 500, "description": "Minimum RMS volume threshold required to run audio detection; lower values increase sensitivity (e.g., 200 high, 500 medium, 1000 low).", "title": "Minimum volume", "type": "integer"}, "listen": {"default": ["bark", "fire_alarm", "speech", "yell"], "description": "List of audio event types to detect (for example: bark, fire_alarm, speech, yell).", "items": {"type": "string"}, "title": "Listen types", "type": "array"}, "filters": {"anyOf": [{"additionalProperties": {"$ref": "#/$defs/AudioFilterConfig"}, "type": "object"}, {"type": "null"}], "default": null, "description": "Per-audio-type filter settings such as confidence thresholds used to reduce false positives.", "title": "Audio filters"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Indicates whether audio detection was originally enabled in the static config file.", "title": "Original audio state"}, "num_threads": {"default": 2, "description": "Number of threads to use for audio detection processing.", "minimum": 1, "title": "Detection threads", "type": "integer"}}, "title": "AudioConfig", "type": "object"}, "AudioFilterConfig": {"additionalProperties": false, "properties": {"threshold": {"default": 0.8, "description": "Minimum confidence threshold for the audio event to be counted.", "exclusiveMaximum": 1.0, "minimum": 0.5, "title": "Minimum audio confidence", "type": "number"}}, "title": "AudioFilterConfig", "type": "object"}, "AudioTranscriptionConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable automatic audio transcription for all cameras; can be overridden per-camera.", "title": "Enable audio transcription", "type": "boolean"}, "language": {"default": "en", "description": "Language code used for transcription/translation (for example 'en' for English). See https://whisper-api.com/docs/languages/ for supported language codes.", "title": "Transcription language", "type": "string"}, "device": {"$ref": "#/$defs/EnrichmentsDeviceEnum", "default": "CPU", "description": "Device key (CPU/GPU) to run the transcription model on. Only NVIDIA CUDA GPUs are currently supported for transcription.", "title": "Transcription device"}, "model_size": {"$ref": "#/$defs/ModelSizeEnum", "default": "small", "description": "Model size to use for offline audio event transcription.", "title": "Model size"}, "live_enabled": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": false, "description": "Enable streaming live transcription for audio as it is received.", "title": "Live transcription"}}, "title": "AudioTranscriptionConfig", "type": "object"}, "AuthConfig": {"additionalProperties": false, "properties": {"enabled": {"default": true, "description": "Enable native authentication for the Frigate UI.", "title": "Enable authentication", "type": "boolean"}, "reset_admin_password": {"default": false, "description": "If true, reset the admin user's password on startup and print the new password in logs.", "title": "Reset admin password", "type": "boolean"}, "cookie_name": {"default": "frigate_token", "description": "Name of the cookie used to store the JWT token for native authentication.", "pattern": "^[a-z_]+$", "title": "JWT cookie name", "type": "string"}, "cookie_secure": {"default": false, "description": "Set the secure flag on the auth cookie; should be true when using TLS.", "title": "Secure cookie flag", "type": "boolean"}, "session_length": {"default": 86400, "description": "Session duration in seconds for JWT-based sessions.", "minimum": 60, "title": "Session length", "type": "integer"}, "refresh_time": {"default": 1800, "description": "When a session is within this many seconds of expiring, refresh it back to full length.", "minimum": 30, "title": "Session refresh window", "type": "integer"}, "failed_login_rate_limit": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Rate limiting rules for failed login attempts to reduce brute-force attacks.", "title": "Failed login limits"}, "trusted_proxies": {"default": [], "description": "List of trusted proxy IPs used when determining client IP for rate limiting.", "items": {"type": "string"}, "title": "Trusted proxies", "type": "array"}, "hash_iterations": {"default": 600000, "description": "Number of PBKDF2-SHA256 iterations to use when hashing user passwords.", "title": "Hash iterations", "type": "integer"}, "roles": {"additionalProperties": {"items": {"type": "string"}, "type": "array"}, "description": "Map roles to camera lists. An empty list grants access to all cameras for the role.", "title": "Role mappings", "type": "object"}, "admin_first_time_login": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": false, "description": "When true the UI may show a help link on the login page informing users how to sign in after an admin password reset. ", "title": "First-time admin flag"}}, "title": "AuthConfig", "type": "object"}, "BaseDetectorConfig": {"additionalProperties": true, "properties": {"type": {"default": "cpu", "description": "Type of detector to use for object detection (for example 'cpu', 'edgetpu', 'openvino').", "title": "Detector Type", "type": "string"}, "model": {"anyOf": [{"$ref": "#/$defs/ModelConfig"}, {"type": "null"}], "default": null, "description": "Detector-specific model configuration options (path, input size, etc.).", "title": "Detector specific model configuration"}, "model_path": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "File path to the detector model binary if required by the chosen detector.", "title": "Detector specific model path"}}, "title": "BaseDetectorConfig", "type": "object"}, "BirdClassificationConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable bird classification.", "title": "Bird classification", "type": "boolean"}, "threshold": {"default": 0.9, "description": "Minimum classification score required to accept a bird classification.", "exclusiveMinimum": 0.0, "maximum": 1.0, "title": "Minimum score", "type": "number"}}, "title": "BirdClassificationConfig", "type": "object"}, "BirdseyeCameraConfig": {"properties": {"enabled": {"default": true, "description": "Enable or disable the Birdseye view feature.", "title": "Enable Birdseye", "type": "boolean"}, "mode": {"$ref": "#/$defs/BirdseyeModeEnum", "default": "objects", "description": "Mode for including cameras in Birdseye: 'objects', 'motion', or 'continuous'.", "title": "Tracking mode"}, "order": {"default": 0, "description": "Numeric position controlling the camera's ordering in the Birdseye layout.", "title": "Position", "type": "integer"}}, "title": "BirdseyeCameraConfig", "type": "object"}, "BirdseyeConfig": {"additionalProperties": false, "properties": {"enabled": {"default": true, "description": "Enable or disable the Birdseye view feature.", "title": "Enable Birdseye", "type": "boolean"}, "mode": {"$ref": "#/$defs/BirdseyeModeEnum", "default": "objects", "description": "Mode for including cameras in Birdseye: 'objects', 'motion', or 'continuous'.", "title": "Tracking mode"}, "restream": {"default": false, "description": "Re-stream the Birdseye output as an RTSP feed; enabling this will keep Birdseye running continuously.", "title": "Restream RTSP", "type": "boolean"}, "width": {"default": 1280, "description": "Output width (pixels) of the composed Birdseye frame.", "title": "Width", "type": "integer"}, "height": {"default": 720, "description": "Output height (pixels) of the composed Birdseye frame.", "title": "Height", "type": "integer"}, "quality": {"default": 8, "description": "Encoding quality for the Birdseye mpeg1 feed (1 highest quality, 31 lowest).", "maximum": 31, "minimum": 1, "title": "Encoding quality", "type": "integer"}, "inactivity_threshold": {"default": 30, "description": "Seconds of inactivity after which a camera will stop being shown in Birdseye.", "exclusiveMinimum": 0, "title": "Inactivity threshold", "type": "integer"}, "layout": {"$ref": "#/$defs/BirdseyeLayoutConfig", "description": "Layout options for the Birdseye composition.", "title": "Layout"}, "idle_heartbeat_fps": {"default": 0.0, "description": "Frames-per-second to resend the last composed Birdseye frame when idle; set to 0 to disable.", "maximum": 10.0, "minimum": 0.0, "title": "Idle heartbeat FPS", "type": "number"}}, "title": "BirdseyeConfig", "type": "object"}, "BirdseyeLayoutConfig": {"additionalProperties": false, "properties": {"scaling_factor": {"default": 2.0, "description": "Scaling factor used by the layout calculator (range 1.0 to 5.0).", "maximum": 5.0, "minimum": 1.0, "title": "Scaling factor", "type": "number"}, "max_cameras": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": null, "description": "Maximum number of cameras to display at once in Birdseye; shows the most recent cameras.", "title": "Max cameras"}}, "title": "BirdseyeLayoutConfig", "type": "object"}, "BirdseyeModeEnum": {"enum": ["objects", "motion", "continuous"], "title": "BirdseyeModeEnum", "type": "string"}, "CameraAudioTranscriptionConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable manually triggered audio event transcription.", "title": "Enable transcription", "type": "boolean"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "title": "Original transcription state"}, "live_enabled": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": false, "description": "Enable streaming live transcription for audio as it is received.", "title": "Live transcription"}}, "title": "CameraAudioTranscriptionConfig", "type": "object"}, "CameraConfig": {"additionalProperties": false, "properties": {"name": {"anyOf": [{"pattern": "^[a-zA-Z0-9_-]+$", "type": "string"}, {"type": "null"}], "default": null, "description": "Camera name is required", "title": "Camera name"}, "friendly_name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Camera friendly name used in the Frigate UI", "title": "Friendly name"}, "enabled": {"default": true, "description": "Enabled", "title": "Enabled", "type": "boolean"}, "audio": {"$ref": "#/$defs/AudioConfig", "description": "Settings for audio-based event detection for this camera.", "title": "Audio detection"}, "audio_transcription": {"$ref": "#/$defs/CameraAudioTranscriptionConfig", "description": "Settings for live and speech audio transcription used for events and live captions.", "title": "Audio transcription"}, "birdseye": {"$ref": "#/$defs/BirdseyeCameraConfig", "description": "Settings for the Birdseye composite view that composes multiple camera feeds into a single layout.", "title": "Birdseye"}, "detect": {"$ref": "#/$defs/DetectConfig", "description": "Settings for the detection/detect role used to run object detection and initialize trackers.", "title": "Object Detection"}, "face_recognition": {"$ref": "#/$defs/CameraFaceRecognitionConfig", "description": "Settings for face detection and recognition for this camera.", "title": "Face recognition"}, "ffmpeg": {"$ref": "#/$defs/CameraFfmpegConfig", "description": "Camera stream inputs and FFmpeg options, including binary path, args, hwaccel, and per-role output args.", "title": "Streams (FFmpeg)"}, "live": {"$ref": "#/$defs/CameraLiveConfig", "description": "Settings used by the Web UI to control live stream selection, resolution and quality.", "title": "Live playback"}, "lpr": {"$ref": "#/$defs/CameraLicensePlateRecognitionConfig", "description": "License plate recognition settings including detection thresholds, formatting, and known plates.", "title": "License Plate Recognition"}, "motion": {"$ref": "#/$defs/MotionConfig", "default": null, "description": "Default motion detection settings for this camera.", "title": "Motion detection"}, "objects": {"$ref": "#/$defs/ObjectConfig", "description": "Object tracking defaults including which labels to track and per-object filters.", "title": "Objects"}, "record": {"$ref": "#/$defs/RecordConfig", "description": "Recording and retention settings for this camera.", "title": "Recording"}, "review": {"$ref": "#/$defs/ReviewConfig", "description": "Settings that control alerts, detections, and GenAI review summaries used by the UI and storage for this camera.", "title": "Review"}, "semantic_search": {"$ref": "#/$defs/CameraSemanticSearchConfig", "description": "Settings for semantic search which builds and queries object embeddings to find similar items.", "title": "Semantic Search"}, "snapshots": {"$ref": "#/$defs/SnapshotsConfig", "description": "Settings for API-generated snapshots of tracked objects for this camera.", "title": "Snapshots"}, "timestamp_style": {"$ref": "#/$defs/TimestampStyleConfig", "description": "Styling options for timestamps applied to snapshots and Debug view.", "title": "Timestamp style"}, "best_image_timeout": {"default": 60, "description": "How long to wait for the image with the highest confidence score.", "title": "Best image timeout", "type": "integer"}, "mqtt": {"$ref": "#/$defs/CameraMqttConfig", "description": "MQTT image publishing settings.", "title": "MQTT"}, "notifications": {"$ref": "#/$defs/NotificationConfig", "description": "Settings to enable and control notifications for this camera.", "title": "Notifications"}, "onvif": {"$ref": "#/$defs/OnvifConfig", "description": "ONVIF connection and PTZ autotracking settings for this camera.", "title": "ONVIF"}, "type": {"$ref": "#/$defs/CameraTypeEnum", "default": "generic", "description": "Camera Type", "title": "Camera type"}, "ui": {"$ref": "#/$defs/CameraUiConfig", "description": "Display ordering and visibility for this camera in the UI. Ordering affects the default dashboard. For more granular control, use camera groups.", "title": "Camera UI"}, "webui_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "URL to visit the camera directly from system page", "title": "Camera URL"}, "profiles": {"additionalProperties": {"$ref": "#/$defs/CameraProfileConfig"}, "description": "Named config profiles with partial overrides that can be activated at runtime.", "title": "Profiles", "type": "object"}, "zones": {"additionalProperties": {"$ref": "#/$defs/ZoneConfig"}, "description": "Zones allow you to define a specific area of the frame so you can determine whether or not an object is within a particular area.", "title": "Zones", "type": "object"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Keep track of original state of camera.", "title": "Original camera state"}}, "required": ["ffmpeg"], "title": "CameraConfig", "type": "object"}, "CameraFaceRecognitionConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable face recognition.", "title": "Enable face recognition", "type": "boolean"}, "min_area": {"default": 750, "description": "Minimum area (pixels) of a detected face box required to attempt recognition.", "title": "Minimum face area", "type": "integer"}}, "title": "CameraFaceRecognitionConfig", "type": "object"}, "CameraFfmpegConfig": {"additionalProperties": false, "properties": {"path": {"default": "default", "description": "Path to the FFmpeg binary to use or a version alias (\"7.0\" or \"8.0\").", "title": "FFmpeg path", "type": "string"}, "global_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "description": "Global arguments passed to FFmpeg processes.", "title": "FFmpeg global arguments"}, "hwaccel_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "auto", "description": "Hardware acceleration arguments for FFmpeg. Provider-specific presets are recommended.", "title": "Hardware acceleration arguments"}, "input_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "preset-rtsp-generic", "description": "Input arguments applied to FFmpeg input streams.", "title": "Input arguments"}, "output_args": {"$ref": "#/$defs/FfmpegOutputArgsConfig", "description": "Default output arguments used for different FFmpeg roles such as detect and record.", "title": "Output arguments"}, "retry_interval": {"default": 10.0, "description": "Seconds to wait before attempting to reconnect a camera stream after failure. Default is 10.", "exclusiveMinimum": 0.0, "title": "FFmpeg retry time", "type": "number"}, "apple_compatibility": {"default": false, "description": "Enable HEVC tagging for better Apple player compatibility when recording H.265.", "title": "Apple compatibility", "type": "boolean"}, "gpu": {"default": 0, "description": "Default GPU index used for hardware acceleration if available.", "title": "GPU index", "type": "integer"}, "inputs": {"description": "List of input stream definitions (paths and roles) for this camera.", "items": {"$ref": "#/$defs/CameraInput"}, "title": "Camera inputs", "type": "array"}}, "required": ["inputs"], "title": "CameraFfmpegConfig", "type": "object"}, "CameraGroupConfig": {"additionalProperties": false, "properties": {"cameras": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "Array of camera names included in this group.", "title": "Camera list"}, "icon": {"default": "generic", "description": "Icon used to represent the camera group in the UI.", "title": "Group icon", "type": "string"}, "order": {"default": 0, "description": "Numeric order used to sort camera groups in the UI; larger numbers appear later.", "title": "Sort order", "type": "integer"}}, "title": "CameraGroupConfig", "type": "object"}, "CameraInput": {"additionalProperties": false, "properties": {"path": {"description": "Camera input stream URL or path.", "title": "Input path", "type": "string"}, "roles": {"description": "Roles for this input stream.", "items": {"$ref": "#/$defs/CameraRoleEnum"}, "title": "Input roles", "type": "array"}, "global_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "FFmpeg global arguments for this input stream.", "title": "FFmpeg global arguments"}, "hwaccel_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "Hardware acceleration arguments for this input stream.", "title": "Hardware acceleration arguments"}, "input_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "Input arguments specific to this stream.", "title": "Input arguments"}}, "required": ["path", "roles"], "title": "CameraInput", "type": "object"}, "CameraLicensePlateRecognitionConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable LPR on this camera.", "title": "Enable LPR", "type": "boolean"}, "expire_time": {"default": 3, "description": "Time in seconds after which an unseen plate is expired from the tracker (for dedicated LPR cameras only).", "exclusiveMinimum": 0, "title": "Expire seconds", "type": "integer"}, "min_area": {"default": 1000, "description": "Minimum plate area (pixels) required to attempt recognition.", "title": "Minimum plate area", "type": "integer"}, "enhancement": {"default": 0, "description": "Enhancement level (0-10) to apply to plate crops prior to OCR; higher values may not always improve results, levels above 5 may only work with night time plates and should be used with caution.", "maximum": 10, "minimum": 0, "title": "Enhancement level", "type": "integer"}}, "title": "CameraLicensePlateRecognitionConfig", "type": "object"}, "CameraLiveConfig": {"additionalProperties": false, "properties": {"streams": {"additionalProperties": {"type": "string"}, "description": "Mapping of configured stream names to restream/go2rtc names used for live playback.", "title": "Live stream names", "type": "object"}, "height": {"default": 720, "description": "Height (pixels) to render the jsmpeg live stream in the Web UI; must be <= detect stream height.", "title": "Live height", "type": "integer"}, "quality": {"default": 8, "description": "Encoding quality for the jsmpeg stream (1 highest, 31 lowest).", "maximum": 31, "minimum": 1, "title": "Live quality", "type": "integer"}}, "title": "CameraLiveConfig", "type": "object"}, "CameraMqttConfig": {"additionalProperties": false, "properties": {"enabled": {"default": true, "description": "Enable publishing image snapshots for objects to MQTT topics for this camera.", "title": "Send image", "type": "boolean"}, "timestamp": {"default": true, "description": "Overlay a timestamp on images published to MQTT.", "title": "Add timestamp", "type": "boolean"}, "bounding_box": {"default": true, "description": "Draw bounding boxes on images published over MQTT.", "title": "Add bounding box", "type": "boolean"}, "crop": {"default": true, "description": "Crop images published to MQTT to the detected object's bounding box.", "title": "Crop image", "type": "boolean"}, "height": {"default": 270, "description": "Height (pixels) to resize images published over MQTT.", "title": "Image height", "type": "integer"}, "required_zones": {"description": "Zones that an object must enter for an MQTT image to be published.", "items": {"type": "string"}, "title": "Required zones", "type": "array"}, "quality": {"default": 70, "description": "JPEG quality for images published to MQTT (0-100).", "maximum": 100, "minimum": 0, "title": "JPEG quality", "type": "integer"}}, "title": "CameraMqttConfig", "type": "object"}, "CameraProfileConfig": {"additionalProperties": false, "description": "A named profile containing partial camera config overrides.\n\nSections set to None inherit from the camera's base config.\nSections that are defined get Pydantic-validated, then only\nexplicitly-set fields are used as overrides via exclude_unset.", "properties": {"enabled": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "title": "Enabled"}, "audio": {"anyOf": [{"$ref": "#/$defs/AudioConfig"}, {"type": "null"}], "default": null}, "birdseye": {"anyOf": [{"$ref": "#/$defs/BirdseyeCameraConfig"}, {"type": "null"}], "default": null}, "detect": {"anyOf": [{"$ref": "#/$defs/DetectConfig"}, {"type": "null"}], "default": null}, "face_recognition": {"anyOf": [{"$ref": "#/$defs/CameraFaceRecognitionConfig"}, {"type": "null"}], "default": null}, "lpr": {"anyOf": [{"$ref": "#/$defs/CameraLicensePlateRecognitionConfig"}, {"type": "null"}], "default": null}, "motion": {"anyOf": [{"$ref": "#/$defs/MotionConfig"}, {"type": "null"}], "default": null}, "notifications": {"anyOf": [{"$ref": "#/$defs/NotificationConfig"}, {"type": "null"}], "default": null}, "objects": {"anyOf": [{"$ref": "#/$defs/ObjectConfig"}, {"type": "null"}], "default": null}, "record": {"anyOf": [{"$ref": "#/$defs/RecordConfig"}, {"type": "null"}], "default": null}, "review": {"anyOf": [{"$ref": "#/$defs/ReviewConfig"}, {"type": "null"}], "default": null}, "snapshots": {"anyOf": [{"$ref": "#/$defs/SnapshotsConfig"}, {"type": "null"}], "default": null}, "zones": {"anyOf": [{"additionalProperties": {"$ref": "#/$defs/ZoneConfig"}, "type": "object"}, {"type": "null"}], "default": null, "title": "Zones"}}, "title": "CameraProfileConfig", "type": "object"}, "CameraRoleEnum": {"enum": ["audio", "record", "detect"], "title": "CameraRoleEnum", "type": "string"}, "CameraSemanticSearchConfig": {"additionalProperties": false, "properties": {"triggers": {"additionalProperties": {"$ref": "#/$defs/TriggerConfig"}, "default": {}, "description": "Actions and matching criteria for camera-specific semantic search triggers.", "title": "Triggers", "type": "object"}}, "title": "CameraSemanticSearchConfig", "type": "object"}, "CameraTypeEnum": {"enum": ["generic", "lpr"], "title": "CameraTypeEnum", "type": "string"}, "CameraUiConfig": {"additionalProperties": false, "properties": {"order": {"default": 0, "description": "Numeric order used to sort the camera in the UI (default dashboard and lists); larger numbers appear later.", "title": "UI order", "type": "integer"}, "dashboard": {"default": true, "description": "Toggle whether this camera is visible everywhere in the Frigate UI. Disabling this will require manually editing the config to view this camera in the UI again.", "title": "Show in UI", "type": "boolean"}, "review": {"default": true, "description": "Toggle whether this camera is visible in review (the review page and its camera filter, motion review, and the history view).", "title": "Show in review", "type": "boolean"}}, "title": "CameraUiConfig", "type": "object"}, "ClassificationConfig": {"additionalProperties": false, "properties": {"bird": {"$ref": "#/$defs/BirdClassificationConfig", "description": "Settings specific to bird classification models.", "title": "Bird classification config"}, "custom": {"additionalProperties": {"$ref": "#/$defs/CustomClassificationConfig"}, "default": {}, "description": "Configuration for custom classification models used for objects or state detection.", "title": "Custom Classification Models", "type": "object"}}, "title": "ClassificationConfig", "type": "object"}, "ColorConfig": {"additionalProperties": false, "properties": {"red": {"default": 255, "description": "Red component (0-255) for timestamp color.", "maximum": 255, "minimum": 0, "title": "Red", "type": "integer"}, "green": {"default": 255, "description": "Green component (0-255) for timestamp color.", "maximum": 255, "minimum": 0, "title": "Green", "type": "integer"}, "blue": {"default": 255, "description": "Blue component (0-255) for timestamp color.", "maximum": 255, "minimum": 0, "title": "Blue", "type": "integer"}}, "title": "ColorConfig", "type": "object"}, "CustomClassificationConfig": {"additionalProperties": false, "properties": {"enabled": {"default": true, "description": "Enable or disable the custom classification model.", "title": "Enable model", "type": "boolean"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Identifier for the custom classification model to use.", "title": "Model name"}, "threshold": {"default": 0.8, "description": "Score threshold used to change the classification state.", "title": "Score threshold", "type": "number"}, "save_attempts": {"anyOf": [{"minimum": 0, "type": "integer"}, {"type": "null"}], "default": null, "description": "How many classification attempts to save for recent classifications UI.", "title": "Save attempts"}, "object_config": {"anyOf": [{"$ref": "#/$defs/CustomClassificationObjectConfig"}, {"type": "null"}], "default": null}, "state_config": {"anyOf": [{"$ref": "#/$defs/CustomClassificationStateConfig"}, {"type": "null"}], "default": null}}, "title": "CustomClassificationConfig", "type": "object"}, "CustomClassificationObjectConfig": {"additionalProperties": false, "properties": {"objects": {"description": "List of object types to run object classification on.", "items": {"type": "string"}, "title": "Classify objects", "type": "array"}, "classification_type": {"$ref": "#/$defs/ObjectClassificationType", "default": "sub_label", "description": "Classification type applied: 'sub_label' (adds sub_label) or other supported types.", "title": "Classification type"}}, "title": "CustomClassificationObjectConfig", "type": "object"}, "CustomClassificationStateCameraConfig": {"additionalProperties": false, "properties": {"crop": {"description": "Crop coordinates to use for running classification on this camera.", "items": {"type": "number"}, "title": "Classification crop", "type": "array"}}, "required": ["crop"], "title": "CustomClassificationStateCameraConfig", "type": "object"}, "CustomClassificationStateConfig": {"additionalProperties": false, "properties": {"cameras": {"additionalProperties": {"$ref": "#/$defs/CustomClassificationStateCameraConfig"}, "description": "Per-camera crop and settings for running state classification.", "title": "Classification cameras", "type": "object"}, "motion": {"default": false, "description": "If true, run classification when motion is detected within the specified crop.", "title": "Run on motion", "type": "boolean"}, "interval": {"anyOf": [{"exclusiveMinimum": 0, "type": "integer"}, {"type": "null"}], "default": null, "description": "Interval (seconds) between periodic classification runs for state classification.", "title": "Classification interval"}}, "required": ["cameras"], "title": "CustomClassificationStateConfig", "type": "object"}, "DatabaseConfig": {"additionalProperties": false, "properties": {"path": {"default": "/config/frigate.db", "description": "Filesystem path where the Frigate SQLite database file will be stored.", "title": "Database path", "type": "string"}}, "title": "DatabaseConfig", "type": "object"}, "DetectConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable object detection for all cameras; can be overridden per-camera.", "title": "Enable object detection", "type": "boolean"}, "height": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": null, "description": "Height (pixels) of frames used for the detect stream; leave empty to use the native stream resolution.", "title": "Detect height"}, "width": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": null, "description": "Width (pixels) of frames used for the detect stream; leave empty to use the native stream resolution.", "title": "Detect width"}, "fps": {"default": 5, "description": "Desired frames per second to run detection on; lower values reduce CPU usage (recommended value is 5, only set higher - at most 10 - if tracking extremely fast moving objects).", "title": "Detect FPS", "type": "integer"}, "min_initialized": {"anyOf": [{"minimum": 2, "type": "integer"}, {"type": "null"}], "default": null, "description": "Number of consecutive detection hits required before creating a tracked object. Increase to reduce false initializations. Default value is fps divided by 2.", "title": "Minimum initialization frames"}, "max_disappeared": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": null, "description": "Number of frames without a detection before a tracked object is considered gone.", "title": "Maximum disappeared frames"}, "stationary": {"$ref": "#/$defs/StationaryConfig", "description": "Settings to detect and manage objects that remain stationary for a period of time.", "title": "Stationary objects config"}, "annotation_offset": {"default": 0, "description": "Milliseconds to shift detect annotations to better align timeline bounding boxes with recordings; can be positive or negative.", "title": "Annotation offset", "type": "integer"}}, "title": "DetectConfig", "type": "object"}, "DetectionsConfig": {"additionalProperties": false, "description": "Configure detections", "properties": {"enabled": {"default": true, "description": "Enable or disable detection events for all cameras; can be overridden per-camera.", "title": "Enable detections", "type": "boolean"}, "labels": {"anyOf": [{"items": {"type": "string"}, "type": "array"}, {"type": "null"}], "default": null, "description": "List of object labels that qualify as detection events.", "title": "Detection labels"}, "required_zones": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "Zones that an object must enter to be considered a detection; leave empty to allow any zone.", "title": "Required zones"}, "cutoff_time": {"default": 30, "description": "Seconds to wait after no detection-causing activity before cutting off a detection.", "title": "Detections cutoff time", "type": "integer"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Tracks whether detections were originally enabled in the static configuration.", "title": "Original detections state"}}, "title": "DetectionsConfig", "type": "object"}, "EnrichmentsDeviceEnum": {"enum": ["GPU", "CPU"], "title": "EnrichmentsDeviceEnum", "type": "string"}, "EventsConfig": {"additionalProperties": false, "properties": {"pre_capture": {"default": 5, "description": "Number of seconds before the detection event to include in the recording.", "maximum": 60, "minimum": 0, "title": "Pre-capture seconds", "type": "integer"}, "post_capture": {"default": 5, "description": "Number of seconds after the detection event to include in the recording.", "minimum": 0, "title": "Post-capture seconds", "type": "integer"}, "retain": {"$ref": "#/$defs/ReviewRetainConfig", "description": "Retention settings for recordings of detection events.", "title": "Event retention"}}, "title": "EventsConfig", "type": "object"}, "FaceRecognitionConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable face recognition for all cameras; can be overridden per-camera.", "title": "Enable face recognition", "type": "boolean"}, "model_size": {"$ref": "#/$defs/ModelSizeEnum", "default": "small", "description": "Model size to use for face embeddings (small/large); larger may require GPU.", "title": "Model size"}, "unknown_score": {"default": 0.8, "description": "Distance threshold below which a face is considered a potential match (higher = stricter).", "exclusiveMinimum": 0.0, "maximum": 1.0, "title": "Unknown score threshold", "type": "number"}, "detection_threshold": {"default": 0.7, "description": "Minimum detection confidence required to consider a face detection valid.", "exclusiveMinimum": 0.0, "maximum": 1.0, "title": "Detection threshold", "type": "number"}, "recognition_threshold": {"default": 0.9, "description": "Face embedding distance threshold to consider two faces a match.", "exclusiveMinimum": 0.0, "maximum": 1.0, "title": "Recognition threshold", "type": "number"}, "min_area": {"default": 750, "description": "Minimum area (pixels) of a detected face box required to attempt recognition.", "title": "Minimum face area", "type": "integer"}, "min_faces": {"default": 1, "description": "Minimum number of face recognitions required before applying a recognized sub-label to a person.", "exclusiveMinimum": 0, "maximum": 6, "title": "Minimum faces", "type": "integer"}, "save_attempts": {"default": 200, "description": "Number of face recognition attempts to retain for recent recognition UI.", "minimum": 0, "title": "Save attempts", "type": "integer"}, "blur_confidence_filter": {"default": true, "description": "Adjust confidence scores based on image blur to reduce false positives for poor quality faces.", "title": "Blur confidence filter", "type": "boolean"}, "device": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information", "title": "Device"}}, "title": "FaceRecognitionConfig", "type": "object"}, "FfmpegConfig": {"additionalProperties": false, "properties": {"path": {"default": "default", "description": "Path to the FFmpeg binary to use or a version alias (\"7.0\" or \"8.0\").", "title": "FFmpeg path", "type": "string"}, "global_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "description": "Global arguments passed to FFmpeg processes.", "title": "FFmpeg global arguments"}, "hwaccel_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "auto", "description": "Hardware acceleration arguments for FFmpeg. Provider-specific presets are recommended.", "title": "Hardware acceleration arguments"}, "input_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "preset-rtsp-generic", "description": "Input arguments applied to FFmpeg input streams.", "title": "Input arguments"}, "output_args": {"$ref": "#/$defs/FfmpegOutputArgsConfig", "description": "Default output arguments used for different FFmpeg roles such as detect and record.", "title": "Output arguments"}, "retry_interval": {"default": 10.0, "description": "Seconds to wait before attempting to reconnect a camera stream after failure. Default is 10.", "exclusiveMinimum": 0.0, "title": "FFmpeg retry time", "type": "number"}, "apple_compatibility": {"default": false, "description": "Enable HEVC tagging for better Apple player compatibility when recording H.265.", "title": "Apple compatibility", "type": "boolean"}, "gpu": {"default": 0, "description": "Default GPU index used for hardware acceleration if available.", "title": "GPU index", "type": "integer"}}, "title": "FfmpegConfig", "type": "object"}, "FfmpegOutputArgsConfig": {"additionalProperties": false, "properties": {"detect": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "description": "Default output arguments for detect role streams.", "title": "Detect output arguments"}, "record": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "preset-record-generic-audio-aac", "description": "Default output arguments for record role streams.", "title": "Record output arguments"}}, "title": "FfmpegOutputArgsConfig", "type": "object"}, "FilterConfig": {"additionalProperties": false, "properties": {"min_area": {"anyOf": [{"type": "integer"}, {"type": "number"}], "default": 0, "description": "Minimum bounding box area (pixels or percentage) required for this object type. Can be pixels (int) or percentage (float between 0.000001 and 0.99).", "title": "Minimum object area"}, "max_area": {"anyOf": [{"type": "integer"}, {"type": "number"}], "default": 24000000, "description": "Maximum bounding box area (pixels or percentage) allowed for this object type. Can be pixels (int) or percentage (float between 0.000001 and 0.99).", "title": "Maximum object area"}, "min_ratio": {"default": 0, "description": "Minimum width/height ratio required for the bounding box to qualify.", "title": "Minimum aspect ratio", "type": "number"}, "max_ratio": {"default": 24000000, "description": "Maximum width/height ratio allowed for the bounding box to qualify.", "title": "Maximum aspect ratio", "type": "number"}, "threshold": {"default": 0.7, "description": "Average detection confidence threshold required for the object to be considered a true positive.", "title": "Confidence threshold", "type": "number"}, "min_score": {"default": 0.5, "description": "Minimum single-frame detection confidence required for the object to be counted.", "title": "Minimum confidence", "type": "number"}, "mask": {"additionalProperties": {"anyOf": [{"$ref": "#/$defs/ObjectMaskConfig"}, {"type": "null"}]}, "description": "Polygon coordinates defining where this filter applies within the frame.", "title": "Filter mask", "type": "object"}, "raw_mask": {"additionalProperties": {"anyOf": [{"$ref": "#/$defs/ObjectMaskConfig"}, {"type": "null"}]}, "title": "Raw Mask", "type": "object"}}, "title": "FilterConfig", "type": "object"}, "GenAIConfig": {"additionalProperties": false, "description": "Primary GenAI Config to define GenAI Provider.", "properties": {"api_key": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "API key required by some providers (can also be set via environment variables).", "title": "API key"}, "base_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Base URL for self-hosted or compatible providers (for example an Ollama instance).", "title": "Base URL"}, "model": {"default": "", "description": "The model to use from the provider for generating descriptions or summaries.", "title": "Model", "type": "string"}, "provider": {"$ref": "#/$defs/GenAIProviderEnum", "description": "The GenAI provider to use (for example: ollama, gemini, openai).", "title": "Provider"}, "roles": {"description": "GenAI roles (chat, descriptions, embeddings); one provider per role.", "items": {"$ref": "#/$defs/GenAIRoleEnum"}, "title": "Roles", "type": "array"}, "provider_options": {"additionalProperties": {}, "default": {}, "description": "Additional provider-specific options to pass to the GenAI client.", "title": "Provider options", "type": "object"}, "runtime_options": {"additionalProperties": {}, "default": {}, "description": "Runtime options passed to the provider for each inference call.", "title": "Runtime options", "type": "object"}}, "required": ["provider"], "title": "GenAIConfig", "type": "object"}, "GenAIObjectConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable GenAI generation of descriptions for tracked objects by default.", "title": "Enable GenAI", "type": "boolean"}, "use_snapshot": {"default": false, "description": "Use object snapshots instead of thumbnails for GenAI description generation.", "title": "Use snapshots", "type": "boolean"}, "prompt": {"default": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "description": "Default prompt template used when generating descriptions with GenAI.", "title": "Caption prompt", "type": "string"}, "object_prompts": {"additionalProperties": {"type": "string"}, "description": "Per-object prompts to customize GenAI outputs for specific labels.", "title": "Object prompts", "type": "object"}, "objects": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "List of object labels to send to GenAI by default.", "title": "GenAI objects"}, "required_zones": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "Zones that must be entered for objects to qualify for GenAI description generation.", "title": "Required zones"}, "debug_save_thumbnails": {"default": false, "description": "Save thumbnails sent to GenAI for debugging and review.", "title": "Save thumbnails", "type": "boolean"}, "send_triggers": {"$ref": "#/$defs/GenAIObjectTriggerConfig", "description": "Defines when frames should be sent to GenAI (on end, after updates, etc.).", "title": "GenAI triggers"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Indicates whether GenAI was enabled in the original static config.", "title": "Original GenAI state"}}, "title": "GenAIObjectConfig", "type": "object"}, "GenAIObjectTriggerConfig": {"additionalProperties": false, "properties": {"tracked_object_end": {"default": true, "description": "Send a request to GenAI when the tracked object ends.", "title": "Send on end", "type": "boolean"}, "after_significant_updates": {"anyOf": [{"minimum": 1, "type": "integer"}, {"type": "null"}], "default": null, "description": "Send a request to GenAI after a specified number of significant updates for the tracked object.", "title": "Early GenAI trigger"}}, "title": "GenAIObjectTriggerConfig", "type": "object"}, "GenAIProviderEnum": {"enum": ["openai", "azure_openai", "gemini", "ollama", "llamacpp"], "title": "GenAIProviderEnum", "type": "string"}, "GenAIReviewConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable GenAI-generated descriptions and summaries for review items.", "title": "Enable GenAI descriptions", "type": "boolean"}, "alerts": {"default": true, "description": "Use GenAI to generate descriptions for alert items.", "title": "Enable GenAI for alerts", "type": "boolean"}, "detections": {"default": false, "description": "Use GenAI to generate descriptions for detection items.", "title": "Enable GenAI for detections", "type": "boolean"}, "image_source": {"$ref": "#/$defs/ImageSourceEnum", "default": "preview", "description": "Source of images sent to GenAI ('preview' or 'recordings'); 'recordings' uses higher quality frames but more tokens.", "title": "Review image source"}, "additional_concerns": {"default": [], "description": "A list of additional concerns or notes the GenAI should consider when evaluating activity on this camera.", "items": {"type": "string"}, "title": "Additional concerns", "type": "array"}, "debug_save_thumbnails": {"default": false, "description": "Save thumbnails that are sent to the GenAI provider for debugging and review.", "title": "Save thumbnails", "type": "boolean"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Tracks whether GenAI review was originally enabled in the static configuration.", "title": "Original GenAI state"}, "preferred_language": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Preferred language to request from the GenAI provider for generated responses.", "title": "Preferred language"}, "activity_context_prompt": {"default": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is.", "description": "Custom prompt describing what is and is not suspicious activity to provide context for GenAI summaries.", "title": "Activity context prompt", "type": "string"}}, "title": "GenAIReviewConfig", "type": "object"}, "GenAIRoleEnum": {"enum": ["chat", "descriptions", "embeddings"], "title": "GenAIRoleEnum", "type": "string"}, "HeaderMappingConfig": {"additionalProperties": false, "properties": {"user": {"default": null, "description": "Header containing the authenticated username provided by the upstream proxy.", "title": "User header", "type": "string"}, "role": {"default": null, "description": "Header containing the authenticated user's role or groups from the upstream proxy.", "title": "Role header", "type": "string"}, "role_map": {"anyOf": [{"additionalProperties": {"items": {"type": "string"}, "type": "array"}, "type": "object"}, {"type": "null"}], "description": "Map upstream group values to Frigate roles (for example map admin groups to the admin role).", "title": "Role mapping"}}, "title": "HeaderMappingConfig", "type": "object"}, "IPv6Config": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable IPv6 support for Frigate services (API and UI) where applicable.", "title": "Enable IPv6", "type": "boolean"}}, "title": "IPv6Config", "type": "object"}, "ImageSourceEnum": {"description": "Image source options for GenAI Review.", "enum": ["preview", "recordings"], "title": "ImageSourceEnum", "type": "string"}, "InputDTypeEnum": {"enum": ["float", "float_denorm", "int"], "title": "InputDTypeEnum", "type": "string"}, "InputTensorEnum": {"enum": ["nchw", "nhwc", "hwnc", "hwcn"], "title": "InputTensorEnum", "type": "string"}, "LicensePlateRecognitionConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable license plate recognition for all cameras; can be overridden per-camera.", "title": "Enable LPR", "type": "boolean"}, "model_size": {"$ref": "#/$defs/ModelSizeEnum", "default": "small", "description": "Model size used for text detection/recognition. Most users should use 'small'.", "title": "Model size"}, "detection_threshold": {"default": 0.7, "description": "Detection confidence threshold to begin running OCR on a suspected plate.", "exclusiveMinimum": 0.0, "maximum": 1.0, "title": "Detection threshold", "type": "number"}, "min_area": {"default": 1000, "description": "Minimum plate area (pixels) required to attempt recognition.", "title": "Minimum plate area", "type": "integer"}, "recognition_threshold": {"default": 0.9, "description": "Confidence threshold required for recognized plate text to be attached as a sub-label.", "exclusiveMinimum": 0.0, "maximum": 1.0, "title": "Recognition threshold", "type": "number"}, "min_plate_length": {"default": 4, "description": "Minimum number of characters a recognized plate must contain to be considered valid.", "title": "Min plate length", "type": "integer"}, "format": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Optional regex to validate recognized plate strings against an expected format.", "title": "Plate format regex"}, "match_distance": {"default": 1, "description": "Number of character mismatches allowed when comparing detected plates to known plates.", "minimum": 0, "title": "Match distance", "type": "integer"}, "known_plates": {"anyOf": [{"additionalProperties": {"items": {"type": "string"}, "type": "array"}, "type": "object"}, {"type": "null"}], "default": {}, "description": "List of plates or regexes to specially track or alert on.", "title": "Known plates"}, "enhancement": {"default": 0, "description": "Enhancement level (0-10) to apply to plate crops prior to OCR; higher values may not always improve results, levels above 5 may only work with night time plates and should be used with caution.", "maximum": 10, "minimum": 0, "title": "Enhancement level", "type": "integer"}, "debug_save_plates": {"default": false, "description": "Save plate crop images for debugging LPR performance.", "title": "Save debug plates", "type": "boolean"}, "device": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information", "title": "Device"}, "replace_rules": {"description": "Regex replacement rules used to normalize detected plate strings before matching.", "items": {"$ref": "#/$defs/ReplaceRule"}, "title": "Replacement rules", "type": "array"}}, "title": "LicensePlateRecognitionConfig", "type": "object"}, "ListenConfig": {"additionalProperties": false, "properties": {"internal": {"anyOf": [{"type": "integer"}, {"type": "string"}], "default": 5000, "description": "Internal listening port for Frigate (default 5000).", "title": "Internal port"}, "external": {"anyOf": [{"type": "integer"}, {"type": "string"}], "default": 8971, "description": "External listening port for Frigate (default 8971).", "title": "External port"}}, "title": "ListenConfig", "type": "object"}, "LogLevel": {"enum": ["debug", "info", "warning", "error", "critical"], "title": "LogLevel", "type": "string"}, "LoggerConfig": {"additionalProperties": false, "properties": {"default": {"$ref": "#/$defs/LogLevel", "default": "info", "title": "Logging level", "description": "Default global log verbosity (debug, info, warning, error)."}, "logs": {"additionalProperties": {"$ref": "#/$defs/LogLevel"}, "description": "Per-component log level overrides to increase or decrease verbosity for specific modules.", "title": "Per-process log level", "type": "object"}}, "title": "LoggerConfig", "type": "object"}, "ModelConfig": {"additionalProperties": false, "properties": {"path": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Path to a custom detection model file (or plus:// for Frigate+ models).", "title": "Custom object detector model path"}, "labelmap_path": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Path to a labelmap file that maps numeric classes to string labels for the detector.", "title": "Label map for custom object detector"}, "width": {"default": 320, "description": "Width of the model input tensor in pixels.", "title": "Object detection model input width", "type": "integer"}, "height": {"default": 320, "description": "Height of the model input tensor in pixels.", "title": "Object detection model input height", "type": "integer"}, "labelmap": {"additionalProperties": {"type": "string"}, "description": "Overrides or remapping entries to merge into the standard labelmap.", "title": "Labelmap customization", "type": "object"}, "attributes_map": {"additionalProperties": {"items": {"type": "string"}, "type": "array"}, "default": {"person": ["amazon", "face"], "car": ["amazon", "an_post", "canada_post", "dhl", "dpd", "fedex", "gls", "license_plate", "nzpost", "postnl", "postnord", "purolator", "royal_mail", "ups", "usps"], "motorcycle": ["license_plate"]}, "description": "Mapping from object labels to attribute labels used to attach metadata (for example 'car' -> ['license_plate']).", "title": "Map of object labels to their attribute labels", "type": "object"}, "input_tensor": {"$ref": "#/$defs/InputTensorEnum", "default": "nhwc", "description": "Tensor format expected by the model: 'nhwc' or 'nchw'.", "title": "Model Input Tensor Shape"}, "input_pixel_format": {"$ref": "#/$defs/PixelFormatEnum", "default": "rgb", "description": "Pixel colorspace expected by the model: 'rgb', 'bgr', or 'yuv'.", "title": "Model Input Pixel Color Format"}, "input_dtype": {"$ref": "#/$defs/InputDTypeEnum", "default": "int", "description": "Data type of the model input tensor (for example 'float32').", "title": "Model Input D Type"}, "model_type": {"$ref": "#/$defs/ModelTypeEnum", "default": "ssd", "description": "Detector model architecture type (ssd, yolox, yolonas) used by some detectors for optimization.", "title": "Object Detection Model Type"}}, "title": "ModelConfig", "type": "object"}, "ModelSizeEnum": {"enum": ["small", "large"], "title": "ModelSizeEnum", "type": "string"}, "ModelTypeEnum": {"enum": ["dfine", "rfdetr", "ssd", "yolox", "yolonas", "yolo-generic"], "title": "ModelTypeEnum", "type": "string"}, "MotionConfig": {"additionalProperties": false, "properties": {"enabled": {"default": true, "description": "Enable or disable motion detection for all cameras; can be overridden per-camera.", "title": "Enable motion detection", "type": "boolean"}, "threshold": {"default": 30, "description": "Pixel difference threshold used by the motion detector; higher values reduce sensitivity (range 1-255).", "maximum": 255, "minimum": 1, "title": "Motion threshold", "type": "integer"}, "lightning_threshold": {"default": 0.8, "description": "Threshold to detect and ignore brief lighting spikes (lower is more sensitive, values between 0.3 and 1.0). This does not prevent motion detection entirely; it merely causes the detector to stop analyzing additional frames once the threshold is exceeded. Motion-based recordings are still created during these events.", "maximum": 1.0, "minimum": 0.3, "title": "Lightning threshold", "type": "number"}, "skip_motion_threshold": {"anyOf": [{"maximum": 1.0, "minimum": 0.0, "type": "number"}, {"type": "null"}], "default": null, "description": "If set to a value between 0.0 and 1.0, and more than this fraction of the image changes in a single frame, the detector will return no motion boxes and immediately recalibrate. This can save CPU and reduce false positives during lightning, storms, etc., but may miss real events such as a PTZ camera auto\u2011tracking an object. The trade\u2011off is between dropping a few megabytes of recordings versus reviewing a couple short clips. Leave unset (None) to disable this feature.", "title": "Skip motion threshold"}, "improve_contrast": {"default": true, "description": "Apply contrast improvement to frames before motion analysis to help detection.", "title": "Improve contrast", "type": "boolean"}, "contour_area": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": 10, "description": "Minimum contour area in pixels required for a motion contour to be counted.", "title": "Contour area"}, "delta_alpha": {"default": 0.2, "description": "Alpha blending factor used in frame differencing for motion calculation.", "title": "Delta alpha", "type": "number"}, "frame_alpha": {"default": 0.01, "description": "Alpha value used when blending frames for motion preprocessing.", "title": "Frame alpha", "type": "number"}, "frame_height": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": 100, "description": "Height in pixels to scale frames to when computing motion.", "title": "Frame height"}, "mask": {"additionalProperties": {"anyOf": [{"$ref": "#/$defs/MotionMaskConfig"}, {"type": "null"}]}, "description": "Ordered x,y coordinates defining the motion mask polygon used to include/exclude areas.", "title": "Mask coordinates", "type": "object"}, "mqtt_off_delay": {"default": 30, "description": "Seconds to wait after last motion before publishing an MQTT 'off' state.", "title": "MQTT off delay", "type": "integer"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Indicates whether motion detection was enabled in the original static configuration.", "title": "Original motion state"}, "raw_mask": {"additionalProperties": {"anyOf": [{"$ref": "#/$defs/MotionMaskConfig"}, {"type": "null"}]}, "title": "Raw Mask", "type": "object"}}, "title": "MotionConfig", "type": "object"}, "MotionMaskConfig": {"additionalProperties": false, "description": "Configuration for a single motion mask.", "properties": {"friendly_name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "A friendly name for this motion mask used in the Frigate UI", "title": "Friendly name"}, "enabled": {"default": true, "description": "Enable or disable this motion mask", "title": "Enabled", "type": "boolean"}, "coordinates": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "", "description": "Ordered x,y coordinates defining the motion mask polygon used to include/exclude areas.", "title": "Coordinates"}, "raw_coordinates": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "", "title": "Raw Coordinates"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "title": "Keep track of original state of motion mask."}}, "title": "MotionMaskConfig", "type": "object"}, "MqttConfig": {"additionalProperties": false, "properties": {"enabled": {"default": true, "description": "Enable or disable MQTT integration for state, events, and snapshots.", "title": "Enable MQTT", "type": "boolean"}, "host": {"default": "", "description": "Hostname or IP address of the MQTT broker.", "title": "MQTT host", "type": "string"}, "port": {"default": 1883, "description": "Port of the MQTT broker (usually 1883 for plain MQTT).", "title": "MQTT port", "type": "integer"}, "topic_prefix": {"default": "frigate", "description": "MQTT topic prefix for all Frigate topics; must be unique if running multiple instances.", "title": "Topic prefix", "type": "string"}, "client_id": {"default": "frigate", "description": "Client identifier used when connecting to the MQTT broker; should be unique per instance.", "title": "Client ID", "type": "string"}, "stats_interval": {"default": 60, "description": "Interval in seconds for publishing system and camera stats to MQTT.", "minimum": 15, "title": "Stats interval", "type": "integer"}, "user": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Optional MQTT username; can be provided via environment variables or secrets.", "title": "MQTT username"}, "password": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Optional MQTT password; can be provided via environment variables or secrets.", "title": "MQTT password"}, "tls_ca_certs": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Path to CA certificate for TLS connections to the broker (for self-signed certs).", "title": "TLS CA certs"}, "tls_client_cert": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Client certificate path for TLS mutual authentication; do not set user/password when using client certs.", "title": "Client cert"}, "tls_client_key": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Private key path for the client certificate.", "title": "Client key"}, "tls_insecure": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Allow insecure TLS connections by skipping hostname verification (not recommended).", "title": "TLS insecure"}, "qos": {"default": 0, "description": "Quality of Service level for MQTT publishes/subscriptions (0, 1, or 2).", "title": "MQTT QoS", "type": "integer"}}, "title": "MqttConfig", "type": "object"}, "NetworkingConfig": {"additionalProperties": false, "properties": {"ipv6": {"$ref": "#/$defs/IPv6Config", "description": "IPv6-specific settings for Frigate network services.", "title": "IPv6 configuration"}, "listen": {"$ref": "#/$defs/ListenConfig", "description": "Configuration for internal and external listening ports. This is for advanced users. For the majority of use cases it's recommended to change the ports section of your Docker compose file.", "title": "Listening ports configuration"}}, "title": "NetworkingConfig", "type": "object"}, "NotificationConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable notifications for all cameras; can be overridden per-camera.", "title": "Enable notifications", "type": "boolean"}, "email": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Email address used for push notifications or required by certain notification providers.", "title": "Notification email"}, "cooldown": {"default": 0, "description": "Cooldown (seconds) between notifications to avoid spamming recipients.", "minimum": 0, "title": "Cooldown period", "type": "integer"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Indicates whether notifications were enabled in the original static configuration.", "title": "Original notifications state"}}, "title": "NotificationConfig", "type": "object"}, "ObjectClassificationType": {"enum": ["sub_label", "attribute"], "title": "ObjectClassificationType", "type": "string"}, "ObjectConfig": {"additionalProperties": false, "properties": {"track": {"default": ["person"], "description": "List of object labels to track for all cameras; can be overridden per-camera.", "items": {"type": "string"}, "title": "Objects to track", "type": "array"}, "filters": {"additionalProperties": {"$ref": "#/$defs/FilterConfig"}, "description": "Filters applied to detected objects to reduce false positives (area, ratio, confidence).", "title": "Object filters", "type": "object"}, "mask": {"additionalProperties": {"anyOf": [{"$ref": "#/$defs/ObjectMaskConfig"}, {"type": "null"}]}, "description": "Mask polygon used to prevent object detection in specified areas.", "title": "Object mask", "type": "object"}, "raw_mask": {"additionalProperties": {"anyOf": [{"$ref": "#/$defs/ObjectMaskConfig"}, {"type": "null"}]}, "title": "Raw Mask", "type": "object"}, "genai": {"$ref": "#/$defs/GenAIObjectConfig", "description": "GenAI options for describing tracked objects and sending frames for generation.", "title": "GenAI object config"}}, "title": "ObjectConfig", "type": "object"}, "ObjectMaskConfig": {"additionalProperties": false, "description": "Configuration for a single object mask.", "properties": {"friendly_name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "A friendly name for this object mask used in the Frigate UI", "title": "Friendly name"}, "enabled": {"default": true, "description": "Enable or disable this object mask", "title": "Enabled", "type": "boolean"}, "coordinates": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "", "description": "Ordered x,y coordinates defining the object mask polygon used to include/exclude areas.", "title": "Coordinates"}, "raw_coordinates": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "", "title": "Raw Coordinates"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "title": "Keep track of original state of object mask."}}, "title": "ObjectMaskConfig", "type": "object"}, "OnvifConfig": {"additionalProperties": false, "properties": {"host": {"default": "", "description": "Host (and optional scheme) for the ONVIF service for this camera.", "title": "ONVIF host", "type": "string"}, "port": {"default": 8000, "description": "Port number for the ONVIF service.", "title": "ONVIF port", "type": "integer"}, "user": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Username for ONVIF authentication; some devices require admin user for ONVIF.", "title": "ONVIF username"}, "password": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Password for ONVIF authentication.", "title": "ONVIF password"}, "tls_insecure": {"default": false, "description": "Skip TLS verification and disable digest auth for ONVIF (unsafe; use in safe networks only).", "title": "Disable TLS verify", "type": "boolean"}, "profile": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Specific ONVIF media profile to use for PTZ control, matched by token or name. If not set, the first profile with valid PTZ configuration is selected automatically.", "title": "ONVIF profile"}, "autotracking": {"$ref": "#/$defs/PtzAutotrackConfig", "description": "Automatically track moving objects and keep them centered in the frame using PTZ camera movements.", "title": "Autotracking"}, "ignore_time_mismatch": {"default": false, "description": "Ignore time synchronization differences between camera and Frigate server for ONVIF communication.", "title": "Ignore time mismatch", "type": "boolean"}}, "title": "OnvifConfig", "type": "object"}, "PixelFormatEnum": {"enum": ["rgb", "bgr", "yuv"], "title": "PixelFormatEnum", "type": "string"}, "ProfileDefinitionConfig": {"additionalProperties": false, "description": "Defines a named profile with a human-readable display name.\n\nThe dict key is the machine name used internally; friendly_name\nis the label shown in the UI and API responses.", "properties": {"friendly_name": {"description": "Display name for this profile shown in the UI.", "title": "Friendly name", "type": "string"}}, "required": ["friendly_name"], "title": "ProfileDefinitionConfig", "type": "object"}, "ProxyConfig": {"additionalProperties": false, "properties": {"header_map": {"$ref": "#/$defs/HeaderMappingConfig", "description": "Map incoming proxy headers to Frigate user and role fields for proxy-based auth.", "title": "Header mapping"}, "logout_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "URL to redirect users to when logging out via the proxy.", "title": "Logout URL"}, "auth_secret": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Optional secret checked against the X-Proxy-Secret header to verify trusted proxies.", "title": "Proxy secret"}, "default_role": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": "viewer", "description": "Default role assigned to proxy-authenticated users when no role mapping applies.", "title": "Default role"}, "separator": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": ",", "description": "Character used to split multiple values provided in proxy headers.", "title": "Separator character"}}, "title": "ProxyConfig", "type": "object"}, "PtzAutotrackConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable automatic PTZ camera tracking of detected objects.", "title": "Enable Autotracking", "type": "boolean"}, "calibrate_on_startup": {"default": false, "description": "Measure PTZ motor speeds on startup to improve tracking accuracy. Frigate will update config with movement_weights after calibration.", "title": "Calibrate on start", "type": "boolean"}, "zooming": {"$ref": "#/$defs/ZoomingModeEnum", "default": "disabled", "description": "Control zoom behavior: disabled (pan/tilt only), absolute (most compatible), or relative (concurrent pan/tilt/zoom).", "title": "Zoom mode"}, "zoom_factor": {"default": 0.3, "description": "Control zoom level on tracked objects. Lower values keep more scene in view; higher values zoom in closer but may lose tracking. Values between 0.1 and 0.75.", "maximum": 0.75, "minimum": 0.1, "title": "Zoom factor", "type": "number"}, "track": {"default": ["person"], "description": "List of object types that should trigger autotracking.", "items": {"type": "string"}, "title": "Tracked objects", "type": "array"}, "required_zones": {"description": "Objects must enter one of these zones before autotracking begins.", "items": {"type": "string"}, "title": "Required zones", "type": "array"}, "return_preset": {"default": "home", "description": "ONVIF preset name configured in camera firmware to return to after tracking ends.", "title": "Return preset", "type": "string"}, "timeout": {"default": 10, "description": "Wait this many seconds after losing tracking before returning camera to preset position.", "title": "Return timeout", "type": "integer"}, "movement_weights": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}, {"type": "null"}], "description": "Calibration values automatically generated by camera calibration. Do not modify manually.", "title": "Movement weights"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Internal field to track whether autotracking was enabled in configuration.", "title": "Original autotrack state"}}, "title": "PtzAutotrackConfig", "type": "object"}, "RecordConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable recording for all cameras; can be overridden per-camera.", "title": "Enable recording", "type": "boolean"}, "expire_interval": {"default": 60, "description": "Minutes between cleanup passes that remove expired recording segments.", "title": "Record cleanup interval", "type": "integer"}, "continuous": {"$ref": "#/$defs/RecordRetainConfig", "description": "Number of days to retain recordings regardless of tracked objects or motion. Set to 0 if you only want to retain recordings of alerts and detections.", "title": "Continuous retention"}, "motion": {"$ref": "#/$defs/RecordRetainConfig", "description": "Number of days to retain recordings triggered by motion regardless of tracked objects. Set to 0 if you only want to retain recordings of alerts and detections.", "title": "Motion retention"}, "detections": {"$ref": "#/$defs/EventsConfig", "description": "Recording retention settings for detection events including pre/post capture durations.", "title": "Detection retention"}, "alerts": {"$ref": "#/$defs/EventsConfig", "description": "Recording retention settings for alert events including pre/post capture durations.", "title": "Alert retention"}, "export": {"$ref": "#/$defs/RecordExportConfig", "description": "Settings used when exporting recordings such as timelapse and hardware acceleration.", "title": "Export config"}, "preview": {"$ref": "#/$defs/RecordPreviewConfig", "description": "Settings controlling the quality of recording previews shown in the UI.", "title": "Preview config"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Indicates whether recording was enabled in the original static configuration.", "title": "Original recording state"}}, "title": "RecordConfig", "type": "object"}, "RecordExportConfig": {"additionalProperties": false, "properties": {"hwaccel_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "auto", "description": "Hardware acceleration args to use for export/transcode operations.", "title": "Export hwaccel args"}, "max_concurrent": {"default": 3, "description": "Maximum number of export jobs to process at the same time.", "minimum": 1, "title": "Maximum concurrent exports", "type": "integer"}}, "title": "RecordExportConfig", "type": "object"}, "RecordPreviewConfig": {"additionalProperties": false, "properties": {"quality": {"$ref": "#/$defs/RecordQualityEnum", "default": "medium", "description": "Preview quality level (very_low, low, medium, high, very_high).", "title": "Preview quality"}}, "title": "RecordPreviewConfig", "type": "object"}, "RecordQualityEnum": {"enum": ["very_low", "low", "medium", "high", "very_high"], "title": "RecordQualityEnum", "type": "string"}, "RecordRetainConfig": {"additionalProperties": false, "properties": {"days": {"default": 0, "description": "Days to retain recordings.", "minimum": 0.0, "title": "Retention days", "type": "number"}}, "title": "RecordRetainConfig", "type": "object"}, "ReplaceRule": {"additionalProperties": false, "properties": {"pattern": {"title": "Regex pattern", "type": "string"}, "replacement": {"title": "Replacement string", "type": "string"}}, "required": ["pattern", "replacement"], "title": "ReplaceRule", "type": "object"}, "RestreamConfig": {"additionalProperties": true, "properties": {}, "title": "RestreamConfig", "type": "object"}, "RetainConfig": {"additionalProperties": false, "properties": {"default": {"type": "number", "default": 10, "title": "Default retention", "description": "Default number of days to retain snapshots."}, "objects": {"additionalProperties": {"type": "number"}, "description": "Per-object overrides for snapshot retention days.", "title": "Object retention", "type": "object"}}, "title": "RetainConfig", "type": "object"}, "RetainModeEnum": {"enum": ["all", "motion", "active_objects"], "title": "RetainModeEnum", "type": "string"}, "ReviewConfig": {"additionalProperties": false, "properties": {"alerts": {"$ref": "#/$defs/AlertsConfig", "description": "Settings for which tracked objects generate alerts and how alerts are retained.", "title": "Alerts config"}, "detections": {"$ref": "#/$defs/DetectionsConfig", "description": "Settings for which tracked objects generate detections (non-alert) and how detections are retained.", "title": "Detections config"}, "genai": {"$ref": "#/$defs/GenAIReviewConfig", "description": "Controls use of generative AI for producing descriptions and summaries of review items.", "title": "GenAI config"}}, "title": "ReviewConfig", "type": "object"}, "ReviewRetainConfig": {"additionalProperties": false, "properties": {"days": {"default": 10, "description": "Number of days to retain recordings of detection events.", "minimum": 0.0, "title": "Retention days", "type": "number"}, "mode": {"$ref": "#/$defs/RetainModeEnum", "default": "motion", "description": "Mode for retention: all (save all segments), motion (save segments with motion), or active_objects (save segments with active objects).", "title": "Retention mode"}}, "title": "ReviewRetainConfig", "type": "object"}, "SemanticSearchConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable the semantic search feature.", "title": "Enable semantic search", "type": "boolean"}, "reindex": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": false, "description": "Trigger a full reindex of historical tracked objects into the embeddings database.", "title": "Reindex on startup"}, "model": {"anyOf": [{"$ref": "#/$defs/SemanticSearchModelEnum"}, {"type": "string"}, {"type": "null"}], "default": "jinav1", "description": "The embeddings model to use for semantic search (for example 'jinav1'), or the name of a GenAI provider with the embeddings role.", "title": "Semantic search model or GenAI provider name"}, "model_size": {"$ref": "#/$defs/ModelSizeEnum", "default": "small", "description": "Select model size; 'small' runs on CPU and 'large' typically requires GPU.", "title": "Model size"}, "device": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information", "title": "Device"}}, "title": "SemanticSearchConfig", "type": "object"}, "SemanticSearchModelEnum": {"enum": ["jinav1", "jinav2"], "title": "SemanticSearchModelEnum", "type": "string"}, "SnapshotsConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable saving snapshots for all cameras; can be overridden per-camera.", "title": "Enable snapshots", "type": "boolean"}, "timestamp": {"default": false, "description": "Overlay a timestamp on snapshots from API.", "title": "Timestamp overlay", "type": "boolean"}, "bounding_box": {"default": true, "description": "Draw bounding boxes for tracked objects on snapshots from API.", "title": "Bounding box overlay", "type": "boolean"}, "crop": {"default": false, "description": "Crop snapshots from API to the detected object's bounding box.", "title": "Crop snapshot", "type": "boolean"}, "required_zones": {"description": "Zones an object must enter for a snapshot to be saved.", "items": {"type": "string"}, "title": "Required zones", "type": "array"}, "height": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": null, "description": "Height (pixels) to resize snapshots from API to; leave empty to preserve original size.", "title": "Snapshot height"}, "retain": {"$ref": "#/$defs/RetainConfig", "description": "Retention settings for snapshots including default days and per-object overrides.", "title": "Snapshot retention"}, "quality": {"default": 60, "description": "Encode quality for saved snapshots (0-100).", "maximum": 100, "minimum": 0, "title": "Snapshot quality", "type": "integer"}}, "title": "SnapshotsConfig", "type": "object"}, "StationaryConfig": {"additionalProperties": false, "properties": {"interval": {"anyOf": [{"exclusiveMinimum": 0, "type": "integer"}, {"type": "null"}], "default": null, "description": "How often (in frames) to run a detection check to confirm a stationary object.", "title": "Stationary interval"}, "threshold": {"anyOf": [{"minimum": 1, "type": "integer"}, {"type": "null"}], "default": null, "description": "Number of frames with no position change required to mark an object as stationary.", "title": "Stationary threshold"}, "max_frames": {"$ref": "#/$defs/StationaryMaxFramesConfig", "description": "Limits how long stationary objects are tracked before being discarded.", "title": "Max frames"}, "classifier": {"default": true, "description": "Use a visual classifier to detect truly stationary objects even when bounding boxes jitter.", "title": "Enable visual classifier", "type": "boolean"}}, "title": "StationaryConfig", "type": "object"}, "StationaryMaxFramesConfig": {"additionalProperties": false, "properties": {"default": {"anyOf": [{"minimum": 1, "type": "integer"}, {"type": "null"}], "default": null, "title": "Default max frames", "description": "Default maximum frames to track a stationary object before stopping."}, "objects": {"additionalProperties": {"type": "integer"}, "description": "Per-object overrides for maximum frames to track stationary objects.", "title": "Object max frames", "type": "object"}}, "title": "StationaryMaxFramesConfig", "type": "object"}, "StatsConfig": {"additionalProperties": false, "properties": {"amd_gpu_stats": {"default": true, "description": "Enable collection of AMD GPU statistics if an AMD GPU is present.", "title": "AMD GPU stats", "type": "boolean"}, "intel_gpu_stats": {"default": true, "description": "Enable collection of Intel GPU statistics if an Intel GPU is present.", "title": "Intel GPU stats", "type": "boolean"}, "network_bandwidth": {"default": false, "description": "Enable per-process network bandwidth monitoring for camera ffmpeg processes and detectors (requires capabilities).", "title": "Network bandwidth", "type": "boolean"}, "intel_gpu_device": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "PCI bus address or DRM device path (e.g. /dev/dri/card1) used to pin Intel GPU stats to a specific device when multiple are present.", "title": "Intel GPU device"}}, "title": "StatsConfig", "type": "object"}, "TelemetryConfig": {"additionalProperties": false, "properties": {"network_interfaces": {"default": [], "description": "List of network interface name prefixes to monitor for bandwidth statistics.", "items": {"type": "string"}, "title": "Network interfaces", "type": "array"}, "stats": {"$ref": "#/$defs/StatsConfig", "description": "Options to enable/disable collection of various system and GPU statistics.", "title": "System stats"}, "version_check": {"default": true, "description": "Enable an outbound check to detect if a newer Frigate version is available.", "title": "Version check", "type": "boolean"}}, "title": "TelemetryConfig", "type": "object"}, "TimeFormatEnum": {"enum": ["browser", "12hour", "24hour"], "title": "TimeFormatEnum", "type": "string"}, "TimestampEffectEnum": {"enum": ["solid", "shadow"], "title": "TimestampEffectEnum", "type": "string"}, "TimestampPositionEnum": {"enum": ["tl", "tr", "bl", "br"], "title": "TimestampPositionEnum", "type": "string"}, "TimestampStyleConfig": {"additionalProperties": false, "properties": {"position": {"$ref": "#/$defs/TimestampPositionEnum", "default": "tl", "description": "Position of the timestamp on the image (tl/tr/bl/br).", "title": "Timestamp position"}, "format": {"default": "%m/%d/%Y %H:%M:%S", "description": "Datetime format string used for timestamps (Python datetime format codes).", "title": "Timestamp format", "type": "string"}, "color": {"$ref": "#/$defs/ColorConfig", "description": "RGB color values for the timestamp text (all values 0-255).", "title": "Timestamp color"}, "thickness": {"default": 2, "description": "Line thickness of the timestamp text.", "title": "Timestamp thickness", "type": "integer"}, "effect": {"anyOf": [{"$ref": "#/$defs/TimestampEffectEnum"}, {"type": "null"}], "default": null, "description": "Visual effect for the timestamp text (none, solid, shadow).", "title": "Timestamp effect"}}, "title": "TimestampStyleConfig", "type": "object"}, "TlsConfig": {"additionalProperties": false, "properties": {"enabled": {"default": true, "description": "Enable TLS for Frigate's web UI and API on the configured TLS port.", "title": "Enable TLS", "type": "boolean"}}, "title": "TlsConfig", "type": "object"}, "TriggerAction": {"enum": ["notification", "sub_label", "attribute"], "title": "TriggerAction", "type": "string"}, "TriggerConfig": {"additionalProperties": false, "properties": {"friendly_name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Optional friendly name displayed in the UI for this trigger.", "title": "Friendly name"}, "enabled": {"default": true, "description": "Enable or disable this semantic search trigger.", "title": "Enable this trigger", "type": "boolean"}, "type": {"$ref": "#/$defs/TriggerType", "default": "description", "description": "Type of trigger: 'thumbnail' (match against image) or 'description' (match against text).", "title": "Trigger type"}, "data": {"description": "Text phrase or thumbnail ID to match against tracked objects.", "title": "Trigger content", "type": "string"}, "threshold": {"default": 0.8, "description": "Minimum similarity score (0-1) required to activate this trigger.", "exclusiveMinimum": 0.0, "maximum": 1.0, "title": "Trigger threshold", "type": "number"}, "actions": {"default": [], "description": "List of actions to execute when trigger matches (notification, sub_label, attribute).", "items": {"$ref": "#/$defs/TriggerAction"}, "title": "Trigger actions", "type": "array"}}, "required": ["data"], "title": "TriggerConfig", "type": "object"}, "TriggerType": {"enum": ["thumbnail", "description"], "title": "TriggerType", "type": "string"}, "UIConfig": {"additionalProperties": false, "properties": {"timezone": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Optional timezone to display across the UI (defaults to browser local time if unset).", "title": "Timezone"}, "time_format": {"$ref": "#/$defs/TimeFormatEnum", "default": "browser", "description": "Time format to use in the UI (browser, 12hour, or 24hour).", "title": "Time format"}, "unit_system": {"$ref": "#/$defs/UnitSystemEnum", "default": "metric", "description": "Unit system for display (metric or imperial) used in the UI and MQTT.", "title": "Unit system"}}, "title": "UIConfig", "type": "object"}, "UnitSystemEnum": {"enum": ["imperial", "metric"], "title": "UnitSystemEnum", "type": "string"}, "ZoneConfig": {"properties": {"friendly_name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "A user-friendly name for the zone, displayed in the Frigate UI. If not set, a formatted version of the zone name will be used.", "title": "Zone name"}, "enabled": {"default": true, "description": "Enable or disable this zone. Disabled zones are ignored at runtime.", "title": "Enabled", "type": "boolean"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "title": "Keep track of original state of zone."}, "filters": {"additionalProperties": {"$ref": "#/$defs/FilterConfig"}, "description": "Filters to apply to objects within this zone. Used to reduce false positives or restrict which objects are considered present in the zone.", "title": "Zone filters", "type": "object"}, "coordinates": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "Polygon coordinates that define the zone area. Can be a comma-separated string or a list of coordinate strings. Coordinates should be relative (0-1) or absolute (legacy).", "title": "Coordinates"}, "distances": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}, {"type": "null"}], "description": "Optional real-world distances for each side of the zone quadrilateral, used for speed or distance calculations. Must have exactly 4 values if set.", "title": "Real-world distances"}, "inertia": {"default": 3, "description": "Number of consecutive frames an object must be detected in the zone before it is considered present. Helps filter out transient detections.", "exclusiveMinimum": 0, "title": "Inertia frames", "type": "integer"}, "loitering_time": {"default": 0, "description": "Number of seconds an object must remain in the zone to be considered as loitering. Set to 0 to disable loitering detection.", "minimum": 0, "title": "Loitering seconds", "type": "integer"}, "speed_threshold": {"anyOf": [{"minimum": 0.1, "type": "number"}, {"type": "null"}], "default": null, "description": "Minimum speed (in real-world units if distances are set) required for an object to be considered present in the zone. Used for speed-based zone triggers.", "title": "Minimum speed"}, "objects": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "List of object types (from labelmap) that can trigger this zone. Can be a string or a list of strings. If empty, all objects are considered.", "title": "Trigger objects"}}, "required": ["coordinates"], "title": "ZoneConfig", "type": "object"}, "ZoomingModeEnum": {"enum": ["disabled", "absolute", "relative"], "title": "ZoomingModeEnum", "type": "string"}}, "additionalProperties": false, "properties": {"version": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Numeric or string version of the active configuration to help detect migrations or format changes.", "title": "Current config version"}, "safe_mode": {"default": false, "description": "When enabled, start Frigate in safe mode with reduced features for troubleshooting.", "title": "Safe mode", "type": "boolean"}, "environment_vars": {"additionalProperties": {"type": "string"}, "description": "Key/value pairs of environment variables to set for the Frigate process in Home Assistant OS. Non-HAOS users must use Docker environment variable configuration instead.", "title": "Environment variables", "type": "object"}, "logger": {"$ref": "#/$defs/LoggerConfig", "description": "Controls default log verbosity and per-component log level overrides.", "title": "Logging"}, "auth": {"$ref": "#/$defs/AuthConfig", "description": "Authentication and session-related settings including cookie and rate limit options.", "title": "Authentication"}, "database": {"$ref": "#/$defs/DatabaseConfig", "description": "Settings for the SQLite database used by Frigate to store tracked object and recording metadata.", "title": "Database"}, "go2rtc": {"$ref": "#/$defs/RestreamConfig", "description": "Settings for the integrated go2rtc restreaming service used for live stream relaying and translation.", "title": "go2rtc"}, "mqtt": {"$ref": "#/$defs/MqttConfig", "description": "Settings for connecting and publishing telemetry, snapshots, and event details to an MQTT broker.", "title": "MQTT"}, "notifications": {"$ref": "#/$defs/NotificationConfig", "description": "Settings to enable and control notifications for all cameras; can be overridden per-camera.", "title": "Notifications"}, "networking": {"$ref": "#/$defs/NetworkingConfig", "description": "Network-related settings such as IPv6 enablement for Frigate endpoints.", "title": "Networking"}, "proxy": {"$ref": "#/$defs/ProxyConfig", "description": "Settings for integrating Frigate behind a reverse proxy that passes authenticated user headers.", "title": "Proxy"}, "telemetry": {"$ref": "#/$defs/TelemetryConfig", "description": "System telemetry and stats options including GPU and network bandwidth monitoring.", "title": "Telemetry"}, "tls": {"$ref": "#/$defs/TlsConfig", "description": "TLS settings for Frigate's web endpoints (port 8971).", "title": "TLS"}, "ui": {"$ref": "#/$defs/UIConfig", "description": "User interface preferences such as timezone, time/date formatting, and units.", "title": "UI"}, "detectors": {"additionalProperties": {"$ref": "#/$defs/BaseDetectorConfig"}, "default": {"cpu": {"type": "cpu"}}, "description": "Configuration for object detectors (CPU, GPU, ONNX backends) and any detector-specific model settings.", "title": "Detector hardware", "type": "object"}, "model": {"$ref": "#/$defs/ModelConfig", "description": "Settings to configure a custom object detection model and its input shape.", "title": "Detection model"}, "genai": {"additionalProperties": {"$ref": "#/$defs/GenAIConfig"}, "description": "Settings for integrated generative AI providers used to generate object descriptions and review summaries.", "title": "Generative AI configuration", "type": "object"}, "cameras": {"additionalProperties": {"$ref": "#/$defs/CameraConfig"}, "description": "Cameras", "title": "Cameras", "type": "object"}, "audio": {"$ref": "#/$defs/AudioConfig", "description": "Settings for audio-based event detection for all cameras; can be overridden per-camera.", "title": "Audio detection"}, "birdseye": {"$ref": "#/$defs/BirdseyeConfig", "description": "Settings for the Birdseye composite view that composes multiple camera feeds into a single layout.", "title": "Birdseye"}, "detect": {"$ref": "#/$defs/DetectConfig", "description": "Settings for the detection/detect role used to run object detection and initialize trackers.", "title": "Object Detection"}, "ffmpeg": {"$ref": "#/$defs/FfmpegConfig", "description": "FFmpeg settings including binary path, args, hwaccel options, and per-role output args.", "title": "FFmpeg"}, "live": {"$ref": "#/$defs/CameraLiveConfig", "description": "Settings to control the jsmpeg live stream resolution and quality. This does not affect restreamed cameras that use go2rtc for live view.", "title": "Live playback"}, "motion": {"anyOf": [{"$ref": "#/$defs/MotionConfig"}, {"type": "null"}], "default": null, "description": "Default motion detection settings applied to cameras unless overridden per-camera.", "title": "Motion detection"}, "objects": {"$ref": "#/$defs/ObjectConfig", "description": "Object tracking defaults including which labels to track and per-object filters.", "title": "Objects"}, "record": {"$ref": "#/$defs/RecordConfig", "description": "Recording and retention settings applied to cameras unless overridden per-camera.", "title": "Recording"}, "review": {"$ref": "#/$defs/ReviewConfig", "description": "Settings that control alerts, detections, and GenAI review summaries used by the UI and storage.", "title": "Review"}, "snapshots": {"$ref": "#/$defs/SnapshotsConfig", "description": "Settings for API-generated snapshots of tracked objects for all cameras; can be overridden per-camera.", "title": "Snapshots"}, "timestamp_style": {"$ref": "#/$defs/TimestampStyleConfig", "description": "Styling options for in-feed timestamps applied to debug view and snapshots.", "title": "Timestamp style"}, "audio_transcription": {"$ref": "#/$defs/AudioTranscriptionConfig", "description": "Settings for live and speech audio transcription used for events and live captions.", "title": "Audio transcription"}, "classification": {"$ref": "#/$defs/ClassificationConfig", "description": "Settings for classification models used to refine object labels or state classification.", "title": "Object classification"}, "semantic_search": {"$ref": "#/$defs/SemanticSearchConfig", "description": "Settings for Semantic Search which builds and queries object embeddings to find similar items.", "title": "Semantic Search"}, "face_recognition": {"$ref": "#/$defs/FaceRecognitionConfig", "description": "Settings for face detection and recognition for all cameras; can be overridden per-camera.", "title": "Face recognition"}, "lpr": {"$ref": "#/$defs/LicensePlateRecognitionConfig", "description": "License plate recognition settings including detection thresholds, formatting, and known plates.", "title": "License Plate Recognition"}, "camera_groups": {"additionalProperties": {"$ref": "#/$defs/CameraGroupConfig"}, "description": "Configuration for named camera groups used to organize cameras in the UI.", "title": "Camera groups", "type": "object"}, "profiles": {"additionalProperties": {"$ref": "#/$defs/ProfileDefinitionConfig"}, "description": "Named profile definitions with friendly names. Camera profiles must reference names defined here.", "title": "Profiles", "type": "object"}, "active_profile": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Currently active profile name. Runtime-only, not persisted in YAML.", "title": "Active profile"}}, "required": ["mqtt", "cameras"], "title": "FrigateConfig", "type": "object"}
\ No newline at end of file
diff --git a/web/e2e/fixtures/mock-data/config-snapshot.json b/web/e2e/fixtures/mock-data/config-snapshot.json
index 6b87982c48..2df402bc99 100644
--- a/web/e2e/fixtures/mock-data/config-snapshot.json
+++ b/web/e2e/fixtures/mock-data/config-snapshot.json
@@ -1 +1 @@
-{"version": null, "safe_mode": false, "environment_vars": {}, "logger": {"default": "info", "logs": {}}, "auth": {"enabled": true, "reset_admin_password": false, "cookie_name": "frigate_token", "cookie_secure": false, "session_length": 86400, "refresh_time": 1800, "failed_login_rate_limit": null, "trusted_proxies": [], "hash_iterations": 600000, "roles": {"admin": [], "viewer": []}, "admin_first_time_login": false}, "database": {"path": "/config/frigate.db"}, "go2rtc": {}, "mqtt": {"enabled": true, "host": "mqtt", "port": 1883, "topic_prefix": "frigate", "client_id": "frigate", "stats_interval": 60, "user": null, "password": null, "tls_ca_certs": null, "tls_client_cert": null, "tls_client_key": null, "tls_insecure": null, "qos": 0}, "notifications": {"enabled": false, "email": null, "cooldown": 0, "enabled_in_config": false}, "networking": {"ipv6": {"enabled": false}, "listen": {"internal": 5000, "external": 8971}}, "proxy": {"header_map": {"user": null, "role": null, "role_map": {}}, "logout_url": null, "auth_secret": null, "default_role": "viewer", "separator": ","}, "telemetry": {"network_interfaces": [], "stats": {"amd_gpu_stats": true, "intel_gpu_stats": true, "network_bandwidth": false, "intel_gpu_device": null}, "version_check": true}, "tls": {"enabled": true}, "ui": {"timezone": null, "time_format": "browser", "date_style": "short", "time_style": "medium", "unit_system": "metric"}, "detectors": {"cpu": {"type": "cpu", "model": {"path": "/cpu_model.tflite", "labelmap_path": null, "width": 320, "height": 320, "labelmap": {}, "attributes_map": {"person": ["amazon", "face"], "car": ["amazon", "an_post", "canada_post", "dhl", "dpd", "fedex", "gls", "license_plate", "nzpost", "postnl", "postnord", "purolator", "royal_mail", "ups", "usps"], "motorcycle": ["license_plate"]}, "input_tensor": "nhwc", "input_pixel_format": "rgb", "input_dtype": "int", "model_type": "ssd"}, "model_path": null}}, "model": {"path": null, "labelmap_path": null, "width": 320, "height": 320, "labelmap": {}, "attributes_map": {"person": ["amazon", "face"], "car": ["amazon", "an_post", "canada_post", "dhl", "dpd", "fedex", "gls", "license_plate", "nzpost", "postnl", "postnord", "purolator", "royal_mail", "ups", "usps"], "motorcycle": ["license_plate"]}, "input_tensor": "nhwc", "input_pixel_format": "rgb", "input_dtype": "int", "model_type": "ssd", "all_attributes": ["amazon", "an_post", "canada_post", "dhl", "dpd", "face", "fedex", "gls", "license_plate", "nzpost", "postnl", "postnord", "purolator", "royal_mail", "ups", "usps"], "colormap": {}}, "genai": {}, "cameras": {"front_door": {"name": "front_door", "friendly_name": null, "enabled": true, "audio": {"enabled": false, "max_not_heard": 30, "min_volume": 500, "listen": ["bark", "fire_alarm", "scream", "speech", "yell"], "filters": {"a_capella": {"threshold": 0.8}, "accelerating": {"threshold": 0.8}, "accordion": {"threshold": 0.8}, "acoustic_guitar": {"threshold": 0.8}, "afrobeat": {"threshold": 0.8}, "air_brake": {"threshold": 0.8}, "air_conditioning": {"threshold": 0.8}, "air_horn": {"threshold": 0.8}, "aircraft": {"threshold": 0.8}, "aircraft_engine": {"threshold": 0.8}, "alarm": {"threshold": 0.8}, "alarm_clock": {"threshold": 0.8}, "ambient_music": {"threshold": 0.8}, "ambulance": {"threshold": 0.8}, "angry_music": {"threshold": 0.8}, "animal": {"threshold": 0.8}, "applause": {"threshold": 0.8}, "arrow": {"threshold": 0.8}, "artillery_fire": {"threshold": 0.8}, "babbling": {"threshold": 0.8}, "background_music": {"threshold": 0.8}, "bagpipes": {"threshold": 0.8}, "bang": {"threshold": 0.8}, "banjo": {"threshold": 0.8}, "bark": {"threshold": 0.8}, "basketball_bounce": {"threshold": 0.8}, "bass_drum": {"threshold": 0.8}, "bass_guitar": {"threshold": 0.8}, "bathtub": {"threshold": 0.8}, "beatboxing": {"threshold": 0.8}, "beep": {"threshold": 0.8}, "bell": {"threshold": 0.8}, "bellow": {"threshold": 0.8}, "bicycle": {"threshold": 0.8}, "bicycle_bell": {"threshold": 0.8}, "bird": {"threshold": 0.8}, "biting": {"threshold": 0.8}, "bleat": {"threshold": 0.8}, "blender": {"threshold": 0.8}, "bluegrass": {"threshold": 0.8}, "blues": {"threshold": 0.8}, "boat": {"threshold": 0.8}, "boiling": {"threshold": 0.8}, "boing": {"threshold": 0.8}, "boom": {"threshold": 0.8}, "bouncing": {"threshold": 0.8}, "bow-wow": {"threshold": 0.8}, "bowed_string_instrument": {"threshold": 0.8}, "brass_instrument": {"threshold": 0.8}, "breaking": {"threshold": 0.8}, "breathing": {"threshold": 0.8}, "burping": {"threshold": 0.8}, "burst": {"threshold": 0.8}, "bus": {"threshold": 0.8}, "busy_signal": {"threshold": 0.8}, "buzz": {"threshold": 0.8}, "buzzer": {"threshold": 0.8}, "cacophony": {"threshold": 0.8}, "camera": {"threshold": 0.8}, "cap_gun": {"threshold": 0.8}, "car": {"threshold": 0.8}, "car_alarm": {"threshold": 0.8}, "car_passing_by": {"threshold": 0.8}, "carnatic_music": {"threshold": 0.8}, "cash_register": {"threshold": 0.8}, "cat": {"threshold": 0.8}, "caterwaul": {"threshold": 0.8}, "cattle": {"threshold": 0.8}, "caw": {"threshold": 0.8}, "cello": {"threshold": 0.8}, "chainsaw": {"threshold": 0.8}, "change_ringing": {"threshold": 0.8}, "chant": {"threshold": 0.8}, "chatter": {"threshold": 0.8}, "cheering": {"threshold": 0.8}, "chewing": {"threshold": 0.8}, "chicken": {"threshold": 0.8}, "child_singing": {"threshold": 0.8}, "children_playing": {"threshold": 0.8}, "chime": {"threshold": 0.8}, "chink": {"threshold": 0.8}, "chird": {"threshold": 0.8}, "chirp": {"threshold": 0.8}, "chirp_tone": {"threshold": 0.8}, "choir": {"threshold": 0.8}, "chop": {"threshold": 0.8}, "chopping": {"threshold": 0.8}, "chorus_effect": {"threshold": 0.8}, "christian_music": {"threshold": 0.8}, "christmas_music": {"threshold": 0.8}, "church_bell": {"threshold": 0.8}, "civil_defense_siren": {"threshold": 0.8}, "clang": {"threshold": 0.8}, "clapping": {"threshold": 0.8}, "clarinet": {"threshold": 0.8}, "classical_music": {"threshold": 0.8}, "clatter": {"threshold": 0.8}, "clickety-clack": {"threshold": 0.8}, "clicking": {"threshold": 0.8}, "clip-clop": {"threshold": 0.8}, "clock": {"threshold": 0.8}, "cluck": {"threshold": 0.8}, "cock-a-doodle-doo": {"threshold": 0.8}, "coin": {"threshold": 0.8}, "computer_keyboard": {"threshold": 0.8}, "coo": {"threshold": 0.8}, "cough": {"threshold": 0.8}, "country": {"threshold": 0.8}, "cowbell": {"threshold": 0.8}, "crack": {"threshold": 0.8}, "crackle": {"threshold": 0.8}, "creak": {"threshold": 0.8}, "cricket": {"threshold": 0.8}, "croak": {"threshold": 0.8}, "crow": {"threshold": 0.8}, "crowd": {"threshold": 0.8}, "crumpling": {"threshold": 0.8}, "crunch": {"threshold": 0.8}, "crushing": {"threshold": 0.8}, "crying": {"threshold": 0.8}, "cupboard_open_or_close": {"threshold": 0.8}, "cutlery": {"threshold": 0.8}, "cymbal": {"threshold": 0.8}, "dance_music": {"threshold": 0.8}, "dental_drill's_drill": {"threshold": 0.8}, "dial_tone": {"threshold": 0.8}, "didgeridoo": {"threshold": 0.8}, "ding": {"threshold": 0.8}, "ding-dong": {"threshold": 0.8}, "disco": {"threshold": 0.8}, "dishes": {"threshold": 0.8}, "distortion": {"threshold": 0.8}, "dog": {"threshold": 0.8}, "dogs": {"threshold": 0.8}, "door": {"threshold": 0.8}, "doorbell": {"threshold": 0.8}, "double_bass": {"threshold": 0.8}, "drawer_open_or_close": {"threshold": 0.8}, "drill": {"threshold": 0.8}, "drip": {"threshold": 0.8}, "drum": {"threshold": 0.8}, "drum_and_bass": {"threshold": 0.8}, "drum_kit": {"threshold": 0.8}, "drum_machine": {"threshold": 0.8}, "drum_roll": {"threshold": 0.8}, "dubstep": {"threshold": 0.8}, "duck": {"threshold": 0.8}, "echo": {"threshold": 0.8}, "effects_unit": {"threshold": 0.8}, "electric_guitar": {"threshold": 0.8}, "electric_piano": {"threshold": 0.8}, "electric_shaver": {"threshold": 0.8}, "electric_toothbrush": {"threshold": 0.8}, "electronic_dance_music": {"threshold": 0.8}, "electronic_music": {"threshold": 0.8}, "electronic_organ": {"threshold": 0.8}, "electronic_tuner": {"threshold": 0.8}, "electronica": {"threshold": 0.8}, "emergency_vehicle": {"threshold": 0.8}, "engine": {"threshold": 0.8}, "engine_knocking": {"threshold": 0.8}, "engine_starting": {"threshold": 0.8}, "environmental_noise": {"threshold": 0.8}, "eruption": {"threshold": 0.8}, "exciting_music": {"threshold": 0.8}, "explosion": {"threshold": 0.8}, "fart": {"threshold": 0.8}, "field_recording": {"threshold": 0.8}, "filing": {"threshold": 0.8}, "fill": {"threshold": 0.8}, "finger_snapping": {"threshold": 0.8}, "fire": {"threshold": 0.8}, "fire_alarm": {"threshold": 0.8}, "fire_engine": {"threshold": 0.8}, "firecracker": {"threshold": 0.8}, "fireworks": {"threshold": 0.8}, "fixed-wing_aircraft": {"threshold": 0.8}, "flamenco": {"threshold": 0.8}, "flap": {"threshold": 0.8}, "flapping_wings": {"threshold": 0.8}, "flute": {"threshold": 0.8}, "fly": {"threshold": 0.8}, "foghorn": {"threshold": 0.8}, "folk_music": {"threshold": 0.8}, "footsteps": {"threshold": 0.8}, "fowl": {"threshold": 0.8}, "french_horn": {"threshold": 0.8}, "frog": {"threshold": 0.8}, "frying": {"threshold": 0.8}, "funk": {"threshold": 0.8}, "fusillade": {"threshold": 0.8}, "gargling": {"threshold": 0.8}, "gasp": {"threshold": 0.8}, "gears": {"threshold": 0.8}, "glass": {"threshold": 0.8}, "glockenspiel": {"threshold": 0.8}, "goat": {"threshold": 0.8}, "gobble": {"threshold": 0.8}, "gong": {"threshold": 0.8}, "goose": {"threshold": 0.8}, "gospel_music": {"threshold": 0.8}, "groan": {"threshold": 0.8}, "growling": {"threshold": 0.8}, "grunge": {"threshold": 0.8}, "grunt": {"threshold": 0.8}, "guitar": {"threshold": 0.8}, "gunshot": {"threshold": 0.8}, "gurgling": {"threshold": 0.8}, "gush": {"threshold": 0.8}, "hair_dryer": {"threshold": 0.8}, "hammer": {"threshold": 0.8}, "hammond_organ": {"threshold": 0.8}, "hands": {"threshold": 0.8}, "happy_music": {"threshold": 0.8}, "harmonic": {"threshold": 0.8}, "harmonica": {"threshold": 0.8}, "harp": {"threshold": 0.8}, "harpsichord": {"threshold": 0.8}, "heart_murmur": {"threshold": 0.8}, "heartbeat": {"threshold": 0.8}, "heavy_engine": {"threshold": 0.8}, "heavy_metal": {"threshold": 0.8}, "helicopter": {"threshold": 0.8}, "hi-hat": {"threshold": 0.8}, "hiccup": {"threshold": 0.8}, "hip_hop_music": {"threshold": 0.8}, "hiss": {"threshold": 0.8}, "honk": {"threshold": 0.8}, "hoot": {"threshold": 0.8}, "horse": {"threshold": 0.8}, "house_music": {"threshold": 0.8}, "howl": {"threshold": 0.8}, "hum": {"threshold": 0.8}, "humming": {"threshold": 0.8}, "ice_cream_truck": {"threshold": 0.8}, "idling": {"threshold": 0.8}, "independent_music": {"threshold": 0.8}, "insect": {"threshold": 0.8}, "inside": {"threshold": 0.8}, "jackhammer": {"threshold": 0.8}, "jazz": {"threshold": 0.8}, "jet_engine": {"threshold": 0.8}, "jingle": {"threshold": 0.8}, "jingle_bell": {"threshold": 0.8}, "keyboard": {"threshold": 0.8}, "keys_jangling": {"threshold": 0.8}, "knock": {"threshold": 0.8}, "laughter": {"threshold": 0.8}, "lawn_mower": {"threshold": 0.8}, "light_engine": {"threshold": 0.8}, "liquid": {"threshold": 0.8}, "livestock": {"threshold": 0.8}, "lullaby": {"threshold": 0.8}, "machine_gun": {"threshold": 0.8}, "mains_hum": {"threshold": 0.8}, "mallet_percussion": {"threshold": 0.8}, "mandolin": {"threshold": 0.8}, "mantra": {"threshold": 0.8}, "maraca": {"threshold": 0.8}, "marimba": {"threshold": 0.8}, "mechanical_fan": {"threshold": 0.8}, "mechanisms": {"threshold": 0.8}, "medium_engine": {"threshold": 0.8}, "meow": {"threshold": 0.8}, "microwave_oven": {"threshold": 0.8}, "middle_eastern_music": {"threshold": 0.8}, "moo": {"threshold": 0.8}, "mosquito": {"threshold": 0.8}, "motor_vehicle": {"threshold": 0.8}, "motorboat": {"threshold": 0.8}, "motorcycle": {"threshold": 0.8}, "mouse": {"threshold": 0.8}, "music": {"threshold": 0.8}, "music_for_children": {"threshold": 0.8}, "music_of_africa": {"threshold": 0.8}, "music_of_asia": {"threshold": 0.8}, "music_of_bollywood": {"threshold": 0.8}, "music_of_latin_america": {"threshold": 0.8}, "musical_instrument": {"threshold": 0.8}, "neigh": {"threshold": 0.8}, "new-age_music": {"threshold": 0.8}, "noise": {"threshold": 0.8}, "ocean": {"threshold": 0.8}, "oink": {"threshold": 0.8}, "opera": {"threshold": 0.8}, "orchestra": {"threshold": 0.8}, "organ": {"threshold": 0.8}, "outside": {"threshold": 0.8}, "owl": {"threshold": 0.8}, "pant": {"threshold": 0.8}, "patter": {"threshold": 0.8}, "percussion": {"threshold": 0.8}, "pets": {"threshold": 0.8}, "piano": {"threshold": 0.8}, "pig": {"threshold": 0.8}, "pigeon": {"threshold": 0.8}, "ping": {"threshold": 0.8}, "pink_noise": {"threshold": 0.8}, "pizzicato": {"threshold": 0.8}, "plop": {"threshold": 0.8}, "plucked_string_instrument": {"threshold": 0.8}, "police_car": {"threshold": 0.8}, "pop_music": {"threshold": 0.8}, "pour": {"threshold": 0.8}, "power_tool": {"threshold": 0.8}, "power_windows": {"threshold": 0.8}, "printer": {"threshold": 0.8}, "progressive_rock": {"threshold": 0.8}, "propeller": {"threshold": 0.8}, "psychedelic_rock": {"threshold": 0.8}, "pulleys": {"threshold": 0.8}, "pulse": {"threshold": 0.8}, "pump": {"threshold": 0.8}, "punk_rock": {"threshold": 0.8}, "purr": {"threshold": 0.8}, "quack": {"threshold": 0.8}, "race_car": {"threshold": 0.8}, "radio": {"threshold": 0.8}, "rail_transport": {"threshold": 0.8}, "railroad_car": {"threshold": 0.8}, "rain": {"threshold": 0.8}, "rain_on_surface": {"threshold": 0.8}, "raindrop": {"threshold": 0.8}, "rapping": {"threshold": 0.8}, "ratchet": {"threshold": 0.8}, "rats": {"threshold": 0.8}, "rattle": {"threshold": 0.8}, "reggae": {"threshold": 0.8}, "reverberation": {"threshold": 0.8}, "reversing_beeps": {"threshold": 0.8}, "rhythm_and_blues": {"threshold": 0.8}, "rimshot": {"threshold": 0.8}, "ringtone": {"threshold": 0.8}, "roar": {"threshold": 0.8}, "roaring_cats": {"threshold": 0.8}, "rock_and_roll": {"threshold": 0.8}, "rock_music": {"threshold": 0.8}, "roll": {"threshold": 0.8}, "rowboat": {"threshold": 0.8}, "rub": {"threshold": 0.8}, "rumble": {"threshold": 0.8}, "run": {"threshold": 0.8}, "rustle": {"threshold": 0.8}, "rustling_leaves": {"threshold": 0.8}, "sad_music": {"threshold": 0.8}, "sailboat": {"threshold": 0.8}, "salsa_music": {"threshold": 0.8}, "sampler": {"threshold": 0.8}, "sanding": {"threshold": 0.8}, "sawing": {"threshold": 0.8}, "saxophone": {"threshold": 0.8}, "scary_music": {"threshold": 0.8}, "scissors": {"threshold": 0.8}, "scrape": {"threshold": 0.8}, "scratch": {"threshold": 0.8}, "scratching": {"threshold": 0.8}, "sewing_machine": {"threshold": 0.8}, "shatter": {"threshold": 0.8}, "sheep": {"threshold": 0.8}, "ship": {"threshold": 0.8}, "shofar": {"threshold": 0.8}, "shuffle": {"threshold": 0.8}, "shuffling_cards": {"threshold": 0.8}, "sidetone": {"threshold": 0.8}, "sigh": {"threshold": 0.8}, "silence": {"threshold": 0.8}, "sine_wave": {"threshold": 0.8}, "singing": {"threshold": 0.8}, "singing_bowl": {"threshold": 0.8}, "single-lens_reflex_camera": {"threshold": 0.8}, "sink": {"threshold": 0.8}, "siren": {"threshold": 0.8}, "sitar": {"threshold": 0.8}, "sizzle": {"threshold": 0.8}, "ska": {"threshold": 0.8}, "skateboard": {"threshold": 0.8}, "skidding": {"threshold": 0.8}, "slam": {"threshold": 0.8}, "slap": {"threshold": 0.8}, "sliding_door": {"threshold": 0.8}, "slosh": {"threshold": 0.8}, "smash": {"threshold": 0.8}, "smoke_detector": {"threshold": 0.8}, "snake": {"threshold": 0.8}, "snare_drum": {"threshold": 0.8}, "sneeze": {"threshold": 0.8}, "snicker": {"threshold": 0.8}, "sniff": {"threshold": 0.8}, "snoring": {"threshold": 0.8}, "snort": {"threshold": 0.8}, "sodeling": {"threshold": 0.8}, "sonar": {"threshold": 0.8}, "song": {"threshold": 0.8}, "soul_music": {"threshold": 0.8}, "sound_effect": {"threshold": 0.8}, "soundtrack_music": {"threshold": 0.8}, "speech": {"threshold": 0.8}, "splash": {"threshold": 0.8}, "splinter": {"threshold": 0.8}, "spray": {"threshold": 0.8}, "squawk": {"threshold": 0.8}, "squeak": {"threshold": 0.8}, "squeal": {"threshold": 0.8}, "squish": {"threshold": 0.8}, "static": {"threshold": 0.8}, "steam": {"threshold": 0.8}, "steam_whistle": {"threshold": 0.8}, "steel_guitar": {"threshold": 0.8}, "steelpan": {"threshold": 0.8}, "stir": {"threshold": 0.8}, "stomach_rumble": {"threshold": 0.8}, "stream": {"threshold": 0.8}, "string_section": {"threshold": 0.8}, "strum": {"threshold": 0.8}, "subway": {"threshold": 0.8}, "swing_music": {"threshold": 0.8}, "synthesizer": {"threshold": 0.8}, "synthetic_singing": {"threshold": 0.8}, "tabla": {"threshold": 0.8}, "tambourine": {"threshold": 0.8}, "tap": {"threshold": 0.8}, "tapping": {"threshold": 0.8}, "tearing": {"threshold": 0.8}, "techno": {"threshold": 0.8}, "telephone": {"threshold": 0.8}, "telephone_bell_ringing": {"threshold": 0.8}, "telephone_dialing": {"threshold": 0.8}, "television": {"threshold": 0.8}, "tender_music": {"threshold": 0.8}, "theme_music": {"threshold": 0.8}, "theremin": {"threshold": 0.8}, "throat_clearing": {"threshold": 0.8}, "throbbing": {"threshold": 0.8}, "thump": {"threshold": 0.8}, "thunder": {"threshold": 0.8}, "thunderstorm": {"threshold": 0.8}, "thunk": {"threshold": 0.8}, "tick": {"threshold": 0.8}, "tick-tock": {"threshold": 0.8}, "timpani": {"threshold": 0.8}, "tire_squeal": {"threshold": 0.8}, "toilet_flush": {"threshold": 0.8}, "tools": {"threshold": 0.8}, "toot": {"threshold": 0.8}, "toothbrush": {"threshold": 0.8}, "traditional_music": {"threshold": 0.8}, "traffic_noise": {"threshold": 0.8}, "train": {"threshold": 0.8}, "train_horn": {"threshold": 0.8}, "train_wheels_squealing": {"threshold": 0.8}, "train_whistle": {"threshold": 0.8}, "trance_music": {"threshold": 0.8}, "trickle": {"threshold": 0.8}, "trombone": {"threshold": 0.8}, "truck": {"threshold": 0.8}, "trumpet": {"threshold": 0.8}, "tubular_bells": {"threshold": 0.8}, "tuning_fork": {"threshold": 0.8}, "turkey": {"threshold": 0.8}, "typewriter": {"threshold": 0.8}, "typing": {"threshold": 0.8}, "ukulele": {"threshold": 0.8}, "vacuum_cleaner": {"threshold": 0.8}, "vehicle": {"threshold": 0.8}, "vibraphone": {"threshold": 0.8}, "vibration": {"threshold": 0.8}, "video_game_music": {"threshold": 0.8}, "violin": {"threshold": 0.8}, "vocal_music": {"threshold": 0.8}, "water": {"threshold": 0.8}, "water_tap": {"threshold": 0.8}, "waterfall": {"threshold": 0.8}, "waves": {"threshold": 0.8}, "wedding_music": {"threshold": 0.8}, "whack": {"threshold": 0.8}, "whale_vocalization": {"threshold": 0.8}, "wheeze": {"threshold": 0.8}, "whimper_dog": {"threshold": 0.8}, "whip": {"threshold": 0.8}, "whir": {"threshold": 0.8}, "whispering": {"threshold": 0.8}, "whistle": {"threshold": 0.8}, "whistling": {"threshold": 0.8}, "white_noise": {"threshold": 0.8}, "whoop": {"threshold": 0.8}, "whoosh": {"threshold": 0.8}, "wild_animals": {"threshold": 0.8}, "wind": {"threshold": 0.8}, "wind_chime": {"threshold": 0.8}, "wind_instrument": {"threshold": 0.8}, "wind_noise": {"threshold": 0.8}, "wood": {"threshold": 0.8}, "wood_block": {"threshold": 0.8}, "writing": {"threshold": 0.8}, "yell": {"threshold": 0.8}, "yip": {"threshold": 0.8}, "zing": {"threshold": 0.8}, "zipper": {"threshold": 0.8}, "zither": {"threshold": 0.8}}, "enabled_in_config": false, "num_threads": 2}, "audio_transcription": {"enabled": false, "enabled_in_config": false, "live_enabled": false}, "birdseye": {"enabled": true, "mode": "objects", "order": 0}, "detect": {"enabled": false, "height": 720, "width": 1280, "fps": 5, "min_initialized": 2, "max_disappeared": 25, "stationary": {"interval": 50, "threshold": 50, "max_frames": {"default": null, "objects": {}}, "classifier": true}, "annotation_offset": 0}, "face_recognition": {"enabled": false, "min_area": 750}, "ffmpeg": {"path": "default", "global_args": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "hwaccel_args": "preset-vaapi", "input_args": "preset-rtsp-generic", "output_args": {"detect": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "record": "preset-record-generic-audio-aac"}, "retry_interval": 10.0, "apple_compatibility": false, "gpu": 0, "inputs": [{"path": "rtsp://10.0.0.1:554/video", "roles": ["record", "detect"], "global_args": [], "hwaccel_args": [], "input_args": []}]}, "live": {"streams": {"front_door": "front_door"}, "height": 720, "quality": 8}, "lpr": {"enabled": false, "expire_time": 3, "min_area": 1000, "enhancement": 0}, "motion": {"enabled": true, "threshold": 30, "lightning_threshold": 0.8, "skip_motion_threshold": null, "improve_contrast": true, "contour_area": 10, "delta_alpha": 0.2, "frame_alpha": 0.01, "frame_height": 100, "mask": {}, "mqtt_off_delay": 30, "enabled_in_config": null}, "objects": {"track": ["person"], "filters": {"person": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.5, "mask": {}}}, "mask": {}, "genai": {"enabled": false, "use_snapshot": false, "prompt": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "object_prompts": {}, "objects": [], "required_zones": [], "debug_save_thumbnails": false, "send_triggers": {"tracked_object_end": true, "after_significant_updates": null}, "enabled_in_config": false}}, "record": {"enabled": false, "expire_interval": 60, "continuous": {"days": 0}, "motion": {"days": 0}, "detections": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "alerts": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "export": {"hwaccel_args": "preset-vaapi"}, "preview": {"quality": "medium"}, "enabled_in_config": false}, "review": {"alerts": {"enabled": true, "labels": ["person", "car"], "required_zones": [], "enabled_in_config": true, "cutoff_time": 40}, "detections": {"enabled": true, "labels": null, "required_zones": [], "cutoff_time": 30, "enabled_in_config": true}, "genai": {"enabled": false, "alerts": true, "detections": false, "image_source": "preview", "additional_concerns": [], "debug_save_thumbnails": false, "enabled_in_config": false, "preferred_language": null, "activity_context_prompt": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is."}}, "semantic_search": {"triggers": {}}, "snapshots": {"enabled": false, "timestamp": false, "bounding_box": true, "crop": false, "required_zones": [], "height": null, "retain": {"default": 10, "mode": "motion", "objects": {}}, "quality": 60}, "timestamp_style": {"position": "tl", "format": "%m/%d/%Y %H:%M:%S", "color": {"red": 255, "green": 255, "blue": 255}, "thickness": 2, "effect": null}, "best_image_timeout": 60, "mqtt": {"enabled": true, "timestamp": true, "bounding_box": true, "crop": true, "height": 270, "required_zones": [], "quality": 70}, "notifications": {"enabled": false, "email": null, "cooldown": 0, "enabled_in_config": false}, "onvif": {"host": "", "port": 8000, "user": null, "password": null, "tls_insecure": false, "profile": null, "autotracking": {"enabled": false, "calibrate_on_startup": false, "zooming": "disabled", "zoom_factor": 0.3, "track": ["person"], "required_zones": [], "return_preset": "home", "timeout": 10, "movement_weights": [], "enabled_in_config": false}, "ignore_time_mismatch": false}, "type": "generic", "ui": {"order": 0, "dashboard": true}, "webui_url": null, "profiles": {}, "zones": {}, "enabled_in_config": true}, "backyard": {"name": "backyard", "friendly_name": null, "enabled": true, "audio": {"enabled": false, "max_not_heard": 30, "min_volume": 500, "listen": ["bark", "fire_alarm", "scream", "speech", "yell"], "filters": {"a_capella": {"threshold": 0.8}, "accelerating": {"threshold": 0.8}, "accordion": {"threshold": 0.8}, "acoustic_guitar": {"threshold": 0.8}, "afrobeat": {"threshold": 0.8}, "air_brake": {"threshold": 0.8}, "air_conditioning": {"threshold": 0.8}, "air_horn": {"threshold": 0.8}, "aircraft": {"threshold": 0.8}, "aircraft_engine": {"threshold": 0.8}, "alarm": {"threshold": 0.8}, "alarm_clock": {"threshold": 0.8}, "ambient_music": {"threshold": 0.8}, "ambulance": {"threshold": 0.8}, "angry_music": {"threshold": 0.8}, "animal": {"threshold": 0.8}, "applause": {"threshold": 0.8}, "arrow": {"threshold": 0.8}, "artillery_fire": {"threshold": 0.8}, "babbling": {"threshold": 0.8}, "background_music": {"threshold": 0.8}, "bagpipes": {"threshold": 0.8}, "bang": {"threshold": 0.8}, "banjo": {"threshold": 0.8}, "bark": {"threshold": 0.8}, "basketball_bounce": {"threshold": 0.8}, "bass_drum": {"threshold": 0.8}, "bass_guitar": {"threshold": 0.8}, "bathtub": {"threshold": 0.8}, "beatboxing": {"threshold": 0.8}, "beep": {"threshold": 0.8}, "bell": {"threshold": 0.8}, "bellow": {"threshold": 0.8}, "bicycle": {"threshold": 0.8}, "bicycle_bell": {"threshold": 0.8}, "bird": {"threshold": 0.8}, "biting": {"threshold": 0.8}, "bleat": {"threshold": 0.8}, "blender": {"threshold": 0.8}, "bluegrass": {"threshold": 0.8}, "blues": {"threshold": 0.8}, "boat": {"threshold": 0.8}, "boiling": {"threshold": 0.8}, "boing": {"threshold": 0.8}, "boom": {"threshold": 0.8}, "bouncing": {"threshold": 0.8}, "bow-wow": {"threshold": 0.8}, "bowed_string_instrument": {"threshold": 0.8}, "brass_instrument": {"threshold": 0.8}, "breaking": {"threshold": 0.8}, "breathing": {"threshold": 0.8}, "burping": {"threshold": 0.8}, "burst": {"threshold": 0.8}, "bus": {"threshold": 0.8}, "busy_signal": {"threshold": 0.8}, "buzz": {"threshold": 0.8}, "buzzer": {"threshold": 0.8}, "cacophony": {"threshold": 0.8}, "camera": {"threshold": 0.8}, "cap_gun": {"threshold": 0.8}, "car": {"threshold": 0.8}, "car_alarm": {"threshold": 0.8}, "car_passing_by": {"threshold": 0.8}, "carnatic_music": {"threshold": 0.8}, "cash_register": {"threshold": 0.8}, "cat": {"threshold": 0.8}, "caterwaul": {"threshold": 0.8}, "cattle": {"threshold": 0.8}, "caw": {"threshold": 0.8}, "cello": {"threshold": 0.8}, "chainsaw": {"threshold": 0.8}, "change_ringing": {"threshold": 0.8}, "chant": {"threshold": 0.8}, "chatter": {"threshold": 0.8}, "cheering": {"threshold": 0.8}, "chewing": {"threshold": 0.8}, "chicken": {"threshold": 0.8}, "child_singing": {"threshold": 0.8}, "children_playing": {"threshold": 0.8}, "chime": {"threshold": 0.8}, "chink": {"threshold": 0.8}, "chird": {"threshold": 0.8}, "chirp": {"threshold": 0.8}, "chirp_tone": {"threshold": 0.8}, "choir": {"threshold": 0.8}, "chop": {"threshold": 0.8}, "chopping": {"threshold": 0.8}, "chorus_effect": {"threshold": 0.8}, "christian_music": {"threshold": 0.8}, "christmas_music": {"threshold": 0.8}, "church_bell": {"threshold": 0.8}, "civil_defense_siren": {"threshold": 0.8}, "clang": {"threshold": 0.8}, "clapping": {"threshold": 0.8}, "clarinet": {"threshold": 0.8}, "classical_music": {"threshold": 0.8}, "clatter": {"threshold": 0.8}, "clickety-clack": {"threshold": 0.8}, "clicking": {"threshold": 0.8}, "clip-clop": {"threshold": 0.8}, "clock": {"threshold": 0.8}, "cluck": {"threshold": 0.8}, "cock-a-doodle-doo": {"threshold": 0.8}, "coin": {"threshold": 0.8}, "computer_keyboard": {"threshold": 0.8}, "coo": {"threshold": 0.8}, "cough": {"threshold": 0.8}, "country": {"threshold": 0.8}, "cowbell": {"threshold": 0.8}, "crack": {"threshold": 0.8}, "crackle": {"threshold": 0.8}, "creak": {"threshold": 0.8}, "cricket": {"threshold": 0.8}, "croak": {"threshold": 0.8}, "crow": {"threshold": 0.8}, "crowd": {"threshold": 0.8}, "crumpling": {"threshold": 0.8}, "crunch": {"threshold": 0.8}, "crushing": {"threshold": 0.8}, "crying": {"threshold": 0.8}, "cupboard_open_or_close": {"threshold": 0.8}, "cutlery": {"threshold": 0.8}, "cymbal": {"threshold": 0.8}, "dance_music": {"threshold": 0.8}, "dental_drill's_drill": {"threshold": 0.8}, "dial_tone": {"threshold": 0.8}, "didgeridoo": {"threshold": 0.8}, "ding": {"threshold": 0.8}, "ding-dong": {"threshold": 0.8}, "disco": {"threshold": 0.8}, "dishes": {"threshold": 0.8}, "distortion": {"threshold": 0.8}, "dog": {"threshold": 0.8}, "dogs": {"threshold": 0.8}, "door": {"threshold": 0.8}, "doorbell": {"threshold": 0.8}, "double_bass": {"threshold": 0.8}, "drawer_open_or_close": {"threshold": 0.8}, "drill": {"threshold": 0.8}, "drip": {"threshold": 0.8}, "drum": {"threshold": 0.8}, "drum_and_bass": {"threshold": 0.8}, "drum_kit": {"threshold": 0.8}, "drum_machine": {"threshold": 0.8}, "drum_roll": {"threshold": 0.8}, "dubstep": {"threshold": 0.8}, "duck": {"threshold": 0.8}, "echo": {"threshold": 0.8}, "effects_unit": {"threshold": 0.8}, "electric_guitar": {"threshold": 0.8}, "electric_piano": {"threshold": 0.8}, "electric_shaver": {"threshold": 0.8}, "electric_toothbrush": {"threshold": 0.8}, "electronic_dance_music": {"threshold": 0.8}, "electronic_music": {"threshold": 0.8}, "electronic_organ": {"threshold": 0.8}, "electronic_tuner": {"threshold": 0.8}, "electronica": {"threshold": 0.8}, "emergency_vehicle": {"threshold": 0.8}, "engine": {"threshold": 0.8}, "engine_knocking": {"threshold": 0.8}, "engine_starting": {"threshold": 0.8}, "environmental_noise": {"threshold": 0.8}, "eruption": {"threshold": 0.8}, "exciting_music": {"threshold": 0.8}, "explosion": {"threshold": 0.8}, "fart": {"threshold": 0.8}, "field_recording": {"threshold": 0.8}, "filing": {"threshold": 0.8}, "fill": {"threshold": 0.8}, "finger_snapping": {"threshold": 0.8}, "fire": {"threshold": 0.8}, "fire_alarm": {"threshold": 0.8}, "fire_engine": {"threshold": 0.8}, "firecracker": {"threshold": 0.8}, "fireworks": {"threshold": 0.8}, "fixed-wing_aircraft": {"threshold": 0.8}, "flamenco": {"threshold": 0.8}, "flap": {"threshold": 0.8}, "flapping_wings": {"threshold": 0.8}, "flute": {"threshold": 0.8}, "fly": {"threshold": 0.8}, "foghorn": {"threshold": 0.8}, "folk_music": {"threshold": 0.8}, "footsteps": {"threshold": 0.8}, "fowl": {"threshold": 0.8}, "french_horn": {"threshold": 0.8}, "frog": {"threshold": 0.8}, "frying": {"threshold": 0.8}, "funk": {"threshold": 0.8}, "fusillade": {"threshold": 0.8}, "gargling": {"threshold": 0.8}, "gasp": {"threshold": 0.8}, "gears": {"threshold": 0.8}, "glass": {"threshold": 0.8}, "glockenspiel": {"threshold": 0.8}, "goat": {"threshold": 0.8}, "gobble": {"threshold": 0.8}, "gong": {"threshold": 0.8}, "goose": {"threshold": 0.8}, "gospel_music": {"threshold": 0.8}, "groan": {"threshold": 0.8}, "growling": {"threshold": 0.8}, "grunge": {"threshold": 0.8}, "grunt": {"threshold": 0.8}, "guitar": {"threshold": 0.8}, "gunshot": {"threshold": 0.8}, "gurgling": {"threshold": 0.8}, "gush": {"threshold": 0.8}, "hair_dryer": {"threshold": 0.8}, "hammer": {"threshold": 0.8}, "hammond_organ": {"threshold": 0.8}, "hands": {"threshold": 0.8}, "happy_music": {"threshold": 0.8}, "harmonic": {"threshold": 0.8}, "harmonica": {"threshold": 0.8}, "harp": {"threshold": 0.8}, "harpsichord": {"threshold": 0.8}, "heart_murmur": {"threshold": 0.8}, "heartbeat": {"threshold": 0.8}, "heavy_engine": {"threshold": 0.8}, "heavy_metal": {"threshold": 0.8}, "helicopter": {"threshold": 0.8}, "hi-hat": {"threshold": 0.8}, "hiccup": {"threshold": 0.8}, "hip_hop_music": {"threshold": 0.8}, "hiss": {"threshold": 0.8}, "honk": {"threshold": 0.8}, "hoot": {"threshold": 0.8}, "horse": {"threshold": 0.8}, "house_music": {"threshold": 0.8}, "howl": {"threshold": 0.8}, "hum": {"threshold": 0.8}, "humming": {"threshold": 0.8}, "ice_cream_truck": {"threshold": 0.8}, "idling": {"threshold": 0.8}, "independent_music": {"threshold": 0.8}, "insect": {"threshold": 0.8}, "inside": {"threshold": 0.8}, "jackhammer": {"threshold": 0.8}, "jazz": {"threshold": 0.8}, "jet_engine": {"threshold": 0.8}, "jingle": {"threshold": 0.8}, "jingle_bell": {"threshold": 0.8}, "keyboard": {"threshold": 0.8}, "keys_jangling": {"threshold": 0.8}, "knock": {"threshold": 0.8}, "laughter": {"threshold": 0.8}, "lawn_mower": {"threshold": 0.8}, "light_engine": {"threshold": 0.8}, "liquid": {"threshold": 0.8}, "livestock": {"threshold": 0.8}, "lullaby": {"threshold": 0.8}, "machine_gun": {"threshold": 0.8}, "mains_hum": {"threshold": 0.8}, "mallet_percussion": {"threshold": 0.8}, "mandolin": {"threshold": 0.8}, "mantra": {"threshold": 0.8}, "maraca": {"threshold": 0.8}, "marimba": {"threshold": 0.8}, "mechanical_fan": {"threshold": 0.8}, "mechanisms": {"threshold": 0.8}, "medium_engine": {"threshold": 0.8}, "meow": {"threshold": 0.8}, "microwave_oven": {"threshold": 0.8}, "middle_eastern_music": {"threshold": 0.8}, "moo": {"threshold": 0.8}, "mosquito": {"threshold": 0.8}, "motor_vehicle": {"threshold": 0.8}, "motorboat": {"threshold": 0.8}, "motorcycle": {"threshold": 0.8}, "mouse": {"threshold": 0.8}, "music": {"threshold": 0.8}, "music_for_children": {"threshold": 0.8}, "music_of_africa": {"threshold": 0.8}, "music_of_asia": {"threshold": 0.8}, "music_of_bollywood": {"threshold": 0.8}, "music_of_latin_america": {"threshold": 0.8}, "musical_instrument": {"threshold": 0.8}, "neigh": {"threshold": 0.8}, "new-age_music": {"threshold": 0.8}, "noise": {"threshold": 0.8}, "ocean": {"threshold": 0.8}, "oink": {"threshold": 0.8}, "opera": {"threshold": 0.8}, "orchestra": {"threshold": 0.8}, "organ": {"threshold": 0.8}, "outside": {"threshold": 0.8}, "owl": {"threshold": 0.8}, "pant": {"threshold": 0.8}, "patter": {"threshold": 0.8}, "percussion": {"threshold": 0.8}, "pets": {"threshold": 0.8}, "piano": {"threshold": 0.8}, "pig": {"threshold": 0.8}, "pigeon": {"threshold": 0.8}, "ping": {"threshold": 0.8}, "pink_noise": {"threshold": 0.8}, "pizzicato": {"threshold": 0.8}, "plop": {"threshold": 0.8}, "plucked_string_instrument": {"threshold": 0.8}, "police_car": {"threshold": 0.8}, "pop_music": {"threshold": 0.8}, "pour": {"threshold": 0.8}, "power_tool": {"threshold": 0.8}, "power_windows": {"threshold": 0.8}, "printer": {"threshold": 0.8}, "progressive_rock": {"threshold": 0.8}, "propeller": {"threshold": 0.8}, "psychedelic_rock": {"threshold": 0.8}, "pulleys": {"threshold": 0.8}, "pulse": {"threshold": 0.8}, "pump": {"threshold": 0.8}, "punk_rock": {"threshold": 0.8}, "purr": {"threshold": 0.8}, "quack": {"threshold": 0.8}, "race_car": {"threshold": 0.8}, "radio": {"threshold": 0.8}, "rail_transport": {"threshold": 0.8}, "railroad_car": {"threshold": 0.8}, "rain": {"threshold": 0.8}, "rain_on_surface": {"threshold": 0.8}, "raindrop": {"threshold": 0.8}, "rapping": {"threshold": 0.8}, "ratchet": {"threshold": 0.8}, "rats": {"threshold": 0.8}, "rattle": {"threshold": 0.8}, "reggae": {"threshold": 0.8}, "reverberation": {"threshold": 0.8}, "reversing_beeps": {"threshold": 0.8}, "rhythm_and_blues": {"threshold": 0.8}, "rimshot": {"threshold": 0.8}, "ringtone": {"threshold": 0.8}, "roar": {"threshold": 0.8}, "roaring_cats": {"threshold": 0.8}, "rock_and_roll": {"threshold": 0.8}, "rock_music": {"threshold": 0.8}, "roll": {"threshold": 0.8}, "rowboat": {"threshold": 0.8}, "rub": {"threshold": 0.8}, "rumble": {"threshold": 0.8}, "run": {"threshold": 0.8}, "rustle": {"threshold": 0.8}, "rustling_leaves": {"threshold": 0.8}, "sad_music": {"threshold": 0.8}, "sailboat": {"threshold": 0.8}, "salsa_music": {"threshold": 0.8}, "sampler": {"threshold": 0.8}, "sanding": {"threshold": 0.8}, "sawing": {"threshold": 0.8}, "saxophone": {"threshold": 0.8}, "scary_music": {"threshold": 0.8}, "scissors": {"threshold": 0.8}, "scrape": {"threshold": 0.8}, "scratch": {"threshold": 0.8}, "scratching": {"threshold": 0.8}, "sewing_machine": {"threshold": 0.8}, "shatter": {"threshold": 0.8}, "sheep": {"threshold": 0.8}, "ship": {"threshold": 0.8}, "shofar": {"threshold": 0.8}, "shuffle": {"threshold": 0.8}, "shuffling_cards": {"threshold": 0.8}, "sidetone": {"threshold": 0.8}, "sigh": {"threshold": 0.8}, "silence": {"threshold": 0.8}, "sine_wave": {"threshold": 0.8}, "singing": {"threshold": 0.8}, "singing_bowl": {"threshold": 0.8}, "single-lens_reflex_camera": {"threshold": 0.8}, "sink": {"threshold": 0.8}, "siren": {"threshold": 0.8}, "sitar": {"threshold": 0.8}, "sizzle": {"threshold": 0.8}, "ska": {"threshold": 0.8}, "skateboard": {"threshold": 0.8}, "skidding": {"threshold": 0.8}, "slam": {"threshold": 0.8}, "slap": {"threshold": 0.8}, "sliding_door": {"threshold": 0.8}, "slosh": {"threshold": 0.8}, "smash": {"threshold": 0.8}, "smoke_detector": {"threshold": 0.8}, "snake": {"threshold": 0.8}, "snare_drum": {"threshold": 0.8}, "sneeze": {"threshold": 0.8}, "snicker": {"threshold": 0.8}, "sniff": {"threshold": 0.8}, "snoring": {"threshold": 0.8}, "snort": {"threshold": 0.8}, "sodeling": {"threshold": 0.8}, "sonar": {"threshold": 0.8}, "song": {"threshold": 0.8}, "soul_music": {"threshold": 0.8}, "sound_effect": {"threshold": 0.8}, "soundtrack_music": {"threshold": 0.8}, "speech": {"threshold": 0.8}, "splash": {"threshold": 0.8}, "splinter": {"threshold": 0.8}, "spray": {"threshold": 0.8}, "squawk": {"threshold": 0.8}, "squeak": {"threshold": 0.8}, "squeal": {"threshold": 0.8}, "squish": {"threshold": 0.8}, "static": {"threshold": 0.8}, "steam": {"threshold": 0.8}, "steam_whistle": {"threshold": 0.8}, "steel_guitar": {"threshold": 0.8}, "steelpan": {"threshold": 0.8}, "stir": {"threshold": 0.8}, "stomach_rumble": {"threshold": 0.8}, "stream": {"threshold": 0.8}, "string_section": {"threshold": 0.8}, "strum": {"threshold": 0.8}, "subway": {"threshold": 0.8}, "swing_music": {"threshold": 0.8}, "synthesizer": {"threshold": 0.8}, "synthetic_singing": {"threshold": 0.8}, "tabla": {"threshold": 0.8}, "tambourine": {"threshold": 0.8}, "tap": {"threshold": 0.8}, "tapping": {"threshold": 0.8}, "tearing": {"threshold": 0.8}, "techno": {"threshold": 0.8}, "telephone": {"threshold": 0.8}, "telephone_bell_ringing": {"threshold": 0.8}, "telephone_dialing": {"threshold": 0.8}, "television": {"threshold": 0.8}, "tender_music": {"threshold": 0.8}, "theme_music": {"threshold": 0.8}, "theremin": {"threshold": 0.8}, "throat_clearing": {"threshold": 0.8}, "throbbing": {"threshold": 0.8}, "thump": {"threshold": 0.8}, "thunder": {"threshold": 0.8}, "thunderstorm": {"threshold": 0.8}, "thunk": {"threshold": 0.8}, "tick": {"threshold": 0.8}, "tick-tock": {"threshold": 0.8}, "timpani": {"threshold": 0.8}, "tire_squeal": {"threshold": 0.8}, "toilet_flush": {"threshold": 0.8}, "tools": {"threshold": 0.8}, "toot": {"threshold": 0.8}, "toothbrush": {"threshold": 0.8}, "traditional_music": {"threshold": 0.8}, "traffic_noise": {"threshold": 0.8}, "train": {"threshold": 0.8}, "train_horn": {"threshold": 0.8}, "train_wheels_squealing": {"threshold": 0.8}, "train_whistle": {"threshold": 0.8}, "trance_music": {"threshold": 0.8}, "trickle": {"threshold": 0.8}, "trombone": {"threshold": 0.8}, "truck": {"threshold": 0.8}, "trumpet": {"threshold": 0.8}, "tubular_bells": {"threshold": 0.8}, "tuning_fork": {"threshold": 0.8}, "turkey": {"threshold": 0.8}, "typewriter": {"threshold": 0.8}, "typing": {"threshold": 0.8}, "ukulele": {"threshold": 0.8}, "vacuum_cleaner": {"threshold": 0.8}, "vehicle": {"threshold": 0.8}, "vibraphone": {"threshold": 0.8}, "vibration": {"threshold": 0.8}, "video_game_music": {"threshold": 0.8}, "violin": {"threshold": 0.8}, "vocal_music": {"threshold": 0.8}, "water": {"threshold": 0.8}, "water_tap": {"threshold": 0.8}, "waterfall": {"threshold": 0.8}, "waves": {"threshold": 0.8}, "wedding_music": {"threshold": 0.8}, "whack": {"threshold": 0.8}, "whale_vocalization": {"threshold": 0.8}, "wheeze": {"threshold": 0.8}, "whimper_dog": {"threshold": 0.8}, "whip": {"threshold": 0.8}, "whir": {"threshold": 0.8}, "whispering": {"threshold": 0.8}, "whistle": {"threshold": 0.8}, "whistling": {"threshold": 0.8}, "white_noise": {"threshold": 0.8}, "whoop": {"threshold": 0.8}, "whoosh": {"threshold": 0.8}, "wild_animals": {"threshold": 0.8}, "wind": {"threshold": 0.8}, "wind_chime": {"threshold": 0.8}, "wind_instrument": {"threshold": 0.8}, "wind_noise": {"threshold": 0.8}, "wood": {"threshold": 0.8}, "wood_block": {"threshold": 0.8}, "writing": {"threshold": 0.8}, "yell": {"threshold": 0.8}, "yip": {"threshold": 0.8}, "zing": {"threshold": 0.8}, "zipper": {"threshold": 0.8}, "zither": {"threshold": 0.8}}, "enabled_in_config": false, "num_threads": 2}, "audio_transcription": {"enabled": false, "enabled_in_config": false, "live_enabled": false}, "birdseye": {"enabled": true, "mode": "objects", "order": 0}, "detect": {"enabled": false, "height": 720, "width": 1280, "fps": 5, "min_initialized": 2, "max_disappeared": 25, "stationary": {"interval": 50, "threshold": 50, "max_frames": {"default": null, "objects": {}}, "classifier": true}, "annotation_offset": 0}, "face_recognition": {"enabled": false, "min_area": 750}, "ffmpeg": {"path": "default", "global_args": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "hwaccel_args": "preset-vaapi", "input_args": "preset-rtsp-generic", "output_args": {"detect": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "record": "preset-record-generic-audio-aac"}, "retry_interval": 10.0, "apple_compatibility": false, "gpu": 0, "inputs": [{"path": "rtsp://10.0.0.2:554/video", "roles": ["record", "detect"], "global_args": [], "hwaccel_args": [], "input_args": []}]}, "live": {"streams": {"backyard": "backyard"}, "height": 720, "quality": 8}, "lpr": {"enabled": false, "expire_time": 3, "min_area": 1000, "enhancement": 0}, "motion": {"enabled": true, "threshold": 30, "lightning_threshold": 0.8, "skip_motion_threshold": null, "improve_contrast": true, "contour_area": 10, "delta_alpha": 0.2, "frame_alpha": 0.01, "frame_height": 100, "mask": {}, "mqtt_off_delay": 30, "enabled_in_config": null}, "objects": {"track": ["person"], "filters": {"person": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.5, "mask": {}}}, "mask": {}, "genai": {"enabled": false, "use_snapshot": false, "prompt": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "object_prompts": {}, "objects": [], "required_zones": [], "debug_save_thumbnails": false, "send_triggers": {"tracked_object_end": true, "after_significant_updates": null}, "enabled_in_config": false}}, "record": {"enabled": false, "expire_interval": 60, "continuous": {"days": 0}, "motion": {"days": 0}, "detections": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "alerts": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "export": {"hwaccel_args": "preset-vaapi"}, "preview": {"quality": "medium"}, "enabled_in_config": false}, "review": {"alerts": {"enabled": true, "labels": ["person", "car"], "required_zones": [], "enabled_in_config": true, "cutoff_time": 40}, "detections": {"enabled": true, "labels": null, "required_zones": [], "cutoff_time": 30, "enabled_in_config": true}, "genai": {"enabled": false, "alerts": true, "detections": false, "image_source": "preview", "additional_concerns": [], "debug_save_thumbnails": false, "enabled_in_config": false, "preferred_language": null, "activity_context_prompt": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is."}}, "semantic_search": {"triggers": {}}, "snapshots": {"enabled": false, "timestamp": false, "bounding_box": true, "crop": false, "required_zones": [], "height": null, "retain": {"default": 10, "mode": "motion", "objects": {}}, "quality": 60}, "timestamp_style": {"position": "tl", "format": "%m/%d/%Y %H:%M:%S", "color": {"red": 255, "green": 255, "blue": 255}, "thickness": 2, "effect": null}, "best_image_timeout": 60, "mqtt": {"enabled": true, "timestamp": true, "bounding_box": true, "crop": true, "height": 270, "required_zones": [], "quality": 70}, "notifications": {"enabled": false, "email": null, "cooldown": 0, "enabled_in_config": false}, "onvif": {"host": "", "port": 8000, "user": null, "password": null, "tls_insecure": false, "profile": null, "autotracking": {"enabled": false, "calibrate_on_startup": false, "zooming": "disabled", "zoom_factor": 0.3, "track": ["person"], "required_zones": [], "return_preset": "home", "timeout": 10, "movement_weights": [], "enabled_in_config": false}, "ignore_time_mismatch": false}, "type": "generic", "ui": {"order": 0, "dashboard": true}, "webui_url": null, "profiles": {}, "zones": {}, "enabled_in_config": true}, "garage": {"name": "garage", "friendly_name": null, "enabled": true, "audio": {"enabled": false, "max_not_heard": 30, "min_volume": 500, "listen": ["bark", "fire_alarm", "scream", "speech", "yell"], "filters": {"a_capella": {"threshold": 0.8}, "accelerating": {"threshold": 0.8}, "accordion": {"threshold": 0.8}, "acoustic_guitar": {"threshold": 0.8}, "afrobeat": {"threshold": 0.8}, "air_brake": {"threshold": 0.8}, "air_conditioning": {"threshold": 0.8}, "air_horn": {"threshold": 0.8}, "aircraft": {"threshold": 0.8}, "aircraft_engine": {"threshold": 0.8}, "alarm": {"threshold": 0.8}, "alarm_clock": {"threshold": 0.8}, "ambient_music": {"threshold": 0.8}, "ambulance": {"threshold": 0.8}, "angry_music": {"threshold": 0.8}, "animal": {"threshold": 0.8}, "applause": {"threshold": 0.8}, "arrow": {"threshold": 0.8}, "artillery_fire": {"threshold": 0.8}, "babbling": {"threshold": 0.8}, "background_music": {"threshold": 0.8}, "bagpipes": {"threshold": 0.8}, "bang": {"threshold": 0.8}, "banjo": {"threshold": 0.8}, "bark": {"threshold": 0.8}, "basketball_bounce": {"threshold": 0.8}, "bass_drum": {"threshold": 0.8}, "bass_guitar": {"threshold": 0.8}, "bathtub": {"threshold": 0.8}, "beatboxing": {"threshold": 0.8}, "beep": {"threshold": 0.8}, "bell": {"threshold": 0.8}, "bellow": {"threshold": 0.8}, "bicycle": {"threshold": 0.8}, "bicycle_bell": {"threshold": 0.8}, "bird": {"threshold": 0.8}, "biting": {"threshold": 0.8}, "bleat": {"threshold": 0.8}, "blender": {"threshold": 0.8}, "bluegrass": {"threshold": 0.8}, "blues": {"threshold": 0.8}, "boat": {"threshold": 0.8}, "boiling": {"threshold": 0.8}, "boing": {"threshold": 0.8}, "boom": {"threshold": 0.8}, "bouncing": {"threshold": 0.8}, "bow-wow": {"threshold": 0.8}, "bowed_string_instrument": {"threshold": 0.8}, "brass_instrument": {"threshold": 0.8}, "breaking": {"threshold": 0.8}, "breathing": {"threshold": 0.8}, "burping": {"threshold": 0.8}, "burst": {"threshold": 0.8}, "bus": {"threshold": 0.8}, "busy_signal": {"threshold": 0.8}, "buzz": {"threshold": 0.8}, "buzzer": {"threshold": 0.8}, "cacophony": {"threshold": 0.8}, "camera": {"threshold": 0.8}, "cap_gun": {"threshold": 0.8}, "car": {"threshold": 0.8}, "car_alarm": {"threshold": 0.8}, "car_passing_by": {"threshold": 0.8}, "carnatic_music": {"threshold": 0.8}, "cash_register": {"threshold": 0.8}, "cat": {"threshold": 0.8}, "caterwaul": {"threshold": 0.8}, "cattle": {"threshold": 0.8}, "caw": {"threshold": 0.8}, "cello": {"threshold": 0.8}, "chainsaw": {"threshold": 0.8}, "change_ringing": {"threshold": 0.8}, "chant": {"threshold": 0.8}, "chatter": {"threshold": 0.8}, "cheering": {"threshold": 0.8}, "chewing": {"threshold": 0.8}, "chicken": {"threshold": 0.8}, "child_singing": {"threshold": 0.8}, "children_playing": {"threshold": 0.8}, "chime": {"threshold": 0.8}, "chink": {"threshold": 0.8}, "chird": {"threshold": 0.8}, "chirp": {"threshold": 0.8}, "chirp_tone": {"threshold": 0.8}, "choir": {"threshold": 0.8}, "chop": {"threshold": 0.8}, "chopping": {"threshold": 0.8}, "chorus_effect": {"threshold": 0.8}, "christian_music": {"threshold": 0.8}, "christmas_music": {"threshold": 0.8}, "church_bell": {"threshold": 0.8}, "civil_defense_siren": {"threshold": 0.8}, "clang": {"threshold": 0.8}, "clapping": {"threshold": 0.8}, "clarinet": {"threshold": 0.8}, "classical_music": {"threshold": 0.8}, "clatter": {"threshold": 0.8}, "clickety-clack": {"threshold": 0.8}, "clicking": {"threshold": 0.8}, "clip-clop": {"threshold": 0.8}, "clock": {"threshold": 0.8}, "cluck": {"threshold": 0.8}, "cock-a-doodle-doo": {"threshold": 0.8}, "coin": {"threshold": 0.8}, "computer_keyboard": {"threshold": 0.8}, "coo": {"threshold": 0.8}, "cough": {"threshold": 0.8}, "country": {"threshold": 0.8}, "cowbell": {"threshold": 0.8}, "crack": {"threshold": 0.8}, "crackle": {"threshold": 0.8}, "creak": {"threshold": 0.8}, "cricket": {"threshold": 0.8}, "croak": {"threshold": 0.8}, "crow": {"threshold": 0.8}, "crowd": {"threshold": 0.8}, "crumpling": {"threshold": 0.8}, "crunch": {"threshold": 0.8}, "crushing": {"threshold": 0.8}, "crying": {"threshold": 0.8}, "cupboard_open_or_close": {"threshold": 0.8}, "cutlery": {"threshold": 0.8}, "cymbal": {"threshold": 0.8}, "dance_music": {"threshold": 0.8}, "dental_drill's_drill": {"threshold": 0.8}, "dial_tone": {"threshold": 0.8}, "didgeridoo": {"threshold": 0.8}, "ding": {"threshold": 0.8}, "ding-dong": {"threshold": 0.8}, "disco": {"threshold": 0.8}, "dishes": {"threshold": 0.8}, "distortion": {"threshold": 0.8}, "dog": {"threshold": 0.8}, "dogs": {"threshold": 0.8}, "door": {"threshold": 0.8}, "doorbell": {"threshold": 0.8}, "double_bass": {"threshold": 0.8}, "drawer_open_or_close": {"threshold": 0.8}, "drill": {"threshold": 0.8}, "drip": {"threshold": 0.8}, "drum": {"threshold": 0.8}, "drum_and_bass": {"threshold": 0.8}, "drum_kit": {"threshold": 0.8}, "drum_machine": {"threshold": 0.8}, "drum_roll": {"threshold": 0.8}, "dubstep": {"threshold": 0.8}, "duck": {"threshold": 0.8}, "echo": {"threshold": 0.8}, "effects_unit": {"threshold": 0.8}, "electric_guitar": {"threshold": 0.8}, "electric_piano": {"threshold": 0.8}, "electric_shaver": {"threshold": 0.8}, "electric_toothbrush": {"threshold": 0.8}, "electronic_dance_music": {"threshold": 0.8}, "electronic_music": {"threshold": 0.8}, "electronic_organ": {"threshold": 0.8}, "electronic_tuner": {"threshold": 0.8}, "electronica": {"threshold": 0.8}, "emergency_vehicle": {"threshold": 0.8}, "engine": {"threshold": 0.8}, "engine_knocking": {"threshold": 0.8}, "engine_starting": {"threshold": 0.8}, "environmental_noise": {"threshold": 0.8}, "eruption": {"threshold": 0.8}, "exciting_music": {"threshold": 0.8}, "explosion": {"threshold": 0.8}, "fart": {"threshold": 0.8}, "field_recording": {"threshold": 0.8}, "filing": {"threshold": 0.8}, "fill": {"threshold": 0.8}, "finger_snapping": {"threshold": 0.8}, "fire": {"threshold": 0.8}, "fire_alarm": {"threshold": 0.8}, "fire_engine": {"threshold": 0.8}, "firecracker": {"threshold": 0.8}, "fireworks": {"threshold": 0.8}, "fixed-wing_aircraft": {"threshold": 0.8}, "flamenco": {"threshold": 0.8}, "flap": {"threshold": 0.8}, "flapping_wings": {"threshold": 0.8}, "flute": {"threshold": 0.8}, "fly": {"threshold": 0.8}, "foghorn": {"threshold": 0.8}, "folk_music": {"threshold": 0.8}, "footsteps": {"threshold": 0.8}, "fowl": {"threshold": 0.8}, "french_horn": {"threshold": 0.8}, "frog": {"threshold": 0.8}, "frying": {"threshold": 0.8}, "funk": {"threshold": 0.8}, "fusillade": {"threshold": 0.8}, "gargling": {"threshold": 0.8}, "gasp": {"threshold": 0.8}, "gears": {"threshold": 0.8}, "glass": {"threshold": 0.8}, "glockenspiel": {"threshold": 0.8}, "goat": {"threshold": 0.8}, "gobble": {"threshold": 0.8}, "gong": {"threshold": 0.8}, "goose": {"threshold": 0.8}, "gospel_music": {"threshold": 0.8}, "groan": {"threshold": 0.8}, "growling": {"threshold": 0.8}, "grunge": {"threshold": 0.8}, "grunt": {"threshold": 0.8}, "guitar": {"threshold": 0.8}, "gunshot": {"threshold": 0.8}, "gurgling": {"threshold": 0.8}, "gush": {"threshold": 0.8}, "hair_dryer": {"threshold": 0.8}, "hammer": {"threshold": 0.8}, "hammond_organ": {"threshold": 0.8}, "hands": {"threshold": 0.8}, "happy_music": {"threshold": 0.8}, "harmonic": {"threshold": 0.8}, "harmonica": {"threshold": 0.8}, "harp": {"threshold": 0.8}, "harpsichord": {"threshold": 0.8}, "heart_murmur": {"threshold": 0.8}, "heartbeat": {"threshold": 0.8}, "heavy_engine": {"threshold": 0.8}, "heavy_metal": {"threshold": 0.8}, "helicopter": {"threshold": 0.8}, "hi-hat": {"threshold": 0.8}, "hiccup": {"threshold": 0.8}, "hip_hop_music": {"threshold": 0.8}, "hiss": {"threshold": 0.8}, "honk": {"threshold": 0.8}, "hoot": {"threshold": 0.8}, "horse": {"threshold": 0.8}, "house_music": {"threshold": 0.8}, "howl": {"threshold": 0.8}, "hum": {"threshold": 0.8}, "humming": {"threshold": 0.8}, "ice_cream_truck": {"threshold": 0.8}, "idling": {"threshold": 0.8}, "independent_music": {"threshold": 0.8}, "insect": {"threshold": 0.8}, "inside": {"threshold": 0.8}, "jackhammer": {"threshold": 0.8}, "jazz": {"threshold": 0.8}, "jet_engine": {"threshold": 0.8}, "jingle": {"threshold": 0.8}, "jingle_bell": {"threshold": 0.8}, "keyboard": {"threshold": 0.8}, "keys_jangling": {"threshold": 0.8}, "knock": {"threshold": 0.8}, "laughter": {"threshold": 0.8}, "lawn_mower": {"threshold": 0.8}, "light_engine": {"threshold": 0.8}, "liquid": {"threshold": 0.8}, "livestock": {"threshold": 0.8}, "lullaby": {"threshold": 0.8}, "machine_gun": {"threshold": 0.8}, "mains_hum": {"threshold": 0.8}, "mallet_percussion": {"threshold": 0.8}, "mandolin": {"threshold": 0.8}, "mantra": {"threshold": 0.8}, "maraca": {"threshold": 0.8}, "marimba": {"threshold": 0.8}, "mechanical_fan": {"threshold": 0.8}, "mechanisms": {"threshold": 0.8}, "medium_engine": {"threshold": 0.8}, "meow": {"threshold": 0.8}, "microwave_oven": {"threshold": 0.8}, "middle_eastern_music": {"threshold": 0.8}, "moo": {"threshold": 0.8}, "mosquito": {"threshold": 0.8}, "motor_vehicle": {"threshold": 0.8}, "motorboat": {"threshold": 0.8}, "motorcycle": {"threshold": 0.8}, "mouse": {"threshold": 0.8}, "music": {"threshold": 0.8}, "music_for_children": {"threshold": 0.8}, "music_of_africa": {"threshold": 0.8}, "music_of_asia": {"threshold": 0.8}, "music_of_bollywood": {"threshold": 0.8}, "music_of_latin_america": {"threshold": 0.8}, "musical_instrument": {"threshold": 0.8}, "neigh": {"threshold": 0.8}, "new-age_music": {"threshold": 0.8}, "noise": {"threshold": 0.8}, "ocean": {"threshold": 0.8}, "oink": {"threshold": 0.8}, "opera": {"threshold": 0.8}, "orchestra": {"threshold": 0.8}, "organ": {"threshold": 0.8}, "outside": {"threshold": 0.8}, "owl": {"threshold": 0.8}, "pant": {"threshold": 0.8}, "patter": {"threshold": 0.8}, "percussion": {"threshold": 0.8}, "pets": {"threshold": 0.8}, "piano": {"threshold": 0.8}, "pig": {"threshold": 0.8}, "pigeon": {"threshold": 0.8}, "ping": {"threshold": 0.8}, "pink_noise": {"threshold": 0.8}, "pizzicato": {"threshold": 0.8}, "plop": {"threshold": 0.8}, "plucked_string_instrument": {"threshold": 0.8}, "police_car": {"threshold": 0.8}, "pop_music": {"threshold": 0.8}, "pour": {"threshold": 0.8}, "power_tool": {"threshold": 0.8}, "power_windows": {"threshold": 0.8}, "printer": {"threshold": 0.8}, "progressive_rock": {"threshold": 0.8}, "propeller": {"threshold": 0.8}, "psychedelic_rock": {"threshold": 0.8}, "pulleys": {"threshold": 0.8}, "pulse": {"threshold": 0.8}, "pump": {"threshold": 0.8}, "punk_rock": {"threshold": 0.8}, "purr": {"threshold": 0.8}, "quack": {"threshold": 0.8}, "race_car": {"threshold": 0.8}, "radio": {"threshold": 0.8}, "rail_transport": {"threshold": 0.8}, "railroad_car": {"threshold": 0.8}, "rain": {"threshold": 0.8}, "rain_on_surface": {"threshold": 0.8}, "raindrop": {"threshold": 0.8}, "rapping": {"threshold": 0.8}, "ratchet": {"threshold": 0.8}, "rats": {"threshold": 0.8}, "rattle": {"threshold": 0.8}, "reggae": {"threshold": 0.8}, "reverberation": {"threshold": 0.8}, "reversing_beeps": {"threshold": 0.8}, "rhythm_and_blues": {"threshold": 0.8}, "rimshot": {"threshold": 0.8}, "ringtone": {"threshold": 0.8}, "roar": {"threshold": 0.8}, "roaring_cats": {"threshold": 0.8}, "rock_and_roll": {"threshold": 0.8}, "rock_music": {"threshold": 0.8}, "roll": {"threshold": 0.8}, "rowboat": {"threshold": 0.8}, "rub": {"threshold": 0.8}, "rumble": {"threshold": 0.8}, "run": {"threshold": 0.8}, "rustle": {"threshold": 0.8}, "rustling_leaves": {"threshold": 0.8}, "sad_music": {"threshold": 0.8}, "sailboat": {"threshold": 0.8}, "salsa_music": {"threshold": 0.8}, "sampler": {"threshold": 0.8}, "sanding": {"threshold": 0.8}, "sawing": {"threshold": 0.8}, "saxophone": {"threshold": 0.8}, "scary_music": {"threshold": 0.8}, "scissors": {"threshold": 0.8}, "scrape": {"threshold": 0.8}, "scratch": {"threshold": 0.8}, "scratching": {"threshold": 0.8}, "sewing_machine": {"threshold": 0.8}, "shatter": {"threshold": 0.8}, "sheep": {"threshold": 0.8}, "ship": {"threshold": 0.8}, "shofar": {"threshold": 0.8}, "shuffle": {"threshold": 0.8}, "shuffling_cards": {"threshold": 0.8}, "sidetone": {"threshold": 0.8}, "sigh": {"threshold": 0.8}, "silence": {"threshold": 0.8}, "sine_wave": {"threshold": 0.8}, "singing": {"threshold": 0.8}, "singing_bowl": {"threshold": 0.8}, "single-lens_reflex_camera": {"threshold": 0.8}, "sink": {"threshold": 0.8}, "siren": {"threshold": 0.8}, "sitar": {"threshold": 0.8}, "sizzle": {"threshold": 0.8}, "ska": {"threshold": 0.8}, "skateboard": {"threshold": 0.8}, "skidding": {"threshold": 0.8}, "slam": {"threshold": 0.8}, "slap": {"threshold": 0.8}, "sliding_door": {"threshold": 0.8}, "slosh": {"threshold": 0.8}, "smash": {"threshold": 0.8}, "smoke_detector": {"threshold": 0.8}, "snake": {"threshold": 0.8}, "snare_drum": {"threshold": 0.8}, "sneeze": {"threshold": 0.8}, "snicker": {"threshold": 0.8}, "sniff": {"threshold": 0.8}, "snoring": {"threshold": 0.8}, "snort": {"threshold": 0.8}, "sodeling": {"threshold": 0.8}, "sonar": {"threshold": 0.8}, "song": {"threshold": 0.8}, "soul_music": {"threshold": 0.8}, "sound_effect": {"threshold": 0.8}, "soundtrack_music": {"threshold": 0.8}, "speech": {"threshold": 0.8}, "splash": {"threshold": 0.8}, "splinter": {"threshold": 0.8}, "spray": {"threshold": 0.8}, "squawk": {"threshold": 0.8}, "squeak": {"threshold": 0.8}, "squeal": {"threshold": 0.8}, "squish": {"threshold": 0.8}, "static": {"threshold": 0.8}, "steam": {"threshold": 0.8}, "steam_whistle": {"threshold": 0.8}, "steel_guitar": {"threshold": 0.8}, "steelpan": {"threshold": 0.8}, "stir": {"threshold": 0.8}, "stomach_rumble": {"threshold": 0.8}, "stream": {"threshold": 0.8}, "string_section": {"threshold": 0.8}, "strum": {"threshold": 0.8}, "subway": {"threshold": 0.8}, "swing_music": {"threshold": 0.8}, "synthesizer": {"threshold": 0.8}, "synthetic_singing": {"threshold": 0.8}, "tabla": {"threshold": 0.8}, "tambourine": {"threshold": 0.8}, "tap": {"threshold": 0.8}, "tapping": {"threshold": 0.8}, "tearing": {"threshold": 0.8}, "techno": {"threshold": 0.8}, "telephone": {"threshold": 0.8}, "telephone_bell_ringing": {"threshold": 0.8}, "telephone_dialing": {"threshold": 0.8}, "television": {"threshold": 0.8}, "tender_music": {"threshold": 0.8}, "theme_music": {"threshold": 0.8}, "theremin": {"threshold": 0.8}, "throat_clearing": {"threshold": 0.8}, "throbbing": {"threshold": 0.8}, "thump": {"threshold": 0.8}, "thunder": {"threshold": 0.8}, "thunderstorm": {"threshold": 0.8}, "thunk": {"threshold": 0.8}, "tick": {"threshold": 0.8}, "tick-tock": {"threshold": 0.8}, "timpani": {"threshold": 0.8}, "tire_squeal": {"threshold": 0.8}, "toilet_flush": {"threshold": 0.8}, "tools": {"threshold": 0.8}, "toot": {"threshold": 0.8}, "toothbrush": {"threshold": 0.8}, "traditional_music": {"threshold": 0.8}, "traffic_noise": {"threshold": 0.8}, "train": {"threshold": 0.8}, "train_horn": {"threshold": 0.8}, "train_wheels_squealing": {"threshold": 0.8}, "train_whistle": {"threshold": 0.8}, "trance_music": {"threshold": 0.8}, "trickle": {"threshold": 0.8}, "trombone": {"threshold": 0.8}, "truck": {"threshold": 0.8}, "trumpet": {"threshold": 0.8}, "tubular_bells": {"threshold": 0.8}, "tuning_fork": {"threshold": 0.8}, "turkey": {"threshold": 0.8}, "typewriter": {"threshold": 0.8}, "typing": {"threshold": 0.8}, "ukulele": {"threshold": 0.8}, "vacuum_cleaner": {"threshold": 0.8}, "vehicle": {"threshold": 0.8}, "vibraphone": {"threshold": 0.8}, "vibration": {"threshold": 0.8}, "video_game_music": {"threshold": 0.8}, "violin": {"threshold": 0.8}, "vocal_music": {"threshold": 0.8}, "water": {"threshold": 0.8}, "water_tap": {"threshold": 0.8}, "waterfall": {"threshold": 0.8}, "waves": {"threshold": 0.8}, "wedding_music": {"threshold": 0.8}, "whack": {"threshold": 0.8}, "whale_vocalization": {"threshold": 0.8}, "wheeze": {"threshold": 0.8}, "whimper_dog": {"threshold": 0.8}, "whip": {"threshold": 0.8}, "whir": {"threshold": 0.8}, "whispering": {"threshold": 0.8}, "whistle": {"threshold": 0.8}, "whistling": {"threshold": 0.8}, "white_noise": {"threshold": 0.8}, "whoop": {"threshold": 0.8}, "whoosh": {"threshold": 0.8}, "wild_animals": {"threshold": 0.8}, "wind": {"threshold": 0.8}, "wind_chime": {"threshold": 0.8}, "wind_instrument": {"threshold": 0.8}, "wind_noise": {"threshold": 0.8}, "wood": {"threshold": 0.8}, "wood_block": {"threshold": 0.8}, "writing": {"threshold": 0.8}, "yell": {"threshold": 0.8}, "yip": {"threshold": 0.8}, "zing": {"threshold": 0.8}, "zipper": {"threshold": 0.8}, "zither": {"threshold": 0.8}}, "enabled_in_config": false, "num_threads": 2}, "audio_transcription": {"enabled": false, "enabled_in_config": false, "live_enabled": false}, "birdseye": {"enabled": true, "mode": "objects", "order": 0}, "detect": {"enabled": false, "height": 720, "width": 1280, "fps": 5, "min_initialized": 2, "max_disappeared": 25, "stationary": {"interval": 50, "threshold": 50, "max_frames": {"default": null, "objects": {}}, "classifier": true}, "annotation_offset": 0}, "face_recognition": {"enabled": false, "min_area": 750}, "ffmpeg": {"path": "default", "global_args": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "hwaccel_args": "preset-vaapi", "input_args": "preset-rtsp-generic", "output_args": {"detect": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "record": "preset-record-generic-audio-aac"}, "retry_interval": 10.0, "apple_compatibility": false, "gpu": 0, "inputs": [{"path": "rtsp://10.0.0.3:554/video", "roles": ["record", "detect"], "global_args": [], "hwaccel_args": [], "input_args": []}]}, "live": {"streams": {"garage": "garage"}, "height": 720, "quality": 8}, "lpr": {"enabled": false, "expire_time": 3, "min_area": 1000, "enhancement": 0}, "motion": {"enabled": true, "threshold": 30, "lightning_threshold": 0.8, "skip_motion_threshold": null, "improve_contrast": true, "contour_area": 10, "delta_alpha": 0.2, "frame_alpha": 0.01, "frame_height": 100, "mask": {}, "mqtt_off_delay": 30, "enabled_in_config": null}, "objects": {"track": ["person"], "filters": {"person": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.5, "mask": {}}}, "mask": {}, "genai": {"enabled": false, "use_snapshot": false, "prompt": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "object_prompts": {}, "objects": [], "required_zones": [], "debug_save_thumbnails": false, "send_triggers": {"tracked_object_end": true, "after_significant_updates": null}, "enabled_in_config": false}}, "record": {"enabled": false, "expire_interval": 60, "continuous": {"days": 0}, "motion": {"days": 0}, "detections": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "alerts": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "export": {"hwaccel_args": "preset-vaapi"}, "preview": {"quality": "medium"}, "enabled_in_config": false}, "review": {"alerts": {"enabled": true, "labels": ["person", "car"], "required_zones": [], "enabled_in_config": true, "cutoff_time": 40}, "detections": {"enabled": true, "labels": null, "required_zones": [], "cutoff_time": 30, "enabled_in_config": true}, "genai": {"enabled": false, "alerts": true, "detections": false, "image_source": "preview", "additional_concerns": [], "debug_save_thumbnails": false, "enabled_in_config": false, "preferred_language": null, "activity_context_prompt": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is."}}, "semantic_search": {"triggers": {}}, "snapshots": {"enabled": false, "timestamp": false, "bounding_box": true, "crop": false, "required_zones": [], "height": null, "retain": {"default": 10, "mode": "motion", "objects": {}}, "quality": 60}, "timestamp_style": {"position": "tl", "format": "%m/%d/%Y %H:%M:%S", "color": {"red": 255, "green": 255, "blue": 255}, "thickness": 2, "effect": null}, "best_image_timeout": 60, "mqtt": {"enabled": true, "timestamp": true, "bounding_box": true, "crop": true, "height": 270, "required_zones": [], "quality": 70}, "notifications": {"enabled": false, "email": null, "cooldown": 0, "enabled_in_config": false}, "onvif": {"host": "", "port": 8000, "user": null, "password": null, "tls_insecure": false, "profile": null, "autotracking": {"enabled": false, "calibrate_on_startup": false, "zooming": "disabled", "zoom_factor": 0.3, "track": ["person"], "required_zones": [], "return_preset": "home", "timeout": 10, "movement_weights": [], "enabled_in_config": false}, "ignore_time_mismatch": false}, "type": "generic", "ui": {"order": 0, "dashboard": true}, "webui_url": null, "profiles": {}, "zones": {}, "enabled_in_config": true}}, "audio": {"enabled": false, "max_not_heard": 30, "min_volume": 500, "listen": ["bark", "fire_alarm", "scream", "speech", "yell"], "filters": {"a_capella": {"threshold": 0.8}, "accelerating": {"threshold": 0.8}, "accordion": {"threshold": 0.8}, "acoustic_guitar": {"threshold": 0.8}, "afrobeat": {"threshold": 0.8}, "air_brake": {"threshold": 0.8}, "air_conditioning": {"threshold": 0.8}, "air_horn": {"threshold": 0.8}, "aircraft": {"threshold": 0.8}, "aircraft_engine": {"threshold": 0.8}, "alarm": {"threshold": 0.8}, "alarm_clock": {"threshold": 0.8}, "ambient_music": {"threshold": 0.8}, "ambulance": {"threshold": 0.8}, "angry_music": {"threshold": 0.8}, "animal": {"threshold": 0.8}, "applause": {"threshold": 0.8}, "arrow": {"threshold": 0.8}, "artillery_fire": {"threshold": 0.8}, "babbling": {"threshold": 0.8}, "background_music": {"threshold": 0.8}, "bagpipes": {"threshold": 0.8}, "bang": {"threshold": 0.8}, "banjo": {"threshold": 0.8}, "bark": {"threshold": 0.8}, "basketball_bounce": {"threshold": 0.8}, "bass_drum": {"threshold": 0.8}, "bass_guitar": {"threshold": 0.8}, "bathtub": {"threshold": 0.8}, "beatboxing": {"threshold": 0.8}, "beep": {"threshold": 0.8}, "bell": {"threshold": 0.8}, "bellow": {"threshold": 0.8}, "bicycle": {"threshold": 0.8}, "bicycle_bell": {"threshold": 0.8}, "bird": {"threshold": 0.8}, "biting": {"threshold": 0.8}, "bleat": {"threshold": 0.8}, "blender": {"threshold": 0.8}, "bluegrass": {"threshold": 0.8}, "blues": {"threshold": 0.8}, "boat": {"threshold": 0.8}, "boiling": {"threshold": 0.8}, "boing": {"threshold": 0.8}, "boom": {"threshold": 0.8}, "bouncing": {"threshold": 0.8}, "bow-wow": {"threshold": 0.8}, "bowed_string_instrument": {"threshold": 0.8}, "brass_instrument": {"threshold": 0.8}, "breaking": {"threshold": 0.8}, "breathing": {"threshold": 0.8}, "burping": {"threshold": 0.8}, "burst": {"threshold": 0.8}, "bus": {"threshold": 0.8}, "busy_signal": {"threshold": 0.8}, "buzz": {"threshold": 0.8}, "buzzer": {"threshold": 0.8}, "cacophony": {"threshold": 0.8}, "camera": {"threshold": 0.8}, "cap_gun": {"threshold": 0.8}, "car": {"threshold": 0.8}, "car_alarm": {"threshold": 0.8}, "car_passing_by": {"threshold": 0.8}, "carnatic_music": {"threshold": 0.8}, "cash_register": {"threshold": 0.8}, "cat": {"threshold": 0.8}, "caterwaul": {"threshold": 0.8}, "cattle": {"threshold": 0.8}, "caw": {"threshold": 0.8}, "cello": {"threshold": 0.8}, "chainsaw": {"threshold": 0.8}, "change_ringing": {"threshold": 0.8}, "chant": {"threshold": 0.8}, "chatter": {"threshold": 0.8}, "cheering": {"threshold": 0.8}, "chewing": {"threshold": 0.8}, "chicken": {"threshold": 0.8}, "child_singing": {"threshold": 0.8}, "children_playing": {"threshold": 0.8}, "chime": {"threshold": 0.8}, "chink": {"threshold": 0.8}, "chird": {"threshold": 0.8}, "chirp": {"threshold": 0.8}, "chirp_tone": {"threshold": 0.8}, "choir": {"threshold": 0.8}, "chop": {"threshold": 0.8}, "chopping": {"threshold": 0.8}, "chorus_effect": {"threshold": 0.8}, "christian_music": {"threshold": 0.8}, "christmas_music": {"threshold": 0.8}, "church_bell": {"threshold": 0.8}, "civil_defense_siren": {"threshold": 0.8}, "clang": {"threshold": 0.8}, "clapping": {"threshold": 0.8}, "clarinet": {"threshold": 0.8}, "classical_music": {"threshold": 0.8}, "clatter": {"threshold": 0.8}, "clickety-clack": {"threshold": 0.8}, "clicking": {"threshold": 0.8}, "clip-clop": {"threshold": 0.8}, "clock": {"threshold": 0.8}, "cluck": {"threshold": 0.8}, "cock-a-doodle-doo": {"threshold": 0.8}, "coin": {"threshold": 0.8}, "computer_keyboard": {"threshold": 0.8}, "coo": {"threshold": 0.8}, "cough": {"threshold": 0.8}, "country": {"threshold": 0.8}, "cowbell": {"threshold": 0.8}, "crack": {"threshold": 0.8}, "crackle": {"threshold": 0.8}, "creak": {"threshold": 0.8}, "cricket": {"threshold": 0.8}, "croak": {"threshold": 0.8}, "crow": {"threshold": 0.8}, "crowd": {"threshold": 0.8}, "crumpling": {"threshold": 0.8}, "crunch": {"threshold": 0.8}, "crushing": {"threshold": 0.8}, "crying": {"threshold": 0.8}, "cupboard_open_or_close": {"threshold": 0.8}, "cutlery": {"threshold": 0.8}, "cymbal": {"threshold": 0.8}, "dance_music": {"threshold": 0.8}, "dental_drill's_drill": {"threshold": 0.8}, "dial_tone": {"threshold": 0.8}, "didgeridoo": {"threshold": 0.8}, "ding": {"threshold": 0.8}, "ding-dong": {"threshold": 0.8}, "disco": {"threshold": 0.8}, "dishes": {"threshold": 0.8}, "distortion": {"threshold": 0.8}, "dog": {"threshold": 0.8}, "dogs": {"threshold": 0.8}, "door": {"threshold": 0.8}, "doorbell": {"threshold": 0.8}, "double_bass": {"threshold": 0.8}, "drawer_open_or_close": {"threshold": 0.8}, "drill": {"threshold": 0.8}, "drip": {"threshold": 0.8}, "drum": {"threshold": 0.8}, "drum_and_bass": {"threshold": 0.8}, "drum_kit": {"threshold": 0.8}, "drum_machine": {"threshold": 0.8}, "drum_roll": {"threshold": 0.8}, "dubstep": {"threshold": 0.8}, "duck": {"threshold": 0.8}, "echo": {"threshold": 0.8}, "effects_unit": {"threshold": 0.8}, "electric_guitar": {"threshold": 0.8}, "electric_piano": {"threshold": 0.8}, "electric_shaver": {"threshold": 0.8}, "electric_toothbrush": {"threshold": 0.8}, "electronic_dance_music": {"threshold": 0.8}, "electronic_music": {"threshold": 0.8}, "electronic_organ": {"threshold": 0.8}, "electronic_tuner": {"threshold": 0.8}, "electronica": {"threshold": 0.8}, "emergency_vehicle": {"threshold": 0.8}, "engine": {"threshold": 0.8}, "engine_knocking": {"threshold": 0.8}, "engine_starting": {"threshold": 0.8}, "environmental_noise": {"threshold": 0.8}, "eruption": {"threshold": 0.8}, "exciting_music": {"threshold": 0.8}, "explosion": {"threshold": 0.8}, "fart": {"threshold": 0.8}, "field_recording": {"threshold": 0.8}, "filing": {"threshold": 0.8}, "fill": {"threshold": 0.8}, "finger_snapping": {"threshold": 0.8}, "fire": {"threshold": 0.8}, "fire_alarm": {"threshold": 0.8}, "fire_engine": {"threshold": 0.8}, "firecracker": {"threshold": 0.8}, "fireworks": {"threshold": 0.8}, "fixed-wing_aircraft": {"threshold": 0.8}, "flamenco": {"threshold": 0.8}, "flap": {"threshold": 0.8}, "flapping_wings": {"threshold": 0.8}, "flute": {"threshold": 0.8}, "fly": {"threshold": 0.8}, "foghorn": {"threshold": 0.8}, "folk_music": {"threshold": 0.8}, "footsteps": {"threshold": 0.8}, "fowl": {"threshold": 0.8}, "french_horn": {"threshold": 0.8}, "frog": {"threshold": 0.8}, "frying": {"threshold": 0.8}, "funk": {"threshold": 0.8}, "fusillade": {"threshold": 0.8}, "gargling": {"threshold": 0.8}, "gasp": {"threshold": 0.8}, "gears": {"threshold": 0.8}, "glass": {"threshold": 0.8}, "glockenspiel": {"threshold": 0.8}, "goat": {"threshold": 0.8}, "gobble": {"threshold": 0.8}, "gong": {"threshold": 0.8}, "goose": {"threshold": 0.8}, "gospel_music": {"threshold": 0.8}, "groan": {"threshold": 0.8}, "growling": {"threshold": 0.8}, "grunge": {"threshold": 0.8}, "grunt": {"threshold": 0.8}, "guitar": {"threshold": 0.8}, "gunshot": {"threshold": 0.8}, "gurgling": {"threshold": 0.8}, "gush": {"threshold": 0.8}, "hair_dryer": {"threshold": 0.8}, "hammer": {"threshold": 0.8}, "hammond_organ": {"threshold": 0.8}, "hands": {"threshold": 0.8}, "happy_music": {"threshold": 0.8}, "harmonic": {"threshold": 0.8}, "harmonica": {"threshold": 0.8}, "harp": {"threshold": 0.8}, "harpsichord": {"threshold": 0.8}, "heart_murmur": {"threshold": 0.8}, "heartbeat": {"threshold": 0.8}, "heavy_engine": {"threshold": 0.8}, "heavy_metal": {"threshold": 0.8}, "helicopter": {"threshold": 0.8}, "hi-hat": {"threshold": 0.8}, "hiccup": {"threshold": 0.8}, "hip_hop_music": {"threshold": 0.8}, "hiss": {"threshold": 0.8}, "honk": {"threshold": 0.8}, "hoot": {"threshold": 0.8}, "horse": {"threshold": 0.8}, "house_music": {"threshold": 0.8}, "howl": {"threshold": 0.8}, "hum": {"threshold": 0.8}, "humming": {"threshold": 0.8}, "ice_cream_truck": {"threshold": 0.8}, "idling": {"threshold": 0.8}, "independent_music": {"threshold": 0.8}, "insect": {"threshold": 0.8}, "inside": {"threshold": 0.8}, "jackhammer": {"threshold": 0.8}, "jazz": {"threshold": 0.8}, "jet_engine": {"threshold": 0.8}, "jingle": {"threshold": 0.8}, "jingle_bell": {"threshold": 0.8}, "keyboard": {"threshold": 0.8}, "keys_jangling": {"threshold": 0.8}, "knock": {"threshold": 0.8}, "laughter": {"threshold": 0.8}, "lawn_mower": {"threshold": 0.8}, "light_engine": {"threshold": 0.8}, "liquid": {"threshold": 0.8}, "livestock": {"threshold": 0.8}, "lullaby": {"threshold": 0.8}, "machine_gun": {"threshold": 0.8}, "mains_hum": {"threshold": 0.8}, "mallet_percussion": {"threshold": 0.8}, "mandolin": {"threshold": 0.8}, "mantra": {"threshold": 0.8}, "maraca": {"threshold": 0.8}, "marimba": {"threshold": 0.8}, "mechanical_fan": {"threshold": 0.8}, "mechanisms": {"threshold": 0.8}, "medium_engine": {"threshold": 0.8}, "meow": {"threshold": 0.8}, "microwave_oven": {"threshold": 0.8}, "middle_eastern_music": {"threshold": 0.8}, "moo": {"threshold": 0.8}, "mosquito": {"threshold": 0.8}, "motor_vehicle": {"threshold": 0.8}, "motorboat": {"threshold": 0.8}, "motorcycle": {"threshold": 0.8}, "mouse": {"threshold": 0.8}, "music": {"threshold": 0.8}, "music_for_children": {"threshold": 0.8}, "music_of_africa": {"threshold": 0.8}, "music_of_asia": {"threshold": 0.8}, "music_of_bollywood": {"threshold": 0.8}, "music_of_latin_america": {"threshold": 0.8}, "musical_instrument": {"threshold": 0.8}, "neigh": {"threshold": 0.8}, "new-age_music": {"threshold": 0.8}, "noise": {"threshold": 0.8}, "ocean": {"threshold": 0.8}, "oink": {"threshold": 0.8}, "opera": {"threshold": 0.8}, "orchestra": {"threshold": 0.8}, "organ": {"threshold": 0.8}, "outside": {"threshold": 0.8}, "owl": {"threshold": 0.8}, "pant": {"threshold": 0.8}, "patter": {"threshold": 0.8}, "percussion": {"threshold": 0.8}, "pets": {"threshold": 0.8}, "piano": {"threshold": 0.8}, "pig": {"threshold": 0.8}, "pigeon": {"threshold": 0.8}, "ping": {"threshold": 0.8}, "pink_noise": {"threshold": 0.8}, "pizzicato": {"threshold": 0.8}, "plop": {"threshold": 0.8}, "plucked_string_instrument": {"threshold": 0.8}, "police_car": {"threshold": 0.8}, "pop_music": {"threshold": 0.8}, "pour": {"threshold": 0.8}, "power_tool": {"threshold": 0.8}, "power_windows": {"threshold": 0.8}, "printer": {"threshold": 0.8}, "progressive_rock": {"threshold": 0.8}, "propeller": {"threshold": 0.8}, "psychedelic_rock": {"threshold": 0.8}, "pulleys": {"threshold": 0.8}, "pulse": {"threshold": 0.8}, "pump": {"threshold": 0.8}, "punk_rock": {"threshold": 0.8}, "purr": {"threshold": 0.8}, "quack": {"threshold": 0.8}, "race_car": {"threshold": 0.8}, "radio": {"threshold": 0.8}, "rail_transport": {"threshold": 0.8}, "railroad_car": {"threshold": 0.8}, "rain": {"threshold": 0.8}, "rain_on_surface": {"threshold": 0.8}, "raindrop": {"threshold": 0.8}, "rapping": {"threshold": 0.8}, "ratchet": {"threshold": 0.8}, "rats": {"threshold": 0.8}, "rattle": {"threshold": 0.8}, "reggae": {"threshold": 0.8}, "reverberation": {"threshold": 0.8}, "reversing_beeps": {"threshold": 0.8}, "rhythm_and_blues": {"threshold": 0.8}, "rimshot": {"threshold": 0.8}, "ringtone": {"threshold": 0.8}, "roar": {"threshold": 0.8}, "roaring_cats": {"threshold": 0.8}, "rock_and_roll": {"threshold": 0.8}, "rock_music": {"threshold": 0.8}, "roll": {"threshold": 0.8}, "rowboat": {"threshold": 0.8}, "rub": {"threshold": 0.8}, "rumble": {"threshold": 0.8}, "run": {"threshold": 0.8}, "rustle": {"threshold": 0.8}, "rustling_leaves": {"threshold": 0.8}, "sad_music": {"threshold": 0.8}, "sailboat": {"threshold": 0.8}, "salsa_music": {"threshold": 0.8}, "sampler": {"threshold": 0.8}, "sanding": {"threshold": 0.8}, "sawing": {"threshold": 0.8}, "saxophone": {"threshold": 0.8}, "scary_music": {"threshold": 0.8}, "scissors": {"threshold": 0.8}, "scrape": {"threshold": 0.8}, "scratch": {"threshold": 0.8}, "scratching": {"threshold": 0.8}, "sewing_machine": {"threshold": 0.8}, "shatter": {"threshold": 0.8}, "sheep": {"threshold": 0.8}, "ship": {"threshold": 0.8}, "shofar": {"threshold": 0.8}, "shuffle": {"threshold": 0.8}, "shuffling_cards": {"threshold": 0.8}, "sidetone": {"threshold": 0.8}, "sigh": {"threshold": 0.8}, "silence": {"threshold": 0.8}, "sine_wave": {"threshold": 0.8}, "singing": {"threshold": 0.8}, "singing_bowl": {"threshold": 0.8}, "single-lens_reflex_camera": {"threshold": 0.8}, "sink": {"threshold": 0.8}, "siren": {"threshold": 0.8}, "sitar": {"threshold": 0.8}, "sizzle": {"threshold": 0.8}, "ska": {"threshold": 0.8}, "skateboard": {"threshold": 0.8}, "skidding": {"threshold": 0.8}, "slam": {"threshold": 0.8}, "slap": {"threshold": 0.8}, "sliding_door": {"threshold": 0.8}, "slosh": {"threshold": 0.8}, "smash": {"threshold": 0.8}, "smoke_detector": {"threshold": 0.8}, "snake": {"threshold": 0.8}, "snare_drum": {"threshold": 0.8}, "sneeze": {"threshold": 0.8}, "snicker": {"threshold": 0.8}, "sniff": {"threshold": 0.8}, "snoring": {"threshold": 0.8}, "snort": {"threshold": 0.8}, "sodeling": {"threshold": 0.8}, "sonar": {"threshold": 0.8}, "song": {"threshold": 0.8}, "soul_music": {"threshold": 0.8}, "sound_effect": {"threshold": 0.8}, "soundtrack_music": {"threshold": 0.8}, "speech": {"threshold": 0.8}, "splash": {"threshold": 0.8}, "splinter": {"threshold": 0.8}, "spray": {"threshold": 0.8}, "squawk": {"threshold": 0.8}, "squeak": {"threshold": 0.8}, "squeal": {"threshold": 0.8}, "squish": {"threshold": 0.8}, "static": {"threshold": 0.8}, "steam": {"threshold": 0.8}, "steam_whistle": {"threshold": 0.8}, "steel_guitar": {"threshold": 0.8}, "steelpan": {"threshold": 0.8}, "stir": {"threshold": 0.8}, "stomach_rumble": {"threshold": 0.8}, "stream": {"threshold": 0.8}, "string_section": {"threshold": 0.8}, "strum": {"threshold": 0.8}, "subway": {"threshold": 0.8}, "swing_music": {"threshold": 0.8}, "synthesizer": {"threshold": 0.8}, "synthetic_singing": {"threshold": 0.8}, "tabla": {"threshold": 0.8}, "tambourine": {"threshold": 0.8}, "tap": {"threshold": 0.8}, "tapping": {"threshold": 0.8}, "tearing": {"threshold": 0.8}, "techno": {"threshold": 0.8}, "telephone": {"threshold": 0.8}, "telephone_bell_ringing": {"threshold": 0.8}, "telephone_dialing": {"threshold": 0.8}, "television": {"threshold": 0.8}, "tender_music": {"threshold": 0.8}, "theme_music": {"threshold": 0.8}, "theremin": {"threshold": 0.8}, "throat_clearing": {"threshold": 0.8}, "throbbing": {"threshold": 0.8}, "thump": {"threshold": 0.8}, "thunder": {"threshold": 0.8}, "thunderstorm": {"threshold": 0.8}, "thunk": {"threshold": 0.8}, "tick": {"threshold": 0.8}, "tick-tock": {"threshold": 0.8}, "timpani": {"threshold": 0.8}, "tire_squeal": {"threshold": 0.8}, "toilet_flush": {"threshold": 0.8}, "tools": {"threshold": 0.8}, "toot": {"threshold": 0.8}, "toothbrush": {"threshold": 0.8}, "traditional_music": {"threshold": 0.8}, "traffic_noise": {"threshold": 0.8}, "train": {"threshold": 0.8}, "train_horn": {"threshold": 0.8}, "train_wheels_squealing": {"threshold": 0.8}, "train_whistle": {"threshold": 0.8}, "trance_music": {"threshold": 0.8}, "trickle": {"threshold": 0.8}, "trombone": {"threshold": 0.8}, "truck": {"threshold": 0.8}, "trumpet": {"threshold": 0.8}, "tubular_bells": {"threshold": 0.8}, "tuning_fork": {"threshold": 0.8}, "turkey": {"threshold": 0.8}, "typewriter": {"threshold": 0.8}, "typing": {"threshold": 0.8}, "ukulele": {"threshold": 0.8}, "vacuum_cleaner": {"threshold": 0.8}, "vehicle": {"threshold": 0.8}, "vibraphone": {"threshold": 0.8}, "vibration": {"threshold": 0.8}, "video_game_music": {"threshold": 0.8}, "violin": {"threshold": 0.8}, "vocal_music": {"threshold": 0.8}, "water": {"threshold": 0.8}, "water_tap": {"threshold": 0.8}, "waterfall": {"threshold": 0.8}, "waves": {"threshold": 0.8}, "wedding_music": {"threshold": 0.8}, "whack": {"threshold": 0.8}, "whale_vocalization": {"threshold": 0.8}, "wheeze": {"threshold": 0.8}, "whimper_dog": {"threshold": 0.8}, "whip": {"threshold": 0.8}, "whir": {"threshold": 0.8}, "whispering": {"threshold": 0.8}, "whistle": {"threshold": 0.8}, "whistling": {"threshold": 0.8}, "white_noise": {"threshold": 0.8}, "whoop": {"threshold": 0.8}, "whoosh": {"threshold": 0.8}, "wild_animals": {"threshold": 0.8}, "wind": {"threshold": 0.8}, "wind_chime": {"threshold": 0.8}, "wind_instrument": {"threshold": 0.8}, "wind_noise": {"threshold": 0.8}, "wood": {"threshold": 0.8}, "wood_block": {"threshold": 0.8}, "writing": {"threshold": 0.8}, "yell": {"threshold": 0.8}, "yip": {"threshold": 0.8}, "zing": {"threshold": 0.8}, "zipper": {"threshold": 0.8}, "zither": {"threshold": 0.8}}, "enabled_in_config": null, "num_threads": 2}, "birdseye": {"enabled": true, "mode": "objects", "restream": false, "width": 1280, "height": 720, "quality": 8, "inactivity_threshold": 30, "layout": {"scaling_factor": 2.0, "max_cameras": null}, "idle_heartbeat_fps": 0.0}, "detect": {"enabled": false, "height": null, "width": null, "fps": 5, "min_initialized": null, "max_disappeared": null, "stationary": {"interval": null, "threshold": null, "max_frames": {"default": null, "objects": {}}, "classifier": true}, "annotation_offset": 0}, "ffmpeg": {"path": "default", "global_args": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "hwaccel_args": "preset-vaapi", "input_args": "preset-rtsp-generic", "output_args": {"detect": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "record": "preset-record-generic-audio-aac"}, "retry_interval": 10.0, "apple_compatibility": false, "gpu": 0}, "live": {"streams": [], "height": 720, "quality": 8}, "motion": null, "objects": {"track": ["person"], "filters": {"amazon": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "dpd": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "an_post": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "royal_mail": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "ups": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "nzpost": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "license_plate": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "canada_post": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "postnord": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "postnl": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "face": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "dhl": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "fedex": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "usps": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "gls": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "purolator": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}}, "mask": {}, "genai": {"enabled": false, "use_snapshot": false, "prompt": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "object_prompts": {}, "objects": [], "required_zones": [], "debug_save_thumbnails": false, "send_triggers": {"tracked_object_end": true, "after_significant_updates": null}, "enabled_in_config": null}}, "record": {"enabled": false, "expire_interval": 60, "continuous": {"days": 0}, "motion": {"days": 0}, "detections": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "alerts": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "export": {"hwaccel_args": "auto"}, "preview": {"quality": "medium"}, "enabled_in_config": null}, "review": {"alerts": {"enabled": true, "labels": ["person", "car"], "required_zones": [], "enabled_in_config": null, "cutoff_time": 40}, "detections": {"enabled": true, "labels": null, "required_zones": [], "cutoff_time": 30, "enabled_in_config": null}, "genai": {"enabled": false, "alerts": true, "detections": false, "image_source": "preview", "additional_concerns": [], "debug_save_thumbnails": false, "enabled_in_config": null, "preferred_language": null, "activity_context_prompt": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is."}}, "snapshots": {"enabled": false, "timestamp": false, "bounding_box": true, "crop": false, "required_zones": [], "height": null, "retain": {"default": 10, "mode": "motion", "objects": {}}, "quality": 60}, "timestamp_style": {"position": "tl", "format": "%m/%d/%Y %H:%M:%S", "color": {"red": 255, "green": 255, "blue": 255}, "thickness": 2, "effect": null}, "audio_transcription": {"enabled": false, "language": "en", "device": "CPU", "model_size": "small", "live_enabled": false}, "classification": {"bird": {"enabled": false, "threshold": 0.9}, "custom": {}}, "semantic_search": {"enabled": false, "reindex": false, "model": "jinav1", "model_size": "small", "device": null}, "face_recognition": {"enabled": false, "model_size": "small", "unknown_score": 0.8, "detection_threshold": 0.7, "recognition_threshold": 0.9, "min_area": 750, "min_faces": 1, "save_attempts": 200, "blur_confidence_filter": true, "device": null}, "lpr": {"enabled": false, "model_size": "small", "detection_threshold": 0.7, "min_area": 1000, "recognition_threshold": 0.9, "min_plate_length": 4, "format": null, "match_distance": 1, "known_plates": {}, "enhancement": 0, "debug_save_plates": false, "device": null, "replace_rules": []}, "camera_groups": {"default": {"cameras": ["front_door", "backyard", "garage"], "icon": "generic", "order": 0}, "outdoor": {"cameras": ["front_door", "backyard"], "icon": "generic", "order": 1}}, "profiles": {}}
\ No newline at end of file
+{"version": null, "safe_mode": false, "environment_vars": {}, "logger": {"default": "info", "logs": {}}, "auth": {"enabled": true, "reset_admin_password": false, "cookie_name": "frigate_token", "cookie_secure": false, "session_length": 86400, "refresh_time": 1800, "failed_login_rate_limit": null, "trusted_proxies": [], "hash_iterations": 600000, "roles": {"admin": [], "viewer": []}, "admin_first_time_login": false}, "database": {"path": "/config/frigate.db"}, "go2rtc": {}, "mqtt": {"enabled": true, "host": "mqtt", "port": 1883, "topic_prefix": "frigate", "client_id": "frigate", "stats_interval": 60, "user": null, "password": null, "tls_ca_certs": null, "tls_client_cert": null, "tls_client_key": null, "tls_insecure": null, "qos": 0}, "notifications": {"enabled": false, "email": null, "cooldown": 0, "enabled_in_config": false}, "networking": {"ipv6": {"enabled": false}, "listen": {"internal": 5000, "external": 8971}}, "proxy": {"header_map": {"user": null, "role": null, "role_map": {}}, "logout_url": null, "auth_secret": null, "default_role": "viewer", "separator": ","}, "telemetry": {"network_interfaces": [], "stats": {"amd_gpu_stats": true, "intel_gpu_stats": true, "network_bandwidth": false, "intel_gpu_device": null}, "version_check": true}, "tls": {"enabled": true}, "ui": {"timezone": null, "time_format": "browser", "unit_system": "metric"}, "detectors": {"cpu": {"type": "cpu", "model": {"path": "/cpu_model.tflite", "labelmap_path": null, "width": 320, "height": 320, "labelmap": {}, "attributes_map": {"person": ["amazon", "face"], "car": ["amazon", "an_post", "canada_post", "dhl", "dpd", "fedex", "gls", "license_plate", "nzpost", "postnl", "postnord", "purolator", "royal_mail", "ups", "usps"], "motorcycle": ["license_plate"]}, "input_tensor": "nhwc", "input_pixel_format": "rgb", "input_dtype": "int", "model_type": "ssd"}, "model_path": null}}, "model": {"path": null, "labelmap_path": null, "width": 320, "height": 320, "labelmap": {}, "attributes_map": {"person": ["amazon", "face"], "car": ["amazon", "an_post", "canada_post", "dhl", "dpd", "fedex", "gls", "license_plate", "nzpost", "postnl", "postnord", "purolator", "royal_mail", "ups", "usps"], "motorcycle": ["license_plate"]}, "input_tensor": "nhwc", "input_pixel_format": "rgb", "input_dtype": "int", "model_type": "ssd", "all_attributes": ["amazon", "an_post", "canada_post", "dhl", "dpd", "face", "fedex", "gls", "license_plate", "nzpost", "postnl", "postnord", "purolator", "royal_mail", "ups", "usps"], "colormap": {}}, "genai": {}, "cameras": {"front_door": {"name": "front_door", "friendly_name": null, "enabled": true, "audio": {"enabled": false, "max_not_heard": 30, "min_volume": 500, "listen": ["bark", "fire_alarm", "speech", "yell"], "filters": {"bark": {"threshold": 0.8}, "fire_alarm": {"threshold": 0.8}, "speech": {"threshold": 0.8}, "yell": {"threshold": 0.8}}, "enabled_in_config": false, "num_threads": 2}, "audio_transcription": {"enabled": false, "enabled_in_config": false, "live_enabled": false}, "birdseye": {"enabled": true, "mode": "objects", "order": 0}, "detect": {"enabled": false, "height": 720, "width": 1280, "fps": 5, "min_initialized": 2, "max_disappeared": 25, "stationary": {"interval": 50, "threshold": 50, "max_frames": {"default": null, "objects": {}}, "classifier": true}, "annotation_offset": 0}, "face_recognition": {"enabled": false, "min_area": 750}, "ffmpeg": {"path": "default", "global_args": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "hwaccel_args": "preset-vaapi", "input_args": "preset-rtsp-generic", "output_args": {"detect": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "record": "preset-record-generic-audio-aac"}, "retry_interval": 10.0, "apple_compatibility": false, "gpu": 0, "inputs": [{"path": "rtsp://10.0.0.1:554/video", "roles": ["record", "detect"], "global_args": [], "hwaccel_args": [], "input_args": []}]}, "live": {"streams": {"front_door": "front_door"}, "height": 720, "quality": 8}, "lpr": {"enabled": false, "expire_time": 3, "min_area": 1000, "enhancement": 0}, "motion": {"enabled": true, "threshold": 30, "lightning_threshold": 0.8, "skip_motion_threshold": null, "improve_contrast": true, "contour_area": 10, "delta_alpha": 0.2, "frame_alpha": 0.01, "frame_height": 100, "mask": {}, "mqtt_off_delay": 30, "enabled_in_config": null}, "objects": {"track": ["person"], "filters": {"person": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.5, "mask": {}}}, "mask": {}, "genai": {"enabled": false, "use_snapshot": false, "prompt": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "object_prompts": {}, "objects": [], "required_zones": [], "debug_save_thumbnails": false, "send_triggers": {"tracked_object_end": true, "after_significant_updates": null}, "enabled_in_config": false}}, "record": {"enabled": false, "expire_interval": 60, "continuous": {"days": 0}, "motion": {"days": 0}, "detections": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "alerts": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "export": {"hwaccel_args": "preset-vaapi", "max_concurrent": 3}, "preview": {"quality": "medium"}, "enabled_in_config": false}, "review": {"alerts": {"enabled": true, "labels": ["person", "car"], "required_zones": [], "enabled_in_config": true, "cutoff_time": 40}, "detections": {"enabled": true, "labels": null, "required_zones": [], "cutoff_time": 30, "enabled_in_config": true}, "genai": {"enabled": false, "alerts": true, "detections": false, "image_source": "preview", "additional_concerns": [], "debug_save_thumbnails": false, "enabled_in_config": false, "preferred_language": null, "activity_context_prompt": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is."}}, "semantic_search": {"triggers": {}}, "snapshots": {"enabled": false, "timestamp": false, "bounding_box": true, "crop": false, "required_zones": [], "height": null, "retain": {"default": 10, "mode": "motion", "objects": {}}, "quality": 60}, "timestamp_style": {"position": "tl", "format": "%m/%d/%Y %H:%M:%S", "color": {"red": 255, "green": 255, "blue": 255}, "thickness": 2, "effect": null}, "best_image_timeout": 60, "mqtt": {"enabled": true, "timestamp": true, "bounding_box": true, "crop": true, "height": 270, "required_zones": [], "quality": 70}, "notifications": {"enabled": false, "email": null, "cooldown": 0, "enabled_in_config": false}, "onvif": {"host": "", "port": 8000, "user": null, "password": null, "tls_insecure": false, "profile": null, "autotracking": {"enabled": false, "calibrate_on_startup": false, "zooming": "disabled", "zoom_factor": 0.3, "track": ["person"], "required_zones": [], "return_preset": "home", "timeout": 10, "movement_weights": [], "enabled_in_config": false}, "ignore_time_mismatch": false}, "type": "generic", "ui": {"order": 0, "dashboard": true, "review": true}, "webui_url": null, "profiles": {}, "zones": {}, "enabled_in_config": true}, "backyard": {"name": "backyard", "friendly_name": null, "enabled": true, "audio": {"enabled": false, "max_not_heard": 30, "min_volume": 500, "listen": ["bark", "fire_alarm", "speech", "yell"], "filters": {"bark": {"threshold": 0.8}, "fire_alarm": {"threshold": 0.8}, "speech": {"threshold": 0.8}, "yell": {"threshold": 0.8}}, "enabled_in_config": false, "num_threads": 2}, "audio_transcription": {"enabled": false, "enabled_in_config": false, "live_enabled": false}, "birdseye": {"enabled": true, "mode": "objects", "order": 0}, "detect": {"enabled": false, "height": 720, "width": 1280, "fps": 5, "min_initialized": 2, "max_disappeared": 25, "stationary": {"interval": 50, "threshold": 50, "max_frames": {"default": null, "objects": {}}, "classifier": true}, "annotation_offset": 0}, "face_recognition": {"enabled": false, "min_area": 750}, "ffmpeg": {"path": "default", "global_args": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "hwaccel_args": "preset-vaapi", "input_args": "preset-rtsp-generic", "output_args": {"detect": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "record": "preset-record-generic-audio-aac"}, "retry_interval": 10.0, "apple_compatibility": false, "gpu": 0, "inputs": [{"path": "rtsp://10.0.0.2:554/video", "roles": ["record", "detect"], "global_args": [], "hwaccel_args": [], "input_args": []}]}, "live": {"streams": {"backyard": "backyard"}, "height": 720, "quality": 8}, "lpr": {"enabled": false, "expire_time": 3, "min_area": 1000, "enhancement": 0}, "motion": {"enabled": true, "threshold": 30, "lightning_threshold": 0.8, "skip_motion_threshold": null, "improve_contrast": true, "contour_area": 10, "delta_alpha": 0.2, "frame_alpha": 0.01, "frame_height": 100, "mask": {}, "mqtt_off_delay": 30, "enabled_in_config": null}, "objects": {"track": ["person"], "filters": {"person": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.5, "mask": {}}}, "mask": {}, "genai": {"enabled": false, "use_snapshot": false, "prompt": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "object_prompts": {}, "objects": [], "required_zones": [], "debug_save_thumbnails": false, "send_triggers": {"tracked_object_end": true, "after_significant_updates": null}, "enabled_in_config": false}}, "record": {"enabled": false, "expire_interval": 60, "continuous": {"days": 0}, "motion": {"days": 0}, "detections": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "alerts": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "export": {"hwaccel_args": "preset-vaapi", "max_concurrent": 3}, "preview": {"quality": "medium"}, "enabled_in_config": false}, "review": {"alerts": {"enabled": true, "labels": ["person", "car"], "required_zones": [], "enabled_in_config": true, "cutoff_time": 40}, "detections": {"enabled": true, "labels": null, "required_zones": [], "cutoff_time": 30, "enabled_in_config": true}, "genai": {"enabled": false, "alerts": true, "detections": false, "image_source": "preview", "additional_concerns": [], "debug_save_thumbnails": false, "enabled_in_config": false, "preferred_language": null, "activity_context_prompt": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is."}}, "semantic_search": {"triggers": {}}, "snapshots": {"enabled": false, "timestamp": false, "bounding_box": true, "crop": false, "required_zones": [], "height": null, "retain": {"default": 10, "mode": "motion", "objects": {}}, "quality": 60}, "timestamp_style": {"position": "tl", "format": "%m/%d/%Y %H:%M:%S", "color": {"red": 255, "green": 255, "blue": 255}, "thickness": 2, "effect": null}, "best_image_timeout": 60, "mqtt": {"enabled": true, "timestamp": true, "bounding_box": true, "crop": true, "height": 270, "required_zones": [], "quality": 70}, "notifications": {"enabled": false, "email": null, "cooldown": 0, "enabled_in_config": false}, "onvif": {"host": "", "port": 8000, "user": null, "password": null, "tls_insecure": false, "profile": null, "autotracking": {"enabled": false, "calibrate_on_startup": false, "zooming": "disabled", "zoom_factor": 0.3, "track": ["person"], "required_zones": [], "return_preset": "home", "timeout": 10, "movement_weights": [], "enabled_in_config": false}, "ignore_time_mismatch": false}, "type": "generic", "ui": {"order": 0, "dashboard": true, "review": true}, "webui_url": null, "profiles": {}, "zones": {}, "enabled_in_config": true}, "garage": {"name": "garage", "friendly_name": null, "enabled": true, "audio": {"enabled": false, "max_not_heard": 30, "min_volume": 500, "listen": ["bark", "fire_alarm", "speech", "yell"], "filters": {"bark": {"threshold": 0.8}, "fire_alarm": {"threshold": 0.8}, "speech": {"threshold": 0.8}, "yell": {"threshold": 0.8}}, "enabled_in_config": false, "num_threads": 2}, "audio_transcription": {"enabled": false, "enabled_in_config": false, "live_enabled": false}, "birdseye": {"enabled": true, "mode": "objects", "order": 0}, "detect": {"enabled": false, "height": 720, "width": 1280, "fps": 5, "min_initialized": 2, "max_disappeared": 25, "stationary": {"interval": 50, "threshold": 50, "max_frames": {"default": null, "objects": {}}, "classifier": true}, "annotation_offset": 0}, "face_recognition": {"enabled": false, "min_area": 750}, "ffmpeg": {"path": "default", "global_args": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "hwaccel_args": "preset-vaapi", "input_args": "preset-rtsp-generic", "output_args": {"detect": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "record": "preset-record-generic-audio-aac"}, "retry_interval": 10.0, "apple_compatibility": false, "gpu": 0, "inputs": [{"path": "rtsp://10.0.0.3:554/video", "roles": ["record", "detect"], "global_args": [], "hwaccel_args": [], "input_args": []}]}, "live": {"streams": {"garage": "garage"}, "height": 720, "quality": 8}, "lpr": {"enabled": false, "expire_time": 3, "min_area": 1000, "enhancement": 0}, "motion": {"enabled": true, "threshold": 30, "lightning_threshold": 0.8, "skip_motion_threshold": null, "improve_contrast": true, "contour_area": 10, "delta_alpha": 0.2, "frame_alpha": 0.01, "frame_height": 100, "mask": {}, "mqtt_off_delay": 30, "enabled_in_config": null}, "objects": {"track": ["person"], "filters": {"person": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.5, "mask": {}}}, "mask": {}, "genai": {"enabled": false, "use_snapshot": false, "prompt": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "object_prompts": {}, "objects": [], "required_zones": [], "debug_save_thumbnails": false, "send_triggers": {"tracked_object_end": true, "after_significant_updates": null}, "enabled_in_config": false}}, "record": {"enabled": false, "expire_interval": 60, "continuous": {"days": 0}, "motion": {"days": 0}, "detections": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "alerts": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "export": {"hwaccel_args": "preset-vaapi", "max_concurrent": 3}, "preview": {"quality": "medium"}, "enabled_in_config": false}, "review": {"alerts": {"enabled": true, "labels": ["person", "car"], "required_zones": [], "enabled_in_config": true, "cutoff_time": 40}, "detections": {"enabled": true, "labels": null, "required_zones": [], "cutoff_time": 30, "enabled_in_config": true}, "genai": {"enabled": false, "alerts": true, "detections": false, "image_source": "preview", "additional_concerns": [], "debug_save_thumbnails": false, "enabled_in_config": false, "preferred_language": null, "activity_context_prompt": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is."}}, "semantic_search": {"triggers": {}}, "snapshots": {"enabled": false, "timestamp": false, "bounding_box": true, "crop": false, "required_zones": [], "height": null, "retain": {"default": 10, "mode": "motion", "objects": {}}, "quality": 60}, "timestamp_style": {"position": "tl", "format": "%m/%d/%Y %H:%M:%S", "color": {"red": 255, "green": 255, "blue": 255}, "thickness": 2, "effect": null}, "best_image_timeout": 60, "mqtt": {"enabled": true, "timestamp": true, "bounding_box": true, "crop": true, "height": 270, "required_zones": [], "quality": 70}, "notifications": {"enabled": false, "email": null, "cooldown": 0, "enabled_in_config": false}, "onvif": {"host": "", "port": 8000, "user": null, "password": null, "tls_insecure": false, "profile": null, "autotracking": {"enabled": false, "calibrate_on_startup": false, "zooming": "disabled", "zoom_factor": 0.3, "track": ["person"], "required_zones": [], "return_preset": "home", "timeout": 10, "movement_weights": [], "enabled_in_config": false}, "ignore_time_mismatch": false}, "type": "generic", "ui": {"order": 0, "dashboard": true, "review": true}, "webui_url": null, "profiles": {}, "zones": {}, "enabled_in_config": true}}, "audio": {"enabled": false, "max_not_heard": 30, "min_volume": 500, "listen": ["bark", "fire_alarm", "speech", "yell"], "filters": {"bark": {"threshold": 0.8}, "fire_alarm": {"threshold": 0.8}, "speech": {"threshold": 0.8}, "yell": {"threshold": 0.8}}, "enabled_in_config": null, "num_threads": 2}, "birdseye": {"enabled": true, "mode": "objects", "restream": false, "width": 1280, "height": 720, "quality": 8, "inactivity_threshold": 30, "layout": {"scaling_factor": 2.0, "max_cameras": null}, "idle_heartbeat_fps": 0.0}, "detect": {"enabled": false, "height": null, "width": null, "fps": 5, "min_initialized": null, "max_disappeared": null, "stationary": {"interval": null, "threshold": null, "max_frames": {"default": null, "objects": {}}, "classifier": true}, "annotation_offset": 0}, "ffmpeg": {"path": "default", "global_args": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "hwaccel_args": "preset-vaapi", "input_args": "preset-rtsp-generic", "output_args": {"detect": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "record": "preset-record-generic-audio-aac"}, "retry_interval": 10.0, "apple_compatibility": false, "gpu": 0}, "live": {"streams": [], "height": 720, "quality": 8}, "motion": null, "objects": {"track": ["person"], "filters": {"royal_mail": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "an_post": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "ups": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "postnord": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "dhl": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "postnl": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "usps": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "face": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "license_plate": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "dpd": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "amazon": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "fedex": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "canada_post": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "nzpost": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "gls": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "purolator": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}}, "mask": {}, "genai": {"enabled": false, "use_snapshot": false, "prompt": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "object_prompts": {}, "objects": [], "required_zones": [], "debug_save_thumbnails": false, "send_triggers": {"tracked_object_end": true, "after_significant_updates": null}, "enabled_in_config": null}}, "record": {"enabled": false, "expire_interval": 60, "continuous": {"days": 0}, "motion": {"days": 0}, "detections": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "alerts": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "export": {"hwaccel_args": "preset-vaapi", "max_concurrent": 3}, "preview": {"quality": "medium"}, "enabled_in_config": null}, "review": {"alerts": {"enabled": true, "labels": ["person", "car"], "required_zones": [], "enabled_in_config": null, "cutoff_time": 40}, "detections": {"enabled": true, "labels": null, "required_zones": [], "cutoff_time": 30, "enabled_in_config": null}, "genai": {"enabled": false, "alerts": true, "detections": false, "image_source": "preview", "additional_concerns": [], "debug_save_thumbnails": false, "enabled_in_config": null, "preferred_language": null, "activity_context_prompt": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is."}}, "snapshots": {"enabled": false, "timestamp": false, "bounding_box": true, "crop": false, "required_zones": [], "height": null, "retain": {"default": 10, "mode": "motion", "objects": {}}, "quality": 60}, "timestamp_style": {"position": "tl", "format": "%m/%d/%Y %H:%M:%S", "color": {"red": 255, "green": 255, "blue": 255}, "thickness": 2, "effect": null}, "audio_transcription": {"enabled": false, "language": "en", "device": "CPU", "model_size": "small", "live_enabled": false}, "classification": {"bird": {"enabled": false, "threshold": 0.9}, "custom": {}}, "semantic_search": {"enabled": false, "reindex": false, "model": "jinav1", "model_size": "small", "device": null}, "face_recognition": {"enabled": false, "model_size": "small", "unknown_score": 0.8, "detection_threshold": 0.7, "recognition_threshold": 0.9, "min_area": 750, "min_faces": 1, "save_attempts": 200, "blur_confidence_filter": true, "device": null}, "lpr": {"enabled": false, "model_size": "small", "detection_threshold": 0.7, "min_area": 1000, "recognition_threshold": 0.9, "min_plate_length": 4, "format": null, "match_distance": 1, "known_plates": {}, "enhancement": 0, "debug_save_plates": false, "device": null, "replace_rules": []}, "camera_groups": {"default": {"cameras": ["front_door", "backyard", "garage"], "icon": "generic", "order": 0}, "outdoor": {"cameras": ["front_door", "backyard"], "icon": "generic", "order": 1}}, "profiles": {}}
\ No newline at end of file
diff --git a/web/e2e/fixtures/mock-data/debug-replay.ts b/web/e2e/fixtures/mock-data/debug-replay.ts
new file mode 100644
index 0000000000..9b9d2c650d
--- /dev/null
+++ b/web/e2e/fixtures/mock-data/debug-replay.ts
@@ -0,0 +1,54 @@
+/**
+ * Debug replay status factory.
+ *
+ * The Replay page polls /api/debug_replay/status every 1s via SWR.
+ * The no-session state shows an empty state; the active state
+ * renders the live camera image + debug toggles + objects/messages
+ * tabs. Used by replay.spec.ts.
+ */
+
+export type DebugReplayStatus = {
+ active: boolean;
+ replay_camera: string | null;
+ source_camera: string | null;
+ start_time: number | null;
+ end_time: number | null;
+ live_ready: boolean;
+};
+
+export function noSessionStatus(): DebugReplayStatus {
+ return {
+ active: false,
+ replay_camera: null,
+ source_camera: null,
+ start_time: null,
+ end_time: null,
+ live_ready: false,
+ };
+}
+
+export function activeSessionStatus(
+ opts: {
+ camera?: string;
+ sourceCamera?: string;
+ startTime?: number;
+ endTime?: number;
+ liveReady?: boolean;
+ } = {},
+): DebugReplayStatus {
+ const {
+ camera = "front_door",
+ sourceCamera = "front_door",
+ startTime = Date.now() / 1000 - 3600,
+ endTime = Date.now() / 1000 - 1800,
+ liveReady = true,
+ } = opts;
+ return {
+ active: true,
+ replay_camera: camera,
+ source_camera: sourceCamera,
+ start_time: startTime,
+ end_time: endTime,
+ live_ready: liveReady,
+ };
+}
diff --git a/web/e2e/fixtures/mock-data/events.json b/web/e2e/fixtures/mock-data/events.json
index a50c1d7bc7..fa698a9b41 100644
--- a/web/e2e/fixtures/mock-data/events.json
+++ b/web/e2e/fixtures/mock-data/events.json
@@ -1 +1 @@
-[{"id": "event-person-001", "label": "person", "sub_label": null, "camera": "front_door", "start_time": 1775487131.3863528, "end_time": 1775487161.3863528, "false_positive": false, "zones": ["front_yard"], "thumbnail": null, "has_clip": true, "has_snapshot": true, "retain_indefinitely": false, "plus_id": null, "model_hash": "abc123", "detector_type": "cpu", "model_type": "ssd", "data": {"top_score": 0.92, "score": 0.92, "region": [0.1, 0.1, 0.5, 0.8], "box": [0.2, 0.15, 0.45, 0.75], "area": 0.18, "ratio": 0.6, "type": "object", "description": "A person walking toward the front door", "average_estimated_speed": 1.2, "velocity_angle": 45.0, "path_data": [[[0.2, 0.5], 0.0], [[0.3, 0.5], 1.0]]}}, {"id": "event-car-001", "label": "car", "sub_label": null, "camera": "backyard", "start_time": 1775483531.3863528, "end_time": 1775483576.3863528, "false_positive": false, "zones": ["driveway"], "thumbnail": null, "has_clip": true, "has_snapshot": true, "retain_indefinitely": false, "plus_id": null, "model_hash": "def456", "detector_type": "cpu", "model_type": "ssd", "data": {"top_score": 0.87, "score": 0.87, "region": [0.3, 0.2, 0.9, 0.7], "box": [0.35, 0.25, 0.85, 0.65], "area": 0.2, "ratio": 1.25, "type": "object", "description": "A car parked in the driveway", "average_estimated_speed": 0.0, "velocity_angle": 0.0, "path_data": []}}, {"id": "event-person-002", "label": "person", "sub_label": null, "camera": "garage", "start_time": 1775479931.3863528, "end_time": 1775479951.3863528, "false_positive": false, "zones": [], "thumbnail": null, "has_clip": false, "has_snapshot": true, "retain_indefinitely": false, "plus_id": null, "model_hash": "ghi789", "detector_type": "cpu", "model_type": "ssd", "data": {"top_score": 0.78, "score": 0.78, "region": [0.0, 0.0, 0.6, 0.9], "box": [0.1, 0.05, 0.5, 0.85], "area": 0.32, "ratio": 0.5, "type": "object", "description": null, "average_estimated_speed": 0.5, "velocity_angle": 90.0, "path_data": [[[0.1, 0.4], 0.0]]}}]
\ No newline at end of file
+[{"id": "event-person-001", "label": "person", "sub_label": null, "camera": "front_door", "start_time": 1780677009.365581, "end_time": 1780677039.365581, "false_positive": false, "zones": ["front_yard"], "thumbnail": null, "has_clip": true, "has_snapshot": true, "retain_indefinitely": false, "plus_id": null, "model_hash": "abc123", "detector_type": "cpu", "model_type": "ssd", "data": {"top_score": 0.92, "score": 0.92, "region": [0.1, 0.1, 0.5, 0.8], "box": [0.2, 0.15, 0.45, 0.75], "area": 0.18, "ratio": 0.6, "type": "object", "description": "A person walking toward the front door", "average_estimated_speed": 1.2, "velocity_angle": 45.0, "path_data": [[[0.2, 0.5], 0.0], [[0.3, 0.5], 1.0]]}}, {"id": "event-car-001", "label": "car", "sub_label": null, "camera": "backyard", "start_time": 1780673409.365581, "end_time": 1780673454.365581, "false_positive": false, "zones": ["driveway"], "thumbnail": null, "has_clip": true, "has_snapshot": true, "retain_indefinitely": false, "plus_id": null, "model_hash": "def456", "detector_type": "cpu", "model_type": "ssd", "data": {"top_score": 0.87, "score": 0.87, "region": [0.3, 0.2, 0.9, 0.7], "box": [0.35, 0.25, 0.85, 0.65], "area": 0.2, "ratio": 1.25, "type": "object", "description": "A car parked in the driveway", "average_estimated_speed": 0.0, "velocity_angle": 0.0, "path_data": []}}, {"id": "event-person-002", "label": "person", "sub_label": null, "camera": "garage", "start_time": 1780669809.365581, "end_time": 1780669829.365581, "false_positive": false, "zones": [], "thumbnail": null, "has_clip": false, "has_snapshot": true, "retain_indefinitely": false, "plus_id": null, "model_hash": "ghi789", "detector_type": "cpu", "model_type": "ssd", "data": {"top_score": 0.78, "score": 0.78, "region": [0.0, 0.0, 0.6, 0.9], "box": [0.1, 0.05, 0.5, 0.85], "area": 0.32, "ratio": 0.5, "type": "object", "description": null, "average_estimated_speed": 0.5, "velocity_angle": 90.0, "path_data": [[[0.1, 0.4], 0.0]]}}]
\ No newline at end of file
diff --git a/web/e2e/fixtures/mock-data/exports.json b/web/e2e/fixtures/mock-data/exports.json
index 9af04f45a8..7e6c787088 100644
--- a/web/e2e/fixtures/mock-data/exports.json
+++ b/web/e2e/fixtures/mock-data/exports.json
@@ -1 +1 @@
-[{"id": "export-001", "camera": "front_door", "name": "Front Door - Person Alert", "date": 1775490731.3863528, "video_path": "/exports/export-001.mp4", "thumb_path": "/exports/export-001-thumb.jpg", "in_progress": false, "export_case_id": null}, {"id": "export-002", "camera": "backyard", "name": "Backyard - Car Detection", "date": 1775483531.3863528, "video_path": "/exports/export-002.mp4", "thumb_path": "/exports/export-002-thumb.jpg", "in_progress": false, "export_case_id": "case-001"}, {"id": "export-003", "camera": "garage", "name": "Garage - In Progress", "date": 1775492531.3863528, "video_path": "/exports/export-003.mp4", "thumb_path": "/exports/export-003-thumb.jpg", "in_progress": true, "export_case_id": null}]
\ No newline at end of file
+[{"id": "export-001", "camera": "front_door", "name": "Front Door - Person Alert", "date": 1780680609.365581, "video_path": "/exports/export-001.mp4", "thumb_path": "/exports/export-001-thumb.jpg", "in_progress": false, "export_case_id": null}, {"id": "export-002", "camera": "backyard", "name": "Backyard - Car Detection", "date": 1780673409.365581, "video_path": "/exports/export-002.mp4", "thumb_path": "/exports/export-002-thumb.jpg", "in_progress": false, "export_case_id": "case-001"}, {"id": "export-003", "camera": "garage", "name": "Garage - In Progress", "date": 1780682409.365581, "video_path": "/exports/export-003.mp4", "thumb_path": "/exports/export-003-thumb.jpg", "in_progress": true, "export_case_id": null}]
\ No newline at end of file
diff --git a/web/e2e/fixtures/mock-data/faces.ts b/web/e2e/fixtures/mock-data/faces.ts
new file mode 100644
index 0000000000..ed2f944cb1
--- /dev/null
+++ b/web/e2e/fixtures/mock-data/faces.ts
@@ -0,0 +1,45 @@
+/**
+ * Face library factories.
+ *
+ * The /api/faces endpoint returns a record keyed by collection name
+ * with the list of face image filenames. Grouped training attempts
+ * live under the "train" key with filenames of the form
+ * `${event_id}-${timestamp}-${label}-${score}.webp`.
+ *
+ * Used by face-library.spec.ts and chat.spec.ts (attachment chip).
+ */
+
+export type FacesMock = Record;
+
+export function basicFacesMock(): FacesMock {
+ return {
+ alice: ["alice-1.webp", "alice-2.webp"],
+ bob: ["bob-1.webp"],
+ charlie: ["charlie-1.webp"],
+ };
+}
+
+export function emptyFacesMock(): FacesMock {
+ return {};
+}
+
+/**
+ * Adds a grouped recent-recognition training attempt to an existing
+ * faces mock. The grouping key on the backend is the event id — so
+ * images with the same event-id prefix render as one dialog-able card.
+ */
+export function withGroupedTrainingAttempt(
+ base: FacesMock,
+ opts: {
+ eventId: string;
+ attempts: Array<{ timestamp: number; label: string; score: number }>;
+ },
+): FacesMock {
+ const trainImages = opts.attempts.map(
+ (a) => `${opts.eventId}-${a.timestamp}-${a.label}-${a.score}.webp`,
+ );
+ return {
+ ...base,
+ train: [...(base.train ?? []), ...trainImages],
+ };
+}
diff --git a/web/e2e/fixtures/mock-data/generate-mock-data.py b/web/e2e/fixtures/mock-data/generate-mock-data.py
index aa96494b92..bba488d3d9 100644
--- a/web/e2e/fixtures/mock-data/generate-mock-data.py
+++ b/web/e2e/fixtures/mock-data/generate-mock-data.py
@@ -111,6 +111,18 @@ def generate_config():
return snapshot
+def generate_config_schema():
+ """Generate the JSON Schema for FrigateConfig from the backend model.
+
+ This is what the app fetches from /api/config/schema.json to drive the
+ RJSF-based config form. Generating it here keeps the e2e fixture in sync
+ with the backend whenever config models change.
+ """
+ from frigate.config import FrigateConfig
+
+ return FrigateConfig.model_json_schema()
+
+
def generate_reviews():
"""Generate ReviewSegmentResponse[] validated against Pydantic + Peewee."""
from frigate.api.defs.response.review_response import ReviewSegmentResponse
@@ -411,6 +423,7 @@ def main():
print()
write_json("config-snapshot.json", generate_config())
+ write_json("config-schema.json", generate_config_schema())
write_json("reviews.json", generate_reviews())
write_json("events.json", generate_events())
write_json("exports.json", generate_exports())
diff --git a/web/e2e/fixtures/mock-data/review-summary.json b/web/e2e/fixtures/mock-data/review-summary.json
index ba54df37c9..bb3afc2ea3 100644
--- a/web/e2e/fixtures/mock-data/review-summary.json
+++ b/web/e2e/fixtures/mock-data/review-summary.json
@@ -1 +1 @@
-{"2026-04-06": {"day": "2026-04-06", "reviewed_alert": 1, "reviewed_detection": 0, "total_alert": 2, "total_detection": 2}, "2026-04-05": {"day": "2026-04-05", "reviewed_alert": 3, "reviewed_detection": 2, "total_alert": 3, "total_detection": 4}}
\ No newline at end of file
+{"2026-06-05": {"day": "2026-06-05", "reviewed_alert": 1, "reviewed_detection": 0, "total_alert": 2, "total_detection": 2}, "2026-06-04": {"day": "2026-06-04", "reviewed_alert": 3, "reviewed_detection": 2, "total_alert": 3, "total_detection": 4}}
\ No newline at end of file
diff --git a/web/e2e/fixtures/mock-data/reviews.json b/web/e2e/fixtures/mock-data/reviews.json
index 4930f01598..0b60850cfa 100644
--- a/web/e2e/fixtures/mock-data/reviews.json
+++ b/web/e2e/fixtures/mock-data/reviews.json
@@ -1 +1 @@
-[{"id": "review-alert-001", "camera": "front_door", "start_time": "2026-04-06T09:52:11.386353", "end_time": "2026-04-06T09:52:41.386353", "has_been_reviewed": false, "severity": "alert", "thumb_path": "/clips/front_door/review-alert-001-thumb.jpg", "data": {"audio": [], "detections": ["person-abc123"], "objects": ["person"], "sub_labels": [], "significant_motion_areas": [], "zones": ["front_yard"]}}, {"id": "review-alert-002", "camera": "backyard", "start_time": "2026-04-06T08:52:11.386353", "end_time": "2026-04-06T08:52:56.386353", "has_been_reviewed": true, "severity": "alert", "thumb_path": "/clips/backyard/review-alert-002-thumb.jpg", "data": {"audio": [], "detections": ["car-def456"], "objects": ["car"], "sub_labels": [], "significant_motion_areas": [], "zones": ["driveway"]}}, {"id": "review-detect-001", "camera": "garage", "start_time": "2026-04-06T07:52:11.386353", "end_time": "2026-04-06T07:52:31.386353", "has_been_reviewed": false, "severity": "detection", "thumb_path": "/clips/garage/review-detect-001-thumb.jpg", "data": {"audio": [], "detections": ["person-ghi789"], "objects": ["person"], "sub_labels": [], "significant_motion_areas": [], "zones": []}}, {"id": "review-detect-002", "camera": "front_door", "start_time": "2026-04-06T06:52:11.386353", "end_time": "2026-04-06T06:52:26.386353", "has_been_reviewed": false, "severity": "detection", "thumb_path": "/clips/front_door/review-detect-002-thumb.jpg", "data": {"audio": [], "detections": ["car-jkl012"], "objects": ["car"], "sub_labels": [], "significant_motion_areas": [], "zones": ["front_yard"]}}]
\ No newline at end of file
+[{"id": "review-alert-001", "camera": "front_door", "start_time": "2026-06-05T11:30:09.365581", "end_time": "2026-06-05T11:30:39.365581", "has_been_reviewed": false, "severity": "alert", "thumb_path": "/clips/front_door/review-alert-001-thumb.jpg", "data": {"audio": [], "detections": ["person-abc123"], "objects": ["person"], "sub_labels": [], "significant_motion_areas": [], "zones": ["front_yard"]}}, {"id": "review-alert-002", "camera": "backyard", "start_time": "2026-06-05T10:30:09.365581", "end_time": "2026-06-05T10:30:54.365581", "has_been_reviewed": true, "severity": "alert", "thumb_path": "/clips/backyard/review-alert-002-thumb.jpg", "data": {"audio": [], "detections": ["car-def456"], "objects": ["car"], "sub_labels": [], "significant_motion_areas": [], "zones": ["driveway"]}}, {"id": "review-detect-001", "camera": "garage", "start_time": "2026-06-05T09:30:09.365581", "end_time": "2026-06-05T09:30:29.365581", "has_been_reviewed": false, "severity": "detection", "thumb_path": "/clips/garage/review-detect-001-thumb.jpg", "data": {"audio": [], "detections": ["person-ghi789"], "objects": ["person"], "sub_labels": [], "significant_motion_areas": [], "zones": []}}, {"id": "review-detect-002", "camera": "front_door", "start_time": "2026-06-05T08:30:09.365581", "end_time": "2026-06-05T08:30:24.365581", "has_been_reviewed": false, "severity": "detection", "thumb_path": "/clips/front_door/review-detect-002-thumb.jpg", "data": {"audio": [], "detections": ["car-jkl012"], "objects": ["car"], "sub_labels": [], "significant_motion_areas": [], "zones": ["front_yard"]}}]
\ No newline at end of file
diff --git a/web/e2e/helpers/api-mocker.ts b/web/e2e/helpers/api-mocker.ts
index 52f10d64b4..e1a191fe0b 100644
--- a/web/e2e/helpers/api-mocker.ts
+++ b/web/e2e/helpers/api-mocker.ts
@@ -113,11 +113,12 @@ export class ApiMocker {
route.fulfill({ json: [] }),
);
- // Sub-labels and attributes (for explore filters)
- await this.page.route("**/api/sub_labels", (route) =>
+ // Sub-labels and attributes (for explore filters).
+ // Use trailing ** so query-string variants (e.g. ?split_joined=1) match.
+ await this.page.route("**/api/sub_labels**", (route) =>
route.fulfill({ json: [] }),
);
- await this.page.route("**/api/labels", (route) =>
+ await this.page.route("**/api/labels**", (route) =>
route.fulfill({ json: ["person", "car"] }),
);
await this.page.route("**/api/*/attributes", (route) =>
diff --git a/web/e2e/helpers/clipboard.ts b/web/e2e/helpers/clipboard.ts
new file mode 100644
index 0000000000..9099073b32
--- /dev/null
+++ b/web/e2e/helpers/clipboard.ts
@@ -0,0 +1,25 @@
+/**
+ * Clipboard read helper for e2e tests.
+ *
+ * Clipboard API requires a browser permission in headless mode.
+ * grantClipboardPermissions() must be called before any readClipboard()
+ * attempt. Used by logs.spec.ts (Copy button) and config-editor.spec.ts
+ * (Copy button).
+ */
+
+import type { BrowserContext, Page } from "@playwright/test";
+
+/**
+ * Grant clipboard-read + clipboard-write permissions on the context.
+ * Call in beforeEach or at the top of a test before the Copy action.
+ */
+export async function grantClipboardPermissions(
+ context: BrowserContext,
+): Promise {
+ await context.grantPermissions(["clipboard-read", "clipboard-write"]);
+}
+
+/** Read the current clipboard contents via the page's navigator.clipboard. */
+export async function readClipboard(page: Page): Promise {
+ return page.evaluate(async () => await navigator.clipboard.readText());
+}
diff --git a/web/e2e/helpers/monaco.ts b/web/e2e/helpers/monaco.ts
new file mode 100644
index 0000000000..6e9bcd871a
--- /dev/null
+++ b/web/e2e/helpers/monaco.ts
@@ -0,0 +1,58 @@
+/**
+ * Monaco editor DOM helpers for e2e tests.
+ *
+ * Monaco is imported as a module-local object in the app and is NOT
+ * exposed on window; we drive + read through the rendered DOM and
+ * keyboard instead. Used by config-editor.spec.ts only.
+ */
+
+import { expect, type Page } from "@playwright/test";
+
+/**
+ * Returns the current visible text of the first Monaco editor on the
+ * page. Monaco virtualizes long files — this reads only the rendered
+ * lines. For short configs (our mocks) that's the full content.
+ */
+export async function getMonacoVisibleText(page: Page): Promise {
+ return page.locator(".monaco-editor .view-lines").first().innerText();
+}
+
+/**
+ * Focus the editor and replace its full content with `value` via
+ * keyboard. Uses Ctrl+A (Cmd+A on macOS Playwright is equivalent)
+ * + Delete + type. Works cross-platform because Playwright normalizes.
+ */
+export async function replaceMonacoValue(
+ page: Page,
+ value: string,
+): Promise {
+ const editor = page.locator(".monaco-editor").first();
+ await editor.click();
+ await page.keyboard.press("ControlOrMeta+A");
+ await page.keyboard.press("Delete");
+ // Use `type` with zero delay — Monaco handles each key.
+ await page.keyboard.type(value, { delay: 0 });
+}
+
+/**
+ * Returns true when the editor shows at least one error-severity
+ * marker. Monaco renders error underlines as `.squiggly-error` in
+ * the `.view-overlays` layer.
+ */
+export async function hasErrorMarkers(page: Page): Promise {
+ const count = await page.locator(".monaco-editor .squiggly-error").count();
+ return count > 0;
+}
+
+/**
+ * Poll until an error marker appears. Monaco schedules marker updates
+ * asynchronously after content changes (debounce + schema validation).
+ */
+export async function waitForErrorMarker(
+ page: Page,
+ timeoutMs: number = 10_000,
+): Promise {
+ await expect
+ .poll(() => hasErrorMarkers(page), { timeout: timeoutMs })
+ .toBe(true);
+}
diff --git a/web/e2e/helpers/overlay-interaction.ts b/web/e2e/helpers/overlay-interaction.ts
new file mode 100644
index 0000000000..81a01d8415
--- /dev/null
+++ b/web/e2e/helpers/overlay-interaction.ts
@@ -0,0 +1,41 @@
+/**
+ * Overlay interaction helpers for Radix-based UI tests.
+ *
+ * These helpers exist to guard the class of bugs fixed by de-duping
+ * `@radix-ui/react-dismissable-layer` across the tree: body pointer-events
+ * getting stuck, dropdown typeahead breaking, tooltips re-popping after a
+ * dropdown closes, and related nested-overlay regressions.
+ */
+
+import { expect, type Page } from "@playwright/test";
+
+/**
+ * Assert that `` is interactive (no stuck `pointer-events: none`).
+ *
+ * Call after closing any overlay. This is the fast secondary assertion —
+ * test specs should also assert a user-visible behavior like "a button
+ * responded to a click" so the test fails on meaningful breakage rather
+ * than just a CSS invariant.
+ */
+export async function expectBodyInteractive(page: Page) {
+ const stuck = await page.evaluate(
+ () => document.body.style.pointerEvents === "none",
+ );
+ expect(stuck, "body.style.pointer-events stuck after overlay close").toBe(
+ false,
+ );
+}
+
+/**
+ * Wait until the `` is no longer marked with `pointer-events: none`.
+ *
+ * Useful right after closing an overlay when Radix's cleanup runs in the
+ * next frame. Throws if the style does not clear within `timeoutMs`.
+ */
+export async function waitForBodyInteractive(page: Page, timeoutMs = 2000) {
+ await page.waitForFunction(
+ () => document.body.style.pointerEvents !== "none",
+ null,
+ { timeout: timeoutMs },
+ );
+}
diff --git a/web/e2e/helpers/ws-frames.ts b/web/e2e/helpers/ws-frames.ts
new file mode 100644
index 0000000000..d46376d871
--- /dev/null
+++ b/web/e2e/helpers/ws-frames.ts
@@ -0,0 +1,65 @@
+/**
+ * WebSocket frame capture helper.
+ *
+ * The ws-mocker intercepts the /ws route, so Playwright's page-level
+ * `websocket` event never fires. This helper patches client-side
+ * WebSocket.prototype.send before any app code runs and mirrors every
+ * sent frame into a window-level array the test can read back.
+ *
+ * Used by live.spec.ts (feature toggles, PTZ preset commands) and
+ * config-editor.spec.ts (restart command via useRestart).
+ */
+
+import { expect, type Page } from "@playwright/test";
+
+export type CapturedFrame = string;
+
+declare global {
+ interface Window {
+ __sentWsFrames: CapturedFrame[];
+ }
+}
+
+/**
+ * Patch WebSocket.prototype.send to capture every outbound frame into
+ * window.__sentWsFrames. Must be called BEFORE page.goto().
+ */
+export async function installWsFrameCapture(page: Page): Promise {
+ await page.addInitScript(() => {
+ window.__sentWsFrames = [];
+ const origSend = WebSocket.prototype.send;
+ WebSocket.prototype.send = function (data) {
+ try {
+ window.__sentWsFrames.push(
+ typeof data === "string" ? data : "(binary)",
+ );
+ } catch {
+ // ignore — best-effort tracing
+ }
+ return origSend.call(this, data);
+ };
+ });
+}
+
+/** Read all captured frames at call time. */
+export async function readWsFrames(page: Page): Promise {
+ return page.evaluate(() => window.__sentWsFrames ?? []);
+}
+
+/**
+ * Poll until at least one captured frame matches the predicate.
+ * Throws via expect if the frame never arrives within timeout.
+ */
+export async function waitForWsFrame(
+ page: Page,
+ matcher: (frame: CapturedFrame) => boolean,
+ opts: { timeout?: number; message?: string } = {},
+): Promise {
+ const { timeout = 2_000, message } = opts;
+ await expect
+ .poll(async () => (await readWsFrames(page)).some(matcher), {
+ timeout,
+ message,
+ })
+ .toBe(true);
+}
diff --git a/web/e2e/helpers/ws-mocker.ts b/web/e2e/helpers/ws-mocker.ts
index 6b29b76396..03db9f7410 100644
--- a/web/e2e/helpers/ws-mocker.ts
+++ b/web/e2e/helpers/ws-mocker.ts
@@ -79,7 +79,20 @@ export class WsMocker {
this.send("model_state", JSON.stringify({}));
}
if (data.topic === "embeddingsReindexProgress") {
- this.send("embeddings_reindex_progress", JSON.stringify(null));
+ // Send a completed reindex state so Explore renders when
+ // semantic_search.enabled is true. A null payload leaves the page
+ // in a permanent loading spinner because !reindexState is truthy.
+ this.send(
+ "embeddings_reindex_progress",
+ JSON.stringify({
+ status: "completed",
+ processed_objects: 0,
+ total_objects: 0,
+ thumbnails: 0,
+ descriptions: 0,
+ time_remaining: null,
+ }),
+ );
}
if (data.topic === "birdseyeLayout") {
this.send("birdseye_layout", JSON.stringify(null));
diff --git a/web/e2e/pages/live.page.ts b/web/e2e/pages/live.page.ts
new file mode 100644
index 0000000000..814064944b
--- /dev/null
+++ b/web/e2e/pages/live.page.ts
@@ -0,0 +1,55 @@
+/**
+ * Live dashboard + single-camera page object.
+ *
+ * Encapsulates selectors and viewport-conditional openers for the
+ * Live route. Does NOT own assertions — specs call expect on the
+ * locators returned from these getters.
+ */
+
+import type { Locator, Page } from "@playwright/test";
+import { BasePage } from "./base.page";
+
+export class LivePage extends BasePage {
+ constructor(page: Page, isDesktop: boolean) {
+ super(page, isDesktop);
+ }
+
+ /** The camera card wrapper on the dashboard, keyed by camera name. */
+ cameraCard(name: string): Locator {
+ return this.page.locator(`[data-camera='${name}']`);
+ }
+
+ /** Back button on the single-camera view header (desktop text). */
+ get backButton(): Locator {
+ return this.page.getByText("Back", { exact: true });
+ }
+
+ /** History button on the single-camera view header (desktop text). */
+ get historyButton(): Locator {
+ return this.page.getByText("History", { exact: true });
+ }
+
+ /** All CameraFeatureToggle elements (active + inactive). */
+ get featureToggles(): Locator {
+ // Use div selector to exclude NavItem anchor elements that share the same classes.
+ return this.page.locator(
+ "div.flex.flex-col.items-center.justify-center.bg-selected, div.flex.flex-col.items-center.justify-center.bg-secondary",
+ );
+ }
+
+ /** Only the active (bg-selected) feature toggles. */
+ get activeFeatureToggles(): Locator {
+ // Use div selector to exclude NavItem anchor elements that share the same classes.
+ return this.page.locator(
+ "div.flex.flex-col.items-center.justify-center.bg-selected",
+ );
+ }
+
+ /** Open the right-click context menu on a camera card (desktop only). */
+ async openContextMenuOn(cameraName: string): Promise {
+ await this.cameraCard(cameraName).first().click({ button: "right" });
+ return this.page
+ .locator('[role="menu"], [data-radix-menu-content]')
+ .first();
+ }
+}
diff --git a/web/e2e/pages/review.page.ts b/web/e2e/pages/review.page.ts
new file mode 100644
index 0000000000..6d9cfb7c24
--- /dev/null
+++ b/web/e2e/pages/review.page.ts
@@ -0,0 +1,52 @@
+/**
+ * Review/events page object.
+ *
+ * Encapsulates severity tab, filter bar, calendar, and mobile filter
+ * drawer selectors. Does NOT own assertions.
+ */
+
+import type { Locator, Page } from "@playwright/test";
+import { BasePage } from "./base.page";
+
+export class ReviewPage extends BasePage {
+ constructor(page: Page, isDesktop: boolean) {
+ super(page, isDesktop);
+ }
+
+ get alertsTab(): Locator {
+ return this.page.getByLabel("Alerts");
+ }
+
+ get detectionsTab(): Locator {
+ return this.page.getByLabel("Detections");
+ }
+
+ get motionTab(): Locator {
+ return this.page.getByRole("radio", { name: "Motion" });
+ }
+
+ get camerasFilterTrigger(): Locator {
+ return this.page.getByRole("button", { name: /cameras/i }).first();
+ }
+
+ get calendarTrigger(): Locator {
+ return this.page.getByRole("button", { name: /24 hours|calendar|date/i });
+ }
+
+ get showReviewedToggle(): Locator {
+ return this.page.getByRole("button", { name: /reviewed/i });
+ }
+
+ get reviewItems(): Locator {
+ return this.page.locator(".review-item");
+ }
+
+ /** The filter popover content (desktop) or drawer (mobile). */
+ get filterOverlay(): Locator {
+ return this.page
+ .locator(
+ '[data-radix-popper-content-wrapper], [role="dialog"], [data-vaul-drawer]',
+ )
+ .first();
+ }
+}
diff --git a/web/e2e/scripts/lint-specs.mjs b/web/e2e/scripts/lint-specs.mjs
index 4724e99bb9..e4046869df 100644
--- a/web/e2e/scripts/lint-specs.mjs
+++ b/web/e2e/scripts/lint-specs.mjs
@@ -14,10 +14,6 @@
*
* @mobile rule: every .spec.ts under specs/ (not specs/_meta/) must
* contain at least one test title or describe with the substring "@mobile".
- *
- * Specs in PENDING_REWRITE are exempt from all rules until they are
- * rewritten with proper assertions and mobile coverage. Remove each
- * entry when its spec is updated.
*/
import { readFileSync, readdirSync, statSync } from "node:fs";
@@ -28,24 +24,6 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
const SPECS_DIR = resolve(__dirname, "..", "specs");
const META_PREFIX = resolve(SPECS_DIR, "_meta");
-// Specs exempt from lint rules until they are rewritten with proper
-// assertions and mobile coverage. Remove each entry when its spec is updated.
-const PENDING_REWRITE = new Set([
- "auth.spec.ts",
- "chat.spec.ts",
- "classification.spec.ts",
- "config-editor.spec.ts",
- "explore.spec.ts",
- "export.spec.ts",
- "face-library.spec.ts",
- "live.spec.ts",
- "logs.spec.ts",
- "navigation.spec.ts",
- "replay.spec.ts",
- "review.spec.ts",
- "system.spec.ts",
-]);
-
const BANNED_PATTERNS = [
{
name: "page.waitForTimeout",
@@ -62,14 +40,12 @@ const BANNED_PATTERNS = [
{
name: "conditional count() assertion",
regex: /\bif\s*\(\s*\(?\s*await\s+[^)]*\.count\s*\(\s*\)\s*\)?\s*[><=!]/,
- advice:
- "Assertions must be unconditional. Use expect(...).toHaveCount(n).",
+ advice: "Assertions must be unconditional. Use expect(...).toHaveCount(n).",
},
{
name: "vacuous textContent length assertion",
regex: /expect\([^)]*\.length\)\.toBeGreaterThan\(0\)/,
- advice:
- "Assert specific content, not that some text exists.",
+ advice: "Assert specific content, not that some text exists.",
},
];
@@ -89,8 +65,6 @@ function walk(dir) {
}
function lintFile(file) {
- const basename = file.split("/").pop();
- if (PENDING_REWRITE.has(basename)) return [];
if (file.includes("/specs/settings/")) return [];
const errors = [];
diff --git a/web/e2e/specs/auth.spec.ts b/web/e2e/specs/auth.spec.ts
index 5f58375185..f0326a5d02 100644
--- a/web/e2e/specs/auth.spec.ts
+++ b/web/e2e/specs/auth.spec.ts
@@ -1,147 +1,110 @@
/**
- * Auth and cross-cutting tests -- HIGH tier.
+ * Auth and role tests -- HIGH tier.
*
- * Tests protected route access for admin/viewer roles,
- * access denied page rendering, viewer nav restrictions,
- * and all routes smoke test.
+ * Admin access to /system, /config, /logs; viewer access denied
+ * markers (via i18n heading, not a data-testid we don't own);
+ * viewer nav restrictions; all-routes smoke.
*/
import { test, expect } from "../fixtures/frigate-test";
import { viewerProfile } from "../fixtures/mock-data/profile";
-test.describe("Auth - Admin Access @high", () => {
- test("admin can access /system and sees system tabs", async ({
- frigateApp,
- }) => {
+test.describe("Auth — admin access @high", () => {
+ test("admin /system renders general tab", async ({ frigateApp }) => {
await frigateApp.goto("/system");
- await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
- await frigateApp.page.waitForTimeout(3000);
- // System page should have named tab buttons
await expect(frigateApp.page.getByLabel("Select general")).toBeVisible({
- timeout: 5_000,
+ timeout: 15_000,
});
});
- test("admin can access /config and Monaco editor loads", async ({
- frigateApp,
- }) => {
+ test("admin /config renders Monaco editor", async ({ frigateApp }) => {
await frigateApp.goto("/config");
- await frigateApp.page.waitForTimeout(5000);
- const editor = frigateApp.page.locator(
- ".monaco-editor, [data-keybinding-context]",
- );
- await expect(editor.first()).toBeVisible({ timeout: 10_000 });
+ await expect(
+ frigateApp.page
+ .locator(".monaco-editor, [data-keybinding-context]")
+ .first(),
+ ).toBeVisible({ timeout: 15_000 });
});
- test("admin can access /logs and sees service tabs", async ({
- frigateApp,
- }) => {
+ test("admin /logs renders frigate tab", async ({ frigateApp }) => {
await frigateApp.goto("/logs");
- await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({
timeout: 5_000,
});
});
-
- test("admin sees Classification nav on desktop", async ({ frigateApp }) => {
- if (frigateApp.isMobile) {
- test.skip();
- return;
- }
- await frigateApp.goto("/");
- await expect(
- frigateApp.page.locator('a[href="/classification"]'),
- ).toBeVisible();
- });
});
-test.describe("Auth - Viewer Restrictions @high", () => {
- test("viewer sees Access Denied on /system", async ({ frigateApp, page }) => {
- await frigateApp.installDefaults({ profile: viewerProfile() });
- await page.goto("/system");
- await page.waitForTimeout(2000);
- // Should show "Access Denied" text
- await expect(page.getByText("Access Denied")).toBeVisible({
- timeout: 5_000,
+test.describe("Auth — viewer restrictions @high", () => {
+ for (const path of ["/system", "/config", "/logs"]) {
+ test(`viewer on ${path} sees AccessDenied`, async ({ frigateApp }) => {
+ await frigateApp.installDefaults({ profile: viewerProfile() });
+ await frigateApp.page.goto(path);
+ await frigateApp.page.waitForSelector("#pageRoot", { timeout: 10_000 });
+ await expect(
+ frigateApp.page.getByRole("heading", {
+ level: 2,
+ name: /access denied/i,
+ }),
+ ).toBeVisible({ timeout: 10_000 });
});
+ }
+
+ test("viewer sees cameras on /", async ({ frigateApp }) => {
+ await frigateApp.installDefaults({ profile: viewerProfile() });
+ await frigateApp.page.goto("/");
+ await expect(
+ frigateApp.page.locator("[data-camera='front_door']"),
+ ).toBeVisible({ timeout: 10_000 });
});
- test("viewer sees Access Denied on /config", async ({ frigateApp, page }) => {
+ test("viewer sees severity tabs on /review", async ({ frigateApp }) => {
await frigateApp.installDefaults({ profile: viewerProfile() });
- await page.goto("/config");
- await page.waitForTimeout(2000);
- await expect(page.getByText("Access Denied")).toBeVisible({
- timeout: 5_000,
- });
- });
-
- test("viewer sees Access Denied on /logs", async ({ frigateApp, page }) => {
- await frigateApp.installDefaults({ profile: viewerProfile() });
- await page.goto("/logs");
- await page.waitForTimeout(2000);
- await expect(page.getByText("Access Denied")).toBeVisible({
- timeout: 5_000,
- });
- });
-
- test("viewer can access Live page and sees cameras", async ({
- frigateApp,
- page,
- }) => {
- await frigateApp.installDefaults({ profile: viewerProfile() });
- await page.goto("/");
- await page.waitForSelector("#pageRoot", { timeout: 10_000 });
- await expect(page.locator("[data-camera='front_door']")).toBeVisible({
+ await frigateApp.page.goto("/review");
+ await expect(frigateApp.page.getByLabel("Alerts")).toBeVisible({
timeout: 10_000,
});
});
- test("viewer can access Review page and sees severity tabs", async ({
+ test("viewer can access all non-admin routes without AccessDenied", async ({
frigateApp,
- page,
- }) => {
- await frigateApp.installDefaults({ profile: viewerProfile() });
- await page.goto("/review");
- await page.waitForSelector("#pageRoot", { timeout: 10_000 });
- await expect(page.getByLabel("Alerts")).toBeVisible({ timeout: 5_000 });
- });
-
- test("viewer can access all main user routes without crash", async ({
- frigateApp,
- page,
}) => {
await frigateApp.installDefaults({ profile: viewerProfile() });
const routes = ["/", "/review", "/explore", "/export", "/settings"];
for (const route of routes) {
- await page.goto(route);
- await page.waitForSelector("#pageRoot", { timeout: 10_000 });
+ await frigateApp.page.goto(route);
+ await frigateApp.page.waitForSelector("#pageRoot", { timeout: 10_000 });
+ await expect(
+ frigateApp.page.getByRole("heading", {
+ level: 2,
+ name: /access denied/i,
+ }),
+ ).toHaveCount(0);
}
});
});
-test.describe("Auth - All Routes Smoke @high", () => {
- test("all user routes render without crash", async ({ frigateApp }) => {
- const routes = ["/", "/review", "/explore", "/export", "/settings"];
- for (const route of routes) {
+test.describe("Auth — viewer nav restrictions (desktop) @high", () => {
+ test.skip(({ frigateApp }) => frigateApp.isMobile, "Sidebar only on desktop");
+
+ test("viewer sidebar hides admin routes", async ({ frigateApp }) => {
+ await frigateApp.installDefaults({ profile: viewerProfile() });
+ await frigateApp.page.goto("/");
+ await frigateApp.page.waitForSelector("#pageRoot", { timeout: 10_000 });
+ for (const href of ["/system", "/config", "/logs"]) {
+ await expect(
+ frigateApp.page.locator(`aside a[href='${href}']`),
+ ).toHaveCount(0);
+ }
+ });
+});
+
+test.describe("Auth — all routes smoke @high @mobile", () => {
+ test("every common route renders #pageRoot", async ({ frigateApp }) => {
+ for (const route of ["/", "/review", "/explore", "/export", "/settings"]) {
await frigateApp.goto(route);
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible({
timeout: 10_000,
});
}
});
-
- test("admin routes render with specific content", async ({ frigateApp }) => {
- // System page should have tab controls
- await frigateApp.goto("/system");
- await frigateApp.page.waitForTimeout(3000);
- await expect(frigateApp.page.getByLabel("Select general")).toBeVisible({
- timeout: 5_000,
- });
-
- // Logs page should have service tabs
- await frigateApp.goto("/logs");
- await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({
- timeout: 5_000,
- });
- });
});
diff --git a/web/e2e/specs/chat.spec.ts b/web/e2e/specs/chat.spec.ts
index ba4a4e6589..cab49828ab 100644
--- a/web/e2e/specs/chat.spec.ts
+++ b/web/e2e/specs/chat.spec.ts
@@ -1,34 +1,358 @@
/**
* Chat page tests -- MEDIUM tier.
*
- * Tests chat interface rendering, input area, and example prompt buttons.
+ * Starting state, NDJSON streaming contract (not SSE), assistant
+ * bubble grows as chunks arrive, error path, and mobile viewport.
*/
-import { test, expect } from "../fixtures/frigate-test";
+import { test, expect, type FrigateApp } from "../fixtures/frigate-test";
-test.describe("Chat Page @medium", () => {
- test("chat page renders without crash", async ({ frigateApp }) => {
- await frigateApp.goto("/chat");
- await frigateApp.page.waitForTimeout(2000);
- await expect(frigateApp.page.locator("body")).toBeVisible();
- });
+/**
+ * Install a window.fetch override on the page so that POSTs to
+ * chat/completion resolve with a real ReadableStream that emits the
+ * given chunks over time. This is the only way to validate
+ * chunk-by-chunk rendering through Playwright — page.route() does not
+ * support streaming responses.
+ *
+ * Must be called BEFORE frigateApp.goto(). The override also exposes
+ * `__chatRequests` on window so tests can assert the outgoing body.
+ */
+async function installChatStreamOverride(
+ app: FrigateApp,
+ chunks: Array>,
+ opts: { chunkDelayMs?: number; status?: number } = {},
+) {
+ const { chunkDelayMs = 40, status = 200 } = opts;
+ await app.page.addInitScript(
+ ({ chunks, chunkDelayMs, status }) => {
+ (window as unknown as { __chatRequests: unknown[] }).__chatRequests = [];
+ const origFetch = window.fetch;
+ window.fetch = async (input, init) => {
+ const url =
+ typeof input === "string"
+ ? input
+ : input instanceof URL
+ ? input.toString()
+ : (input as Request).url;
+ if (url.includes("chat/completion")) {
+ const body =
+ init?.body instanceof String || typeof init?.body === "string"
+ ? JSON.parse(init!.body as string)
+ : null;
+ (
+ window as unknown as { __chatRequests: unknown[] }
+ ).__chatRequests.push({ url, body });
+ if (status !== 200) {
+ return new Response(JSON.stringify({ error: "boom" }), {
+ status,
+ });
+ }
+ const encoder = new TextEncoder();
+ const stream = new ReadableStream({
+ async start(controller) {
+ for (const chunk of chunks) {
+ await new Promise((r) => setTimeout(r, chunkDelayMs));
+ controller.enqueue(
+ encoder.encode(JSON.stringify(chunk) + "\n"),
+ );
+ }
+ controller.close();
+ },
+ });
+ return new Response(stream, { status: 200 });
+ }
+ return origFetch.call(window, input as RequestInfo, init);
+ };
+ },
+ { chunks, chunkDelayMs, status },
+ );
+}
- test("chat page has interactive input or buttons", async ({ frigateApp }) => {
+test.describe("Chat — starting state @medium", () => {
+ test("empty message list renders ChatStartingState with title and input", async ({
+ frigateApp,
+ }) => {
await frigateApp.goto("/chat");
- await frigateApp.page.waitForTimeout(2000);
- const interactive = frigateApp.page.locator("input, textarea, button");
- const count = await interactive.count();
- expect(count).toBeGreaterThan(0);
+ await expect(
+ frigateApp.page.getByRole("heading", { level: 1 }),
+ ).toBeVisible({ timeout: 10_000 });
+ await expect(frigateApp.page.getByPlaceholder(/ask/i)).toBeVisible();
+ // Four quick-reply buttons from starting_requests.*
+ const quickReplies = frigateApp.page.locator(
+ "button:has-text('Show recent events'), button:has-text('Show camera status'), button:has-text('What happened'), button:has-text('Watch')",
+ );
+ await expect(quickReplies.first()).toBeVisible({ timeout: 5_000 });
});
+});
- test("chat input accepts text", async ({ frigateApp }) => {
+test.describe("Chat — streaming @medium", () => {
+ test("submission POSTs to chat/completion with stream: true", async ({
+ frigateApp,
+ }) => {
+ await installChatStreamOverride(frigateApp, [
+ { type: "content", delta: "Hel" },
+ { type: "content", delta: "lo" },
+ {
+ type: "messages",
+ messages: [
+ { role: "system", content: "sys" },
+ { role: "user", content: "hello chat" },
+ { role: "assistant", content: "Hello" },
+ ],
+ },
+ { type: "done" },
+ ]);
await frigateApp.goto("/chat");
- await frigateApp.page.waitForTimeout(2000);
- const input = frigateApp.page.locator("input, textarea").first();
- if (await input.isVisible().catch(() => false)) {
- await input.fill("What cameras detected a person today?");
- const value = await input.inputValue();
- expect(value.length).toBeGreaterThan(0);
- }
+ const input = frigateApp.page.getByPlaceholder(/ask/i);
+ await expect(input).toBeVisible({ timeout: 10_000 });
+ await input.fill("hello chat");
+ await input.press("Enter");
+
+ await expect
+ .poll(
+ async () =>
+ frigateApp.page.evaluate(
+ () =>
+ (window as unknown as { __chatRequests: unknown[] })
+ .__chatRequests?.length ?? 0,
+ ),
+ { timeout: 5_000 },
+ )
+ .toBeGreaterThan(0);
+
+ const request = await frigateApp.page.evaluate(
+ () =>
+ (
+ window as unknown as {
+ __chatRequests: Array<{
+ url: string;
+ body: { stream: boolean; messages: Array<{ content: string }> };
+ }>;
+ }
+ ).__chatRequests[0],
+ );
+ expect(request.body.stream).toBe(true);
+ expect(
+ request.body.messages[request.body.messages.length - 1].content,
+ ).toBe("hello chat");
+ });
+
+ test("NDJSON content chunks accumulate in the assistant bubble", async ({
+ frigateApp,
+ }) => {
+ await installChatStreamOverride(
+ frigateApp,
+ [
+ { type: "content", delta: "Hel" },
+ { type: "content", delta: "lo, " },
+ { type: "content", delta: "world!" },
+ {
+ type: "messages",
+ messages: [
+ { role: "system", content: "sys" },
+ { role: "user", content: "greet me" },
+ { role: "assistant", content: "Hello, world!" },
+ ],
+ },
+ { type: "done" },
+ ],
+ { chunkDelayMs: 50 },
+ );
+ await frigateApp.goto("/chat");
+ const input = frigateApp.page.getByPlaceholder(/ask/i);
+ await expect(input).toBeVisible({ timeout: 10_000 });
+ await input.fill("greet me");
+ await input.press("Enter");
+
+ await expect(frigateApp.page.getByText(/Hello, world!/i)).toBeVisible({
+ timeout: 10_000,
+ });
+ });
+
+ test("tool calls in the chain render a ToolCallsGroup", async ({
+ frigateApp,
+ }) => {
+ const toolTurn = [
+ { role: "system", content: "sys" },
+ { role: "user", content: "find people" },
+ {
+ role: "assistant",
+ content: null,
+ tool_calls: [
+ {
+ id: "call_1",
+ type: "function",
+ function: {
+ name: "search_objects",
+ arguments: '{"label":"person"}',
+ },
+ },
+ ],
+ },
+ { role: "tool", tool_call_id: "call_1", content: "[]" },
+ ];
+ await installChatStreamOverride(frigateApp, [
+ { type: "messages", messages: toolTurn },
+ { type: "content", delta: "Searching for people." },
+ {
+ type: "messages",
+ messages: [
+ ...toolTurn,
+ { role: "assistant", content: "Searching for people." },
+ ],
+ },
+ { type: "done" },
+ ]);
+ await frigateApp.goto("/chat");
+ const input = frigateApp.page.getByPlaceholder(/ask/i);
+ await expect(input).toBeVisible({ timeout: 10_000 });
+ await input.fill("find people");
+ await input.press("Enter");
+
+ // ToolCallsGroup normalizes "search_objects" → "Search Objects" via
+ // normalizeName(). Match the rendered display label instead.
+ await expect(frigateApp.page.getByText(/search objects/i)).toBeVisible({
+ timeout: 10_000,
+ });
+ await expect(
+ frigateApp.page.getByText(/searching for people/i),
+ ).toBeVisible({ timeout: 5_000 });
+ });
+});
+
+test.describe("Chat — stop @medium", () => {
+ test("Stop button aborts an in-flight stream and freezes the partial message", async ({
+ frigateApp,
+ }) => {
+ // A long chunk sequence with big delays gives us time to hit Stop.
+ await installChatStreamOverride(
+ frigateApp,
+ [
+ { type: "content", delta: "First chunk. " },
+ { type: "content", delta: "Second chunk. " },
+ { type: "content", delta: "Third chunk. " },
+ ],
+ { chunkDelayMs: 300 },
+ );
+ await frigateApp.goto("/chat");
+ const input = frigateApp.page.getByPlaceholder(/ask/i);
+ await expect(input).toBeVisible({ timeout: 10_000 });
+ await input.fill("slow response please");
+ await input.press("Enter");
+
+ // Wait for the first chunk to render
+ await expect(frigateApp.page.getByText(/First chunk\./)).toBeVisible({
+ timeout: 10_000,
+ });
+
+ // The Stop button is a destructive rounded button shown while isLoading.
+ // It contains only an FaStop SVG icon (no visible text). Find it by the
+ // destructive variant class or fall back to aria-label.
+ const stopBtn = frigateApp.page
+ .locator("button.bg-destructive, button[class*='destructive']")
+ .first();
+ await stopBtn.click({ timeout: 3_000 }).catch(async () => {
+ await frigateApp.page
+ .getByRole("button", { name: /stop|cancel/i })
+ .first()
+ .click();
+ });
+
+ // Third chunk should never appear.
+ await expect(frigateApp.page.getByText(/Third chunk\./)).toHaveCount(0);
+ });
+});
+
+test.describe("Chat — error @medium", () => {
+ test("non-OK response renders an error banner", async ({ frigateApp }) => {
+ await installChatStreamOverride(frigateApp, [], { status: 500 });
+ await frigateApp.goto("/chat");
+ const input = frigateApp.page.getByPlaceholder(/ask/i);
+ await expect(input).toBeVisible({ timeout: 10_000 });
+ await input.fill("trigger error");
+ await input.press("Enter");
+ // The error banner is a role="alert" paragraph; target by role so we
+ // don't collide with the user-message bubble that contains "trigger
+ // error" (which would match /error/ in strict mode).
+ await expect(
+ frigateApp.page.getByRole("alert").filter({
+ hasText: /boom|something went wrong/i,
+ }),
+ ).toBeVisible({ timeout: 5_000 });
+ });
+});
+
+test.describe("Chat — attachment chip @medium", () => {
+ test("attaching an event renders a ChatAttachmentChip", async ({
+ frigateApp,
+ }) => {
+ // The chat starts with an empty message list (ChatStartingState).
+ // After sending a message, ChatEntry with the paperclip button appears.
+ // We use the stream override so the first message completes quickly.
+ await installChatStreamOverride(frigateApp, [
+ { type: "content", delta: "Done." },
+ {
+ type: "messages",
+ messages: [
+ { role: "system", content: "sys" },
+ { role: "user", content: "hello" },
+ { role: "assistant", content: "Done." },
+ ],
+ },
+ { type: "done" },
+ ]);
+ await frigateApp.goto("/chat");
+
+ // Send a first message to transition out of ChatStartingState so the
+ // full ChatEntry (with the paperclip) is visible.
+ const input = frigateApp.page.getByPlaceholder(/ask/i);
+ await expect(input).toBeVisible({ timeout: 10_000 });
+ await input.fill("hello");
+ await input.press("Enter");
+ // Wait for the assistant response to complete so isLoading becomes false
+ // and the paperclip button is re-enabled.
+ await expect(frigateApp.page.getByText(/Done\./i)).toBeVisible({
+ timeout: 10_000,
+ });
+
+ // The paperclip button has aria-label from t("attachment_picker_placeholder")
+ // = "Attach an event".
+ const paperclip = frigateApp.page
+ .getByRole("button", { name: /attach an event/i })
+ .first();
+ await expect(paperclip).toBeVisible({ timeout: 5_000 });
+ await paperclip.click();
+
+ // The popover shows a paste input with placeholder "Or paste event ID".
+ const idInput = frigateApp.page
+ .locator('input[placeholder*="event" i], input[aria-label*="attach" i]')
+ .first();
+ await expect(idInput).toBeVisible({ timeout: 3_000 });
+ await idInput.fill("test-event-1");
+ await frigateApp.page
+ .getByRole("button", { name: /^attach$/i })
+ .first()
+ .click();
+
+ // The ChatAttachmentChip renders in the composer area. It shows an
+ // activity indicator while loading event data (event_ids API not mocked),
+ // so assert on the chip container being present in the composer.
+ await expect(
+ frigateApp.page.locator(
+ "[class*='inline-flex'][class*='rounded-lg'][class*='border']",
+ ),
+ ).toBeVisible({ timeout: 5_000 });
+ });
+});
+
+test.describe("Chat — mobile @medium @mobile", () => {
+ test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
+
+ test("chat input is focusable at mobile viewport", async ({ frigateApp }) => {
+ await frigateApp.goto("/chat");
+ const input = frigateApp.page.getByPlaceholder(/ask/i);
+ await expect(input).toBeVisible({ timeout: 10_000 });
+ await input.focus();
+ await expect(input).toBeFocused();
});
});
diff --git a/web/e2e/specs/classification.spec.ts b/web/e2e/specs/classification.spec.ts
index 9dd0815c60..83a33a815d 100644
--- a/web/e2e/specs/classification.spec.ts
+++ b/web/e2e/specs/classification.spec.ts
@@ -1,33 +1,228 @@
/**
* Classification page tests -- MEDIUM tier.
*
- * Tests model selection view rendering and interactive elements.
+ * Model list driven by config.classification.custom + per-model
+ * dataset fetches. Admin-only access.
*/
import { test, expect } from "../fixtures/frigate-test";
+import { viewerProfile } from "../fixtures/mock-data/profile";
-test.describe("Classification @medium", () => {
- test("classification page renders without crash", async ({ frigateApp }) => {
+const CUSTOM_MODELS = {
+ object_classifier: {
+ name: "object_classifier",
+ object_config: { objects: ["person"], classification_type: "sub_label" },
+ },
+ state_classifier: {
+ name: "state_classifier",
+ state_config: { cameras: { front_door: { crop: [0, 0, 1, 1] } } },
+ },
+};
+
+async function installDatasetRoute(
+ app: { page: import("@playwright/test").Page },
+ name: string,
+ body: Record = { categories: {} },
+) {
+ await app.page.route(
+ new RegExp(`/api/classification/${name}/dataset`),
+ (route) => route.fulfill({ json: body }),
+ );
+}
+
+async function installTrainRoute(
+ app: { page: import("@playwright/test").Page },
+ name: string,
+) {
+ await app.page.route(
+ new RegExp(`/api/classification/${name}/train`),
+ (route) => route.fulfill({ json: [] }),
+ );
+}
+
+test.describe("Classification — model list @medium", () => {
+ test("custom models render by name", async ({ frigateApp }) => {
+ await frigateApp.installDefaults({
+ config: { classification: { custom: CUSTOM_MODELS } },
+ });
+ await installDatasetRoute(frigateApp, "object_classifier");
+ await installDatasetRoute(frigateApp, "state_classifier");
await frigateApp.goto("/classification");
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
+ await expect(frigateApp.page.getByText("object_classifier")).toBeVisible({
+ timeout: 10_000,
+ });
});
- test("classification page shows content and controls", async ({
- frigateApp,
- }) => {
+ test("empty custom map renders without crash", async ({ frigateApp }) => {
+ await frigateApp.installDefaults({
+ config: { classification: { custom: {} } },
+ });
await frigateApp.goto("/classification");
- await frigateApp.page.waitForTimeout(2000);
- const text = await frigateApp.page.textContent("#pageRoot");
- expect(text?.length).toBeGreaterThan(0);
+ await expect(frigateApp.page.locator("#pageRoot")).toBeVisible({
+ timeout: 10_000,
+ });
});
- test("classification page has interactive elements", async ({
+ test("toggling to states view switches the rendered card set", async ({
frigateApp,
}) => {
+ await frigateApp.installDefaults({
+ config: { classification: { custom: CUSTOM_MODELS } },
+ });
+ await installDatasetRoute(frigateApp, "object_classifier");
+ await installDatasetRoute(frigateApp, "state_classifier");
await frigateApp.goto("/classification");
- await frigateApp.page.waitForTimeout(2000);
- const buttons = frigateApp.page.locator("#pageRoot button");
- const count = await buttons.count();
- expect(count).toBeGreaterThanOrEqual(0);
+ // Objects is default — object_classifier visible, state_classifier hidden.
+ await expect(frigateApp.page.getByText("object_classifier")).toBeVisible({
+ timeout: 10_000,
+ });
+ await expect(frigateApp.page.getByText("state_classifier")).toHaveCount(0);
+
+ // Click the "states" toggle. Radix ToggleGroup type="single" uses role="radio".
+ const statesToggle = frigateApp.page
+ .getByRole("radio", { name: /state/i })
+ .first();
+ await expect(statesToggle).toBeVisible({ timeout: 5_000 });
+ await statesToggle.click();
+
+ await expect(frigateApp.page.getByText("state_classifier")).toBeVisible({
+ timeout: 5_000,
+ });
+ await expect(frigateApp.page.getByText("object_classifier")).toHaveCount(0);
+ });
+});
+
+test.describe("Classification — model detail navigation @medium", () => {
+ test("clicking a model card opens ModelTrainingView", async ({
+ frigateApp,
+ }) => {
+ await frigateApp.installDefaults({
+ config: { classification: { custom: CUSTOM_MODELS } },
+ });
+ await installDatasetRoute(frigateApp, "object_classifier");
+ await installDatasetRoute(frigateApp, "state_classifier");
+ await installTrainRoute(frigateApp, "object_classifier");
+ await frigateApp.goto("/classification");
+
+ const objectCard = frigateApp.page.getByText("object_classifier").first();
+ await expect(objectCard).toBeVisible({ timeout: 10_000 });
+ await objectCard.click();
+
+ // ModelTrainingView renders a Back button (aria-label "Back").
+ // useOverlayState stores the selected model in window.history.state
+ // (not the URL), so we verify the state transition via the DOM.
+ await expect(
+ frigateApp.page.getByRole("button", { name: /back/i }),
+ ).toBeVisible({ timeout: 5_000 });
+
+ // The model grid is no longer shown; state_classifier card is gone.
+ await expect(frigateApp.page.getByText("state_classifier")).toHaveCount(0);
+ });
+});
+
+test.describe("Classification — delete model (desktop) @medium", () => {
+ test.skip(
+ ({ frigateApp }) => frigateApp.isMobile,
+ "Delete action menu is desktop-focused",
+ );
+
+ test("deleting a model fires DELETE + PUT /config/set", async ({
+ frigateApp,
+ }) => {
+ let deleteCalled = false;
+ let configSetCalled = false;
+
+ // installDefaults must run first because Playwright matches routes in
+ // LIFO order — routes registered after installDefaults take precedence
+ // over the generic catch-all registered inside it.
+ await frigateApp.installDefaults({
+ config: { classification: { custom: CUSTOM_MODELS } },
+ });
+ await installDatasetRoute(frigateApp, "object_classifier");
+ await installDatasetRoute(frigateApp, "state_classifier");
+
+ // Register spy routes after installDefaults so they win over the catch-all.
+ await frigateApp.page.route(
+ /\/api\/classification\/object_classifier$/,
+ async (route) => {
+ if (route.request().method() === "DELETE") {
+ deleteCalled = true;
+ await route.fulfill({ json: { success: true } });
+ return;
+ }
+ return route.fallback();
+ },
+ );
+ await frigateApp.page.route("**/api/config/set", async (route) => {
+ if (route.request().method() === "PUT") configSetCalled = true;
+ await route.fulfill({ json: { success: true, require_restart: false } });
+ });
+ await frigateApp.goto("/classification");
+ await expect(frigateApp.page.getByText("object_classifier")).toBeVisible({
+ timeout: 10_000,
+ });
+
+ // The card-level actions menu (FiMoreVertical three-dot icon) is a
+ // DropdownMenuTrigger with asChild on a BlurredIconButton div.
+ // Radix forwards aria-haspopup="menu" to the child element.
+ // Scope the selector to the model card grid to avoid hitting the
+ // settings sidebar trigger.
+ const cardGrid = frigateApp.page.locator(".grid.auto-rows-max");
+ await expect(cardGrid).toBeVisible({ timeout: 5_000 });
+ const trigger = cardGrid.locator('[aria-haspopup="menu"]').first();
+ await expect(trigger).toBeVisible({ timeout: 5_000 });
+ await trigger.click();
+ const deleteItem = frigateApp.page
+ .getByRole("menuitem", { name: /delete/i })
+ .first();
+ await expect(deleteItem).toBeVisible({ timeout: 5_000 });
+ await deleteItem.click();
+
+ // Confirm the AlertDialog.
+ const alert = frigateApp.page.getByRole("alertdialog");
+ await expect(alert).toBeVisible({ timeout: 5_000 });
+ await alert
+ .getByRole("button", { name: /delete|confirm/i })
+ .first()
+ .click();
+
+ await expect.poll(() => deleteCalled, { timeout: 5_000 }).toBe(true);
+ await expect.poll(() => configSetCalled, { timeout: 5_000 }).toBe(true);
+ });
+});
+
+test.describe("Classification — admin only @medium", () => {
+ test("viewer navigating to /classification is redirected to access-denied", async ({
+ frigateApp,
+ }) => {
+ await frigateApp.installDefaults({ profile: viewerProfile() });
+ await frigateApp.page.goto("/classification");
+ await frigateApp.page.waitForSelector("#pageRoot", { timeout: 10_000 });
+ await expect(frigateApp.page).toHaveURL(/\/unauthorized/, {
+ timeout: 10_000,
+ });
+ await expect(
+ frigateApp.page.getByRole("heading", {
+ level: 2,
+ name: /access denied/i,
+ }),
+ ).toBeVisible({ timeout: 10_000 });
+ });
+});
+
+test.describe("Classification — mobile @medium @mobile", () => {
+ test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
+
+ test("page renders at mobile viewport", async ({ frigateApp }) => {
+ await frigateApp.installDefaults({
+ config: { classification: { custom: CUSTOM_MODELS } },
+ });
+ await installDatasetRoute(frigateApp, "object_classifier");
+ await installDatasetRoute(frigateApp, "state_classifier");
+ await frigateApp.goto("/classification");
+ await expect(frigateApp.page.locator("#pageRoot")).toBeVisible({
+ timeout: 10_000,
+ });
});
});
diff --git a/web/e2e/specs/clone-camera.spec.ts b/web/e2e/specs/clone-camera.spec.ts
new file mode 100644
index 0000000000..1c75e71a3d
--- /dev/null
+++ b/web/e2e/specs/clone-camera.spec.ts
@@ -0,0 +1,181 @@
+/**
+ * Camera clone dialog E2E tests.
+ *
+ * Covers the design invariants that don't depend on per-camera resolution
+ * differences in the mock fixture:
+ * 1. Dialog opens from the "Clone settings" button below Add/Delete.
+ * 2. A source camera must be chosen inside the dialog before cloning.
+ * 3. "Stream URLs and roles" is forced on and disabled for new-camera target.
+ * 4. Cloning to a new camera issues a single add PUT and shows a restart prompt.
+ * 5. The existing-camera target selects multiple destinations via a switch
+ * popover (with an "All cameras" toggle and source exclusion); the closed
+ * trigger summarizes the selection by name or as "All cameras".
+ *
+ * The spatial-mismatch warning path is exercised in unit-level review and via
+ * manual QA — the shared mock fixture ships every camera at 1280×720. The
+ * existing-camera PUT fan-out is likewise not asserted here: the mock cameras
+ * are identical apart from stream URLs (which existing-camera clones never
+ * copy) and the schema mock is empty, so a clone onto them produces no diff
+ * and no PUT. That path is covered by unit-level review and manual QA.
+ */
+
+import { test, expect } from "../fixtures/frigate-test";
+
+async function openCloneDialog(frigateApp: {
+ page: import("@playwright/test").Page;
+}) {
+ await frigateApp.page
+ .getByRole("button", { name: /^Clone settings$/i })
+ .click();
+ await expect(frigateApp.page.getByRole("dialog")).toBeVisible();
+}
+
+async function selectSource(
+ frigateApp: { page: import("@playwright/test").Page },
+ source: string,
+) {
+ await frigateApp.page.getByRole("dialog").getByRole("combobox").click();
+ await frigateApp.page
+ .getByRole("option", { name: source, exact: true })
+ .click();
+}
+
+test.describe("Camera clone dialog @medium @mobile", () => {
+ test.beforeEach(async ({ frigateApp }) => {
+ await frigateApp.goto("/settings?page=cameraManagement");
+ await expect(
+ frigateApp.page.getByRole("heading", { name: /Manage Cameras/i }),
+ ).toBeVisible();
+ });
+
+ test("opens the dialog from the Clone settings button", async ({
+ frigateApp,
+ }) => {
+ await openCloneDialog(frigateApp);
+
+ await expect(
+ frigateApp.page.getByRole("dialog").getByText(/Clone camera settings/i),
+ ).toBeVisible();
+
+ // The Clone button is disabled until a source (and target) is chosen.
+ await expect(
+ frigateApp.page.getByRole("button", { name: /^Clone$/i }),
+ ).toBeDisabled();
+ });
+
+ test("forces Stream URLs and roles on for new-camera target", async ({
+ frigateApp,
+ }) => {
+ await openCloneDialog(frigateApp);
+ await selectSource(frigateApp, "Front Door");
+
+ // The "New camera" radio is selected by default; the Streams group renders
+ // the ffmpeg_live checkbox as forced-checked and disabled.
+ const streamsLabel = frigateApp.page
+ .locator("label")
+ .filter({ hasText: /Stream URLs and roles/i });
+ await expect(streamsLabel).toBeVisible();
+
+ const streamsCheckbox = streamsLabel.getByRole("checkbox");
+ await expect(streamsCheckbox).toBeChecked();
+ await expect(streamsCheckbox).toBeDisabled();
+ });
+
+ test("issues a single add PUT and shows restart toast for new-camera target", async ({
+ frigateApp,
+ }) => {
+ const requests: { body: unknown }[] = [];
+
+ await frigateApp.page.route("**/api/config/set", async (route) => {
+ const body = route.request().postDataJSON();
+ requests.push({ body });
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({ success: true, require_restart: false }),
+ });
+ });
+
+ await frigateApp.goto("/settings?page=cameraManagement");
+ await expect(
+ frigateApp.page.getByRole("heading", { name: /Manage Cameras/i }),
+ ).toBeVisible();
+
+ await openCloneDialog(frigateApp);
+ await selectSource(frigateApp, "Front Door");
+
+ const nameInput = frigateApp.page.getByPlaceholder(
+ /e\.g\., back_door or Back Door/i,
+ );
+ await nameInput.fill("clone_target_one");
+
+ // With a source picked and a valid name, changeCount > 0 enables Clone.
+ await expect(
+ frigateApp.page.getByRole("button", { name: /^Clone$/i }),
+ ).toBeEnabled({ timeout: 5_000 });
+
+ await frigateApp.page.getByRole("button", { name: /^Clone$/i }).click();
+
+ // New-camera clones bundle into a single atomic add PUT (avoids
+ // per-section validation ordering issues).
+ await expect.poll(() => requests.length, { timeout: 10_000 }).toBe(1);
+
+ const firstBody = requests[0].body as {
+ requires_restart?: number;
+ update_topic?: string;
+ };
+ expect(firstBody.update_topic).toMatch(
+ /config\/cameras\/clone_target_one\/add/,
+ );
+ expect(firstBody.requires_restart).toBe(1);
+
+ // The toast offers a Restart action because new-camera always needs restart.
+ // .first() avoids strict-mode rejection when both the toast action and the
+ // RestartDialog trigger render concurrently.
+ await expect(
+ frigateApp.page.getByRole("button", { name: /Restart/i }).first(),
+ ).toBeVisible({ timeout: 8_000 });
+ });
+
+ test("selects multiple existing destination cameras via a switch popover", async ({
+ frigateApp,
+ }) => {
+ await openCloneDialog(frigateApp);
+ await selectSource(frigateApp, "Front Door");
+
+ await frigateApp.page
+ .getByRole("radio", { name: /Existing cameras/i })
+ .click();
+
+ const dialog = frigateApp.page.getByRole("dialog");
+
+ // The destination trigger starts with the empty-selection placeholder.
+ await dialog
+ .getByRole("button", { name: /Select at least one camera/i })
+ .click();
+
+ // The chosen source is excluded from the destination switch list.
+ await expect(
+ dialog.getByRole("switch", { name: /Backyard/i }),
+ ).toBeVisible();
+ await expect(dialog.getByRole("switch", { name: /Garage/i })).toBeVisible();
+ await expect(
+ dialog.getByRole("switch", { name: /^Front Door$/i }),
+ ).toHaveCount(0);
+
+ // Selecting a single camera summarizes by name once the popover closes.
+ await dialog.getByRole("switch", { name: /Backyard/i }).click();
+ await frigateApp.page.keyboard.press("Escape");
+ await expect(
+ dialog.getByRole("button", { name: /^Backyard$/i }),
+ ).toBeVisible();
+
+ // Reopen and select everything; the trigger collapses to "All cameras".
+ await dialog.getByRole("button", { name: /^Backyard$/i }).click();
+ await dialog.getByRole("switch", { name: /^All cameras$/i }).click();
+ await frigateApp.page.keyboard.press("Escape");
+ await expect(
+ dialog.getByRole("button", { name: /^All cameras$/i }),
+ ).toBeVisible();
+ });
+});
diff --git a/web/e2e/specs/config-editor.spec.ts b/web/e2e/specs/config-editor.spec.ts
index 1de6fc52b7..2b51a93636 100644
--- a/web/e2e/specs/config-editor.spec.ts
+++ b/web/e2e/specs/config-editor.spec.ts
@@ -1,44 +1,276 @@
/**
- * Config Editor page tests -- MEDIUM tier.
+ * Config Editor tests -- MEDIUM tier.
*
- * Tests Monaco editor loading, YAML content rendering,
- * save button presence, and copy button interaction.
+ * Monaco load + value, Save (config/save?save_option=saveonly),
+ * Save error path, Save and Restart (WS frame via useRestart),
+ * Copy (clipboard), schema markers.
*/
import { test, expect } from "../fixtures/frigate-test";
+import { installWsFrameCapture, waitForWsFrame } from "../helpers/ws-frames";
+import { grantClipboardPermissions, readClipboard } from "../helpers/clipboard";
+import {
+ getMonacoVisibleText,
+ replaceMonacoValue,
+ waitForErrorMarker,
+} from "../helpers/monaco";
-test.describe("Config Editor @medium", () => {
- test("config editor loads Monaco editor with content", async ({
- frigateApp,
- }) => {
+const SAMPLE_CONFIG =
+ "mqtt:\n host: mqtt\ncameras:\n front_door:\n enabled: true\n";
+
+async function installSaveRoute(
+ app: { page: import("@playwright/test").Page },
+ status: number,
+ body: Record,
+): Promise<{
+ capturedUrl: () => string | null;
+ capturedBody: () => string | null;
+}> {
+ let lastUrl: string | null = null;
+ let lastBody: string | null = null;
+ await app.page.route("**/api/config/save**", async (route) => {
+ lastUrl = route.request().url();
+ lastBody = route.request().postData();
+ await route.fulfill({ status, json: body });
+ });
+ return {
+ capturedUrl: () => lastUrl,
+ capturedBody: () => lastBody,
+ };
+}
+
+test.describe("Config Editor — Monaco @medium", () => {
+ test("editor loads with mocked configRaw content", async ({ frigateApp }) => {
+ await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG });
await frigateApp.goto("/config");
- await frigateApp.page.waitForTimeout(5000);
- // Monaco editor should render with a specific class
- const editor = frigateApp.page.locator(
- ".monaco-editor, [data-keybinding-context]",
+ await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
+ { timeout: 15_000 },
+ );
+ // Assert via DOM-rendered visible text (Monaco virtualizes — works
+ // for short configs which covers our mocked content).
+ await expect
+ .poll(() => getMonacoVisibleText(frigateApp.page), { timeout: 10_000 })
+ .toContain("front_door");
+ });
+});
+
+test.describe("Config Editor — Save @medium", () => {
+ test.skip(
+ ({ frigateApp }) => frigateApp.isMobile,
+ "Save button copy is desktop-visible (hidden md:block)",
+ );
+
+ test("clicking Save Only POSTs config/save?save_option=saveonly", async ({
+ frigateApp,
+ }) => {
+ await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG });
+ const capture = await installSaveRoute(frigateApp, 200, {
+ message: "Config saved",
+ });
+ await frigateApp.goto("/config");
+ await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
+ { timeout: 15_000 },
+ );
+ await frigateApp.page.getByLabel("Save Only").click();
+ await expect
+ .poll(() => capture.capturedUrl(), { timeout: 5_000 })
+ .toMatch(/config\/save\?save_option=saveonly/);
+ // Body is the raw YAML as text/plain
+ await expect
+ .poll(() => capture.capturedBody(), { timeout: 5_000 })
+ .toContain("front_door");
+ });
+
+ test("Save error shows the server message in the error area", async ({
+ frigateApp,
+ }) => {
+ await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG });
+ await installSaveRoute(frigateApp, 400, {
+ message: "Invalid field `cameras.front_door`",
+ });
+ await frigateApp.goto("/config");
+ await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
+ { timeout: 15_000 },
+ );
+ await frigateApp.page.getByLabel("Save Only").click();
+ await expect(frigateApp.page.getByText(/Invalid field/i)).toBeVisible({
+ timeout: 5_000,
+ });
+ });
+});
+
+test.describe("Config Editor — Save and Restart @medium", () => {
+ test.skip(
+ ({ frigateApp }) => frigateApp.isMobile,
+ "Save and Restart button copy is desktop-visible",
+ );
+
+ test("Save and Restart opens dialog; confirm sends WS restart frame", async ({
+ frigateApp,
+ }) => {
+ await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG });
+ await installSaveRoute(frigateApp, 200, { message: "Saved" });
+ await installWsFrameCapture(frigateApp.page);
+
+ await frigateApp.goto("/config");
+ await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
+ { timeout: 15_000 },
+ );
+
+ await frigateApp.page.getByLabel("Save & Restart").click();
+ const dialog = frigateApp.page.getByRole("alertdialog");
+ await expect(dialog).toBeVisible({ timeout: 5_000 });
+
+ await dialog.getByRole("button", { name: /restart/i }).click();
+ await waitForWsFrame(
+ frigateApp.page,
+ (frame) => frame.includes('"restart"') || frame.includes("restart"),
+ { message: "useRestart should send a WS frame on the restart topic" },
+ );
+ });
+
+ test("cancelling the restart dialog leaves body interactive", async ({
+ frigateApp,
+ }) => {
+ await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG });
+ await installSaveRoute(frigateApp, 200, { message: "Saved" });
+
+ await frigateApp.goto("/config");
+ await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
+ { timeout: 15_000 },
+ );
+
+ await frigateApp.page.getByLabel("Save & Restart").click();
+ const dialog = frigateApp.page.getByRole("alertdialog");
+ await expect(dialog).toBeVisible({ timeout: 5_000 });
+ await dialog.getByRole("button", { name: /cancel/i }).click();
+ await expect(dialog).not.toBeVisible({ timeout: 3_000 });
+ await expect(
+ frigateApp.page.locator(".monaco-editor").first(),
+ ).toBeVisible();
+ });
+});
+
+test.describe("Config Editor — Copy @medium", () => {
+ test.skip(
+ ({ frigateApp }) => frigateApp.isMobile,
+ "Copy button copy is desktop-visible",
+ );
+
+ test("Copy places the editor value in the clipboard", async ({
+ frigateApp,
+ context,
+ }) => {
+ await grantClipboardPermissions(context);
+ await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG });
+ await frigateApp.goto("/config");
+ await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
+ { timeout: 15_000 },
+ );
+
+ await frigateApp.page.getByLabel("Copy Config").click();
+ await expect
+ .poll(() => readClipboard(frigateApp.page), { timeout: 5_000 })
+ .toContain("front_door");
+ });
+});
+
+test.describe("Config Editor — schema markers @medium", () => {
+ test.skip(
+ ({ frigateApp }) => frigateApp.isMobile,
+ "Schema validation assumes focused desktop editing",
+ );
+
+ test("invalid YAML renders at least one error marker in the DOM", async ({
+ frigateApp,
+ }) => {
+ await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG });
+ await frigateApp.goto("/config");
+ await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
+ { timeout: 15_000 },
+ );
+
+ // Replace editor contents with clearly invalid YAML via keyboard.
+ await replaceMonacoValue(
+ frigateApp.page,
+ "this is not: [yaml: and has {unbalanced",
+ );
+ // Monaco debounces marker evaluation; the .squiggly-error decoration
+ // appears asynchronously in the .view-overlays layer.
+ await waitForErrorMarker(frigateApp.page);
+ });
+});
+
+test.describe("Config Editor — Cmd+S keyboard shortcut @medium", () => {
+ test.skip(
+ ({ frigateApp }) => frigateApp.isMobile,
+ "Keyboard save shortcut is desktop-only",
+ );
+
+ test("Cmd/Ctrl+S fires the same config/save POST as the Save button", async ({
+ frigateApp,
+ }) => {
+ await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG });
+ const capture = await installSaveRoute(frigateApp, 200, {
+ message: "Saved",
+ });
+ await frigateApp.goto("/config");
+ await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
+ { timeout: 15_000 },
+ );
+
+ // Focus the editor so Monaco's keybinding receives the shortcut.
+ await frigateApp.page.locator(".monaco-editor").first().click();
+ await frigateApp.page.keyboard.press("ControlOrMeta+s");
+
+ await expect
+ .poll(() => capture.capturedUrl(), { timeout: 5_000 })
+ .toMatch(/config\/save\?save_option=saveonly/);
+ });
+});
+
+test.describe("Config Editor — Safe Mode auto-validation @medium", () => {
+ test("safe-mode config auto-posts on mount and shows the inline error", async ({
+ frigateApp,
+ }) => {
+ // Thread safe_mode: true through the config override, then stub
+ // config/save to return a validation error. The page's
+ // initialValidationRef effect runs on mount and POSTs
+ // config/save?save_option=saveonly with the raw config; the 400
+ // surfaces through setError.
+ // installDefaults must come first so our specific route wins (LIFO).
+ await frigateApp.installDefaults({
+ config: { safe_mode: true } as unknown as Record,
+ configRaw: "cameras:\n front_door:\n ffmpeg: {}\n",
+ });
+ let autoSaveCalled = false;
+ await frigateApp.page.route("**/api/config/save**", async (route) => {
+ autoSaveCalled = true;
+ await route.fulfill({
+ status: 400,
+ json: { message: "safe-mode validation failure" },
+ });
+ });
+
+ await frigateApp.goto("/config");
+ await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
+ { timeout: 15_000 },
+ );
+ await expect.poll(() => autoSaveCalled, { timeout: 10_000 }).toBe(true);
+ await expect(
+ frigateApp.page.getByText(/safe-mode validation failure/i),
+ ).toBeVisible({ timeout: 5_000 });
+ });
+});
+
+test.describe("Config Editor — mobile @medium @mobile", () => {
+ test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
+
+ test("editor renders at narrow viewport", async ({ frigateApp }) => {
+ await frigateApp.installDefaults({ configRaw: SAMPLE_CONFIG });
+ await frigateApp.goto("/config");
+ await expect(frigateApp.page.locator(".monaco-editor").first()).toBeVisible(
+ { timeout: 15_000 },
);
- await expect(editor.first()).toBeVisible({ timeout: 10_000 });
- });
-
- test("config editor has action buttons", async ({ frigateApp }) => {
- await frigateApp.goto("/config");
- await frigateApp.page.waitForTimeout(5000);
- const buttons = frigateApp.page.locator("button");
- const count = await buttons.count();
- expect(count).toBeGreaterThan(0);
- });
-
- test("config editor button clicks do not crash", async ({ frigateApp }) => {
- await frigateApp.goto("/config");
- await frigateApp.page.waitForTimeout(5000);
- // Find buttons with SVG icons (copy, save, etc.)
- const iconButtons = frigateApp.page.locator("button:has(svg)");
- const count = await iconButtons.count();
- if (count > 0) {
- // Click the first icon button (likely copy)
- await iconButtons.first().click();
- await frigateApp.page.waitForTimeout(500);
- }
- await expect(frigateApp.page.locator("body")).toBeVisible();
});
});
diff --git a/web/e2e/specs/explore.spec.ts b/web/e2e/specs/explore.spec.ts
index 1811338a4c..9d239a3f91 100644
--- a/web/e2e/specs/explore.spec.ts
+++ b/web/e2e/specs/explore.spec.ts
@@ -1,97 +1,265 @@
/**
* Explore page tests -- HIGH tier.
*
- * Tests search input with text entry and clearing, camera filter popover
- * opening with camera names, and content rendering with mock events.
+ * Search input, Enter submission, camera filter popover (desktop),
+ * event grid rendering with mocked events, mobile filter drawer.
+ *
+ * DEVIATION NOTES (from original plan):
+ *
+ * 1. Search input: InputWithTags is only rendered when
+ * config.semantic_search.enabled is true. Tests that exercise the search
+ * input override the config accordingly, using model:"genai" (not in the
+ * JINA_EMBEDDING_MODELS list) so the page skips local model-state checks
+ * and renders without waiting for model-download WS messages.
+ *
+ * 2. Filter buttons (Cameras, Labels, More Filters): SearchFilterGroup is
+ * only rendered when hasExistingSearch is true. Tests navigate with a URL
+ * param (?labels=person) to surface the filter bar.
+ *
+ * 3. Cameras button: accessible name is "Cameras Filter" (aria-label), not
+ * "All Cameras" (inner text). Use getByLabel("Cameras Filter").
+ *
+ * 4. Labels: button accessible name is "Labels" (aria-label). With
+ * ?labels=person, the text shows "Person" rather than "All Labels".
+ * Use getByLabel("Labels").
+ *
+ * 5. Sub-labels / Zones: These live inside the "More Filters" dialog
+ * (SearchFilterDialog), not as standalone top-level buttons. The Zones
+ * test opens "More Filters" and asserts zone content from config.
+ *
+ * 6. similarity_search_id URL param: This param does not exist in the app.
+ * The correct entrypoint for similarity search is
+ * ?search_type=similarity&event_id=. The test uses this URL and
+ * polls for the resulting API request.
*/
import { test, expect } from "../fixtures/frigate-test";
-test.describe("Explore Page - Search @high", () => {
- test("explore page renders with filter buttons", async ({ frigateApp }) => {
+// Semantic search config override used by multiple tests. Using model:
+// "genai" (not in JINA_EMBEDDING_MODELS) sets isGenaiEmbeddings=true, which
+// skips local model-state checks and lets the page render without waiting for
+// individual model download WS messages. The WS mocker returns a completed
+// reindexState so !reindexState is false and the loading gate clears.
+const SEMANTIC_SEARCH_CONFIG = {
+ semantic_search: { enabled: true, model: "genai" },
+} as const;
+
+// ---------------------------------------------------------------------------
+// Search input (semantic_search must be enabled)
+// ---------------------------------------------------------------------------
+
+test.describe("Explore — search @high", () => {
+ test("search input accepts text and clears", async ({ frigateApp }) => {
+ // Enable semantic search so InputWithTags renders.
+ await frigateApp.installDefaults({ config: SEMANTIC_SEARCH_CONFIG });
await frigateApp.goto("/explore");
+ const searchInput = frigateApp.page.locator("input").first();
+ await expect(searchInput).toBeVisible({ timeout: 10_000 });
+ await searchInput.fill("person");
+ await expect(searchInput).toHaveValue("person");
+ await searchInput.fill("");
+ await expect(searchInput).toHaveValue("");
+ });
+
+ test("Enter submission does not crash the page", async ({ frigateApp }) => {
+ await frigateApp.installDefaults({ config: SEMANTIC_SEARCH_CONFIG });
+ await frigateApp.goto("/explore");
+ const searchInput = frigateApp.page.locator("input").first();
+ await expect(searchInput).toBeVisible({ timeout: 10_000 });
+ await searchInput.fill("car in driveway");
+ await searchInput.press("Enter");
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
- const buttons = frigateApp.page.locator("#pageRoot button");
- await expect(buttons.first()).toBeVisible({ timeout: 10_000 });
- });
-
- test("search input accepts text and can be cleared", async ({
- frigateApp,
- }) => {
- await frigateApp.goto("/explore");
- await frigateApp.page.waitForTimeout(1000);
- const searchInput = frigateApp.page.locator("input").first();
- if (await searchInput.isVisible()) {
- await searchInput.fill("person");
- await expect(searchInput).toHaveValue("person");
- await searchInput.fill("");
- await expect(searchInput).toHaveValue("");
- }
- });
-
- test("search input submits on Enter", async ({ frigateApp }) => {
- await frigateApp.goto("/explore");
- await frigateApp.page.waitForTimeout(1000);
- const searchInput = frigateApp.page.locator("input").first();
- if (await searchInput.isVisible()) {
- await searchInput.fill("car in driveway");
- await searchInput.press("Enter");
- await frigateApp.page.waitForTimeout(1000);
- // Page should not crash after search submit
- await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
- }
});
});
-test.describe("Explore Page - Filters @high", () => {
- test("camera filter button opens popover with camera names (desktop)", async ({
- frigateApp,
- }) => {
- if (frigateApp.isMobile) {
- test.skip();
- return;
- }
- await frigateApp.goto("/explore");
- await frigateApp.page.waitForTimeout(1000);
- const camerasBtn = frigateApp.page.getByRole("button", {
- name: /cameras/i,
- });
- if (await camerasBtn.isVisible().catch(() => false)) {
- await camerasBtn.click();
- await frigateApp.page.waitForTimeout(500);
- const popover = frigateApp.page.locator(
- "[data-radix-popper-content-wrapper]",
- );
- await expect(popover.first()).toBeVisible({ timeout: 3_000 });
- // Camera names from config should be in the popover
- await expect(frigateApp.page.getByText("Front Door")).toBeVisible();
- await frigateApp.page.keyboard.press("Escape");
- }
+// ---------------------------------------------------------------------------
+// Filter bar — desktop only
+// Filter buttons appear once hasExistingSearch is true (URL params present).
+// ---------------------------------------------------------------------------
+
+test.describe("Explore — filters (desktop) @high", () => {
+ test.skip(({ frigateApp }) => frigateApp.isMobile, "Desktop popovers");
+
+ test("Cameras popover lists configured cameras", async ({ frigateApp }) => {
+ // Navigate with a labels filter param so the filter bar renders.
+ await frigateApp.goto("/explore?labels=person");
+ // CamerasFilterButton has aria-label="Cameras Filter". Use getByLabel to
+ // match against the accessible name (not the inner "All Cameras" text).
+ const camerasBtn = frigateApp.page.getByLabel("Cameras Filter").first();
+ await expect(camerasBtn).toBeVisible({ timeout: 10_000 });
+ await camerasBtn.click();
+ // DropdownMenu on desktop wraps content in data-radix-popper-content-wrapper.
+ const popover = frigateApp.page.locator(
+ "[data-radix-popper-content-wrapper]",
+ );
+ await expect(popover.first()).toBeVisible({ timeout: 3_000 });
+ await expect(frigateApp.page.getByText("Front Door")).toBeVisible();
});
- test("filter button opens and closes overlay cleanly", async ({
+ test("Labels filter lists labels from config", async ({ frigateApp }) => {
+ // Navigate with an existing search so the filter bar renders.
+ await frigateApp.goto("/explore?labels=person");
+ // GeneralFilterButton has aria-label="Labels". With ?labels=person the
+ // button text shows "Person" (the selected label), but the aria-label
+ // remains "Labels".
+ const labelsBtn = frigateApp.page.getByLabel("Labels").first();
+ await expect(labelsBtn).toBeVisible({ timeout: 10_000 });
+ await labelsBtn.click();
+ // PlatformAwareDialog renders on desktop as a dropdown/popover overlay.
+ const overlay = frigateApp.page.locator(
+ "[data-radix-popper-content-wrapper], [role='dialog'], [data-state='open']",
+ );
+ await expect(overlay.first()).toBeVisible({ timeout: 3_000 });
+ // "person" is already selected (it's in the URL); assert it appears in
+ // the overlay content.
+ await expect(overlay.first().getByText(/person/i)).toBeVisible();
+ await frigateApp.page.keyboard.press("Escape");
+ });
+
+ test("Sub-labels filter renders inside More Filters dialog", async ({
frigateApp,
}) => {
- await frigateApp.goto("/explore");
- await frigateApp.page.waitForTimeout(1000);
- const firstButton = frigateApp.page.locator("#pageRoot button").first();
- await expect(firstButton).toBeVisible({ timeout: 5_000 });
- await firstButton.click();
- await frigateApp.page.waitForTimeout(500);
+ // Sub-labels live inside SearchFilterDialog ("More Filters" button).
+ // With sub_labels mocked as [], the section still renders its heading.
+ await frigateApp.page.route("**/api/sub_labels**", (route) =>
+ route.fulfill({ json: [] }),
+ );
+ await frigateApp.goto("/explore?labels=person");
+ const moreBtn = frigateApp.page.getByLabel("More Filters").first();
+ await expect(moreBtn).toBeVisible({ timeout: 10_000 });
+ await moreBtn.click();
+ const overlay = frigateApp.page.locator(
+ "[data-radix-popper-content-wrapper], [role='dialog'], [data-state='open']",
+ );
+ await expect(overlay.first()).toBeVisible({ timeout: 3_000 });
+ // "Sub Labels" section heading always renders inside the dialog.
+ await expect(
+ frigateApp.page.getByText(/sub.?label/i).first(),
+ ).toBeVisible();
+ await frigateApp.page.keyboard.press("Escape");
+ });
+
+ test("Zones filter lists configured zones inside More Filters dialog", async ({
+ frigateApp,
+ }) => {
+ // Override config to guarantee a known zone on front_door.
+ await frigateApp.installDefaults({
+ config: {
+ cameras: {
+ front_door: {
+ zones: {
+ front_yard: { coordinates: "0.1,0.1,0.9,0.1,0.9,0.9,0.1,0.9" },
+ },
+ },
+ },
+ },
+ });
+ await frigateApp.goto("/explore?labels=person");
+ const moreBtn = frigateApp.page.getByLabel("More Filters").first();
+ await expect(moreBtn).toBeVisible({ timeout: 10_000 });
+ await moreBtn.click();
+ const overlay = frigateApp.page.locator(
+ "[data-radix-popper-content-wrapper], [role='dialog'], [data-state='open']",
+ );
+ await expect(overlay.first()).toBeVisible({ timeout: 3_000 });
+ await expect(frigateApp.page.getByText(/front.?yard/i)).toBeVisible();
await frigateApp.page.keyboard.press("Escape");
- await frigateApp.page.waitForTimeout(300);
- // Page is still functional after open/close cycle
- await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
});
});
-test.describe("Explore Page - Content @high", () => {
- test("explore page shows content with mock events", async ({
+// ---------------------------------------------------------------------------
+// Content
+// ---------------------------------------------------------------------------
+
+test.describe("Explore — content @high", () => {
+ test("page renders with mock events", async ({ frigateApp }) => {
+ await frigateApp.goto("/explore");
+ await expect(frigateApp.page.locator("#pageRoot")).toBeVisible({
+ timeout: 10_000,
+ });
+ await expect(
+ frigateApp.page.locator("#pageRoot button").first(),
+ ).toBeVisible({ timeout: 10_000 });
+ });
+
+ test("empty events renders without crash", async ({ frigateApp }) => {
+ await frigateApp.installDefaults({ events: [] });
+ await frigateApp.goto("/explore");
+ await expect(frigateApp.page.locator("#pageRoot")).toBeVisible({
+ timeout: 10_000,
+ });
+ });
+
+ test("search fires a /api/events request with the query", async ({
frigateApp,
}) => {
+ await frigateApp.installDefaults({ config: SEMANTIC_SEARCH_CONFIG });
+ const eventsRequests: string[] = [];
+ frigateApp.page.on("request", (req) => {
+ const url = req.url();
+ if (/\/api\/events/.test(url)) eventsRequests.push(url);
+ });
await frigateApp.goto("/explore");
- await frigateApp.page.waitForTimeout(3000);
- const pageText = await frigateApp.page.textContent("#pageRoot");
- expect(pageText?.length).toBeGreaterThan(0);
+ const searchInput = frigateApp.page.locator("input").first();
+ await expect(searchInput).toBeVisible({ timeout: 10_000 });
+
+ const before = eventsRequests.length;
+ await searchInput.fill("person in driveway");
+ await searchInput.press("Enter");
+ await expect
+ .poll(() => eventsRequests.length > before, { timeout: 5_000 })
+ .toBe(true);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Similarity search URL param
+// ---------------------------------------------------------------------------
+
+test.describe("Explore — similarity search (desktop) @high", () => {
+ test.skip(
+ ({ frigateApp }) => frigateApp.isMobile,
+ "Similarity trigger is hover-based; desktop-focused",
+ );
+
+ test("URL similarity search params fetch events", async ({ frigateApp }) => {
+ const eventsRequests: string[] = [];
+ frigateApp.page.on("request", (req) => {
+ const url = req.url();
+ if (/\/api\/events/.test(url)) eventsRequests.push(url);
+ });
+ // The app uses search_type=similarity&event_id= (not
+ // similarity_search_id). This exercises the same similarity search code
+ // path as clicking "Find Similar" on a thumbnail.
+ // Use a valid event-id format (timestamp.fractional-alphanumeric).
+ await frigateApp.goto(
+ "/explore?search_type=similarity&event_id=1712412000.000000-abc123",
+ );
+ await expect(frigateApp.page.locator("#pageRoot")).toBeVisible({
+ timeout: 10_000,
+ });
+ // Poll to allow any pending SWR fetch to complete and be captured.
+ await expect
+ .poll(() => eventsRequests.length, { timeout: 5_000 })
+ .toBeGreaterThan(0);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Mobile
+// ---------------------------------------------------------------------------
+
+test.describe("Explore — mobile @high @mobile", () => {
+ test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
+
+ test("search input is focusable at mobile viewport", async ({
+ frigateApp,
+ }) => {
+ await frigateApp.installDefaults({ config: SEMANTIC_SEARCH_CONFIG });
+ await frigateApp.goto("/explore");
+ const searchInput = frigateApp.page.locator("input").first();
+ await expect(searchInput).toBeVisible({ timeout: 10_000 });
+ await searchInput.focus();
+ await expect(searchInput).toBeFocused();
});
});
diff --git a/web/e2e/specs/export.spec.ts b/web/e2e/specs/export.spec.ts
index 4db98d5e95..5b7e9f0b39 100644
--- a/web/e2e/specs/export.spec.ts
+++ b/web/e2e/specs/export.spec.ts
@@ -1,4 +1,114 @@
import { test, expect } from "../fixtures/frigate-test";
+import {
+ expectBodyInteractive,
+ waitForBodyInteractive,
+} from "../helpers/overlay-interaction";
+
+test.describe("Export Page - Delete race @high", () => {
+ // Empirical guard for radix-ui/primitives#3445: when a modal DropdownMenu
+ // opens an AlertDialog and the AlertDialog's confirm action causes the
+ // parent's optimistic cache update to unmount the card, we want to know
+ // whether the deduped react-dismissable-layer (1.1.11) handles the
+ // pointer-events stack cleanup or whether `modal={false}` is still
+ // required on the DropdownMenu. The classic "canonical" pattern, distinct
+ // from the FaceSelectionDialog auto-unmount race already covered by
+ // face-library.spec.ts.
+ test("deleting an export via dropdown→alert→confirm leaves body interactive", async ({
+ frigateApp,
+ }) => {
+ if (frigateApp.isMobile) {
+ test.skip();
+ return;
+ }
+
+ const initialExports = [
+ {
+ id: "export-race-001",
+ camera: "front_door",
+ name: "Race - Test Export",
+ date: 1775490731.3863528,
+ video_path: "/exports/export-race-001.mp4",
+ thumb_path: "/exports/export-race-001-thumb.jpg",
+ in_progress: false,
+ export_case_id: null,
+ },
+ ];
+ let deleted = false;
+
+ await frigateApp.installDefaults({
+ exports: initialExports,
+ });
+
+ // Flip /api/export to empty after the delete POST is observed so the
+ // page's SWR mutate sees the export gone.
+ await frigateApp.page.route("**/api/export**", async (route) => {
+ const payload = deleted ? [] : initialExports;
+ await route.fulfill({ json: payload });
+ });
+ await frigateApp.page.route("**/api/exports/delete", async (route) => {
+ deleted = true;
+ const delayMs = Number(
+ (globalThis as { process?: { env?: Record } }).process
+ ?.env?.DELETE_DELAY_MS ?? "100",
+ );
+ if (delayMs > 0) {
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
+ }
+ await route.fulfill({ json: { success: true } });
+ });
+
+ await frigateApp.goto("/export");
+ await expect(frigateApp.page.getByText("Race - Test Export")).toBeVisible({
+ timeout: 5_000,
+ });
+
+ // Open the kebab menu on the export card. The kebab uses the
+ // (misleading) aria-label "Edit name" from ExportCard's source — it
+ // wraps the FiMoreVertical icon. There is exactly one such button on
+ // the page once we have a single export rendered.
+ const kebab = frigateApp.page
+ .getByRole("button", { name: /edit name/i })
+ .first();
+ await expect(kebab).toBeVisible({ timeout: 5_000 });
+ await kebab.click();
+
+ const menu = frigateApp.page
+ .locator('[role="menu"], [data-radix-menu-content]')
+ .first();
+ await expect(menu).toBeVisible({ timeout: 3_000 });
+
+ // Delete Export
+ await menu
+ .getByRole("menuitem", { name: /delete export/i })
+ .first()
+ .click();
+
+ // AlertDialog at page level. The confirm button's accessible name is
+ // "Delete Export" (its aria-label), the visible text is just "Delete".
+ const confirm = frigateApp.page.getByRole("alertdialog");
+ await expect(confirm).toBeVisible({ timeout: 3_000 });
+ await confirm
+ .getByRole("button", { name: /^delete export$/i })
+ .first()
+ .click();
+
+ // The card optimistically disappears, the dialog closes, and body
+ // pointer-events must come unstuck.
+ await expect(
+ frigateApp.page.getByText("Race - Test Export"),
+ ).not.toBeVisible({ timeout: 5_000 });
+ await waitForBodyInteractive(frigateApp.page, 5_000);
+ await expectBodyInteractive(frigateApp.page);
+
+ // Sanity: another page-level button still responds.
+ const newCase = frigateApp.page.getByRole("button", { name: /new case/i });
+ await expect(newCase).toBeVisible({ timeout: 3_000 });
+ await newCase.click();
+ await expect(
+ frigateApp.page.getByRole("dialog").filter({ hasText: /create case/i }),
+ ).toBeVisible({ timeout: 3_000 });
+ });
+});
test.describe("Export Page - Overview @high", () => {
test("renders uncategorized exports and case cards from mock data", async ({
diff --git a/web/e2e/specs/face-library.spec.ts b/web/e2e/specs/face-library.spec.ts
index d68b8f8a53..e499178b7b 100644
--- a/web/e2e/specs/face-library.spec.ts
+++ b/web/e2e/specs/face-library.spec.ts
@@ -1,32 +1,542 @@
/**
- * Face Library page tests -- MEDIUM tier.
+ * Face Library page tests -- HIGH tier.
*
- * Tests face grid rendering, empty state, and interactive controls.
+ * Collection selector, face tiles, grouped recent-recognition dialog
+ * (migrated from radix-overlay-regressions.spec.ts), and mobile
+ * library selector.
*/
-import { test, expect } from "../fixtures/frigate-test";
+import { type Locator } from "@playwright/test";
+import { test, expect, type FrigateApp } from "../fixtures/frigate-test";
+import {
+ basicFacesMock,
+ emptyFacesMock,
+ withGroupedTrainingAttempt,
+} from "../fixtures/mock-data/faces";
+import {
+ expectBodyInteractive,
+ waitForBodyInteractive,
+} from "../helpers/overlay-interaction";
-test.describe("Face Library @medium", () => {
- test("face library page renders without crash", async ({ frigateApp }) => {
+const GROUPED_EVENT_ID = "1775487131.3863528-abc123";
+
+function groupedFacesMock() {
+ return withGroupedTrainingAttempt(basicFacesMock(), {
+ eventId: GROUPED_EVENT_ID,
+ attempts: [
+ { timestamp: 1775487131.3863528, label: "unknown", score: 0.95 },
+ { timestamp: 1775487132.3863528, label: "unknown", score: 0.91 },
+ ],
+ });
+}
+
+async function installGroupedFaces(app: FrigateApp) {
+ await app.api.install({
+ events: [
+ {
+ id: GROUPED_EVENT_ID,
+ label: "person",
+ sub_label: null,
+ camera: "front_door",
+ start_time: 1775487131.3863528,
+ end_time: 1775487161.3863528,
+ false_positive: false,
+ zones: ["front_yard"],
+ thumbnail: null,
+ has_clip: true,
+ has_snapshot: true,
+ retain_indefinitely: false,
+ plus_id: null,
+ model_hash: "abc123",
+ detector_type: "cpu",
+ model_type: "ssd",
+ data: {
+ top_score: 0.92,
+ score: 0.92,
+ region: [0.1, 0.1, 0.5, 0.8],
+ box: [0.2, 0.15, 0.45, 0.75],
+ area: 0.18,
+ ratio: 0.6,
+ type: "object",
+ path_data: [],
+ },
+ },
+ ],
+ faces: groupedFacesMock(),
+ });
+}
+
+async function openGroupedFaceDialog(app: FrigateApp): Promise {
+ await installGroupedFaces(app);
+ await app.goto("/faces");
+ const groupedImage = app.page
+ .locator('img[src*="clips/faces/train/"]')
+ .first();
+ const groupedCard = groupedImage.locator("xpath=..");
+ await expect(groupedImage).toBeVisible({ timeout: 5_000 });
+ await groupedCard.click();
+ const dialog = app.page
+ .getByRole("dialog")
+ .filter({ has: app.page.locator('img[src*="clips/faces/train/"]') })
+ .first();
+ await expect(dialog).toBeVisible({ timeout: 5_000 });
+ await expect(dialog.locator('img[src*="clips/faces/train/"]')).toHaveCount(2);
+ return dialog;
+}
+
+/**
+ * Opens the LibrarySelector dropdown (the single button at the top-left of
+ * the Face Library page) and returns the dropdown menu locator.
+ *
+ * The LibrarySelector is a single DropdownMenu whose trigger shows the
+ * current tab name + count (e.g. "Recent Recognitions (0)"). Named face
+ * collections (alice, bob, charlie) are items inside this dropdown.
+ */
+async function openLibraryDropdown(app: FrigateApp): Promise {
+ // The trigger is the first button on the page with a parenthesised count.
+ const trigger = app.page
+ .getByRole("button")
+ .filter({ hasText: /\(\d+\)/ })
+ .first();
+ await expect(trigger).toBeVisible({ timeout: 10_000 });
+ await trigger.click();
+ const menu = app.page
+ .locator('[role="menu"], [data-radix-menu-content]')
+ .first();
+ await expect(menu).toBeVisible({ timeout: 5_000 });
+ return menu;
+}
+
+test.describe("Face Library — collection selector @high", () => {
+ test("selector shows named face collections", async ({ frigateApp }) => {
+ await frigateApp.installDefaults({ faces: basicFacesMock() });
+ await frigateApp.goto("/faces");
+ // Named collections appear in the LibrarySelector dropdown.
+ const menu = await openLibraryDropdown(frigateApp);
+ await expect(menu.getByText(/alice/i).first()).toBeVisible({
+ timeout: 5_000,
+ });
+ });
+
+ test("empty state renders when no faces exist", async ({ frigateApp }) => {
+ await frigateApp.installDefaults({ faces: emptyFacesMock() });
await frigateApp.goto("/faces");
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
+ await expect(
+ frigateApp.page.locator('img[src*="/clips/faces/"]'),
+ ).toHaveCount(0);
});
- test("face library shows empty state with no faces", async ({
- frigateApp,
- }) => {
+ test("tiles render for each named collection", async ({ frigateApp }) => {
+ await frigateApp.installDefaults({ faces: basicFacesMock() });
await frigateApp.goto("/faces");
- await frigateApp.page.waitForTimeout(2000);
- // With empty faces mock, should show empty state or content
- const text = await frigateApp.page.textContent("#pageRoot");
- expect(text?.length).toBeGreaterThan(0);
+ // Open the dropdown — collections list shows "alice (2)" and "bob (1)".
+ const menu = await openLibraryDropdown(frigateApp);
+ await expect(
+ menu.locator('[role="menuitem"]').filter({ hasText: /alice/i }).first(),
+ ).toBeVisible({ timeout: 5_000 });
+ await expect(
+ menu.locator('[role="menuitem"]').filter({ hasText: /bob/i }).first(),
+ ).toBeVisible();
});
+});
- test("face library has interactive buttons", async ({ frigateApp }) => {
+test.describe("Face Library — delete flow (desktop) @high", () => {
+ test.skip(
+ ({ frigateApp }) => frigateApp.isMobile,
+ "Delete action menu is desktop-focused",
+ );
+
+ test("deleting a collection fires POST /faces//delete", async ({
+ frigateApp,
+ }) => {
+ let deleteUrl: string | null = null;
+ let deleteBody: unknown = null;
+ // Install base mocks first, then register our more-specific route AFTER
+ // so it takes priority over the ApiMocker catch-all (Playwright LIFO order).
+ await frigateApp.installDefaults({ faces: basicFacesMock() });
+ await frigateApp.page.route(
+ /\/api\/faces\/[^/]+\/delete/,
+ async (route) => {
+ deleteUrl = route.request().url();
+ deleteBody = route.request().postDataJSON();
+ await route.fulfill({ json: { success: true } });
+ },
+ );
+ await frigateApp.goto("/faces");
+
+ // Open the LibrarySelector dropdown and click the trash icon next
+ // to the alice row. The trash icon is a ghost-variant Button inside
+ // the DropdownMenuItem — it becomes visible on hover/focus.
+ const menu = await openLibraryDropdown(frigateApp);
+ const aliceRow = menu
+ .locator('[role="menuitem"]')
+ .filter({ hasText: /alice/i })
+ .first();
+ await expect(aliceRow).toBeVisible({ timeout: 5_000 });
+ // Hover first to make hover-only opacity-0 buttons visible.
+ await aliceRow.hover();
+ // The icon buttons have no aria-label or title. The row renders exactly
+ // two buttons in fixed source order: [0] LuPencil (rename), [1] LuTrash2
+ // (delete). This order is determined by FaceLibrary.tsx and is stable.
+ const trashBtn = aliceRow.locator("button").nth(1);
+ await trashBtn.click();
+
+ // The delete confirmation is a Dialog (not AlertDialog) in this flow.
+ const dialog = frigateApp.page.getByRole("dialog");
+ await expect(dialog).toBeVisible({ timeout: 5_000 });
+ await dialog
+ .getByRole("button", { name: /delete/i })
+ .first()
+ .click();
+
+ await expect
+ .poll(() => deleteUrl, { timeout: 5_000 })
+ .toMatch(/\/faces\/alice\/delete/);
+ expect(deleteBody).toMatchObject({ ids: expect.any(Array) });
+ });
+});
+
+test.describe("Face Library — rename flow (desktop) @high", () => {
+ test.skip(
+ ({ frigateApp }) => frigateApp.isMobile,
+ "Rename action menu is desktop-focused",
+ );
+
+ test("renaming a collection fires PUT /faces//rename", async ({
+ frigateApp,
+ }) => {
+ let renameUrl: string | null = null;
+ let renameBody: unknown = null;
+ // Install base mocks first, then register our more-specific route AFTER
+ // so it takes priority over the ApiMocker catch-all (Playwright LIFO order).
+ await frigateApp.installDefaults({ faces: basicFacesMock() });
+ await frigateApp.page.route(
+ /\/api\/faces\/[^/]+\/rename/,
+ async (route) => {
+ renameUrl = route.request().url();
+ renameBody = route.request().postDataJSON();
+ await route.fulfill({ json: { success: true } });
+ },
+ );
+ await frigateApp.goto("/faces");
+
+ // Open the LibrarySelector dropdown and click the pencil (rename) icon
+ // next to alice. The icon is a ghost Button inside the DropdownMenuItem.
+ const menu = await openLibraryDropdown(frigateApp);
+ const aliceRow = menu
+ .locator('[role="menuitem"]')
+ .filter({ hasText: /alice/i })
+ .first();
+ await expect(aliceRow).toBeVisible({ timeout: 5_000 });
+ await aliceRow.hover();
+ // The icon buttons have no aria-label or title. The row renders exactly
+ // two buttons in fixed source order: [0] LuPencil (rename), [1] LuTrash2
+ // (delete). This order is determined by FaceLibrary.tsx and is stable.
+ const pencilBtn = aliceRow.locator("button").nth(0);
+ await pencilBtn.click();
+
+ // TextEntryDialog — fill the input and confirm.
+ const dialog = frigateApp.page.getByRole("dialog");
+ await expect(dialog).toBeVisible({ timeout: 5_000 });
+ await dialog.locator("input").first().fill("alice_renamed");
+ await dialog
+ .getByRole("button", { name: /save|rename|confirm/i })
+ .first()
+ .click();
+
+ await expect
+ .poll(() => renameUrl, { timeout: 5_000 })
+ .toMatch(/\/faces\/alice\/rename/);
+ expect(renameBody).toEqual({ new_name: "alice_renamed" });
+ });
+});
+
+test.describe("Face Library — upload flow @high", () => {
+ test.skip(
+ ({ frigateApp }) => frigateApp.isMobile,
+ "Upload button has no accessible text on mobile — icon-only on narrow viewports",
+ );
+
+ test("Upload button opens the upload dialog", async ({ frigateApp }) => {
+ await frigateApp.installDefaults({ faces: basicFacesMock() });
+ await frigateApp.goto("/faces");
+
+ // Navigate to the alice tab by opening the dropdown and clicking alice.
+ const menu = await openLibraryDropdown(frigateApp);
+ await menu
+ .locator('[role="menuitem"]')
+ .filter({ hasText: /alice/i })
+ .first()
+ .click();
+
+ // After switching to alice, the Upload Image button appears in the toolbar.
+ const uploadBtn = frigateApp.page
+ .getByRole("button")
+ .filter({ hasText: /upload/i })
+ .first();
+ await expect(uploadBtn).toBeVisible({ timeout: 5_000 });
+ await uploadBtn.click();
+
+ // UploadImageDialog renders a file input + confirm button.
+ const dialog = frigateApp.page.getByRole("dialog");
+ await expect(dialog).toBeVisible({ timeout: 5_000 });
+ await expect(dialog.locator('input[type="file"]')).toHaveCount(1);
+ });
+});
+
+test.describe("FaceSelectionDialog @high", () => {
+ test.skip(
+ ({ frigateApp }) => frigateApp.isMobile,
+ "Grouped dropdown flow is desktop-only",
+ );
+
+ test("reclassify dropdown selects a name and closes cleanly", async ({
+ frigateApp,
+ }) => {
+ // Migrated from radix-overlay-regressions.spec.ts.
+ const dialog = await openGroupedFaceDialog(frigateApp);
+ const triggers = dialog.locator('[aria-haspopup="menu"]');
+ await expect(triggers).toHaveCount(2);
+
+ await triggers.first().click();
+ const menu = frigateApp.page
+ .locator('[role="menu"], [data-radix-menu-content]')
+ .first();
+ await expect(menu).toBeVisible({ timeout: 5_000 });
+
+ await menu.getByRole("menuitem", { name: /^bob$/i }).click();
+
+ await expect(menu).not.toBeVisible({ timeout: 3_000 });
+ await expect(dialog).toBeVisible();
+
+ const tooltipVisible = await frigateApp.page
+ .locator('[role="tooltip"]')
+ .filter({ hasText: /train face/i })
+ .isVisible()
+ .catch(() => false);
+ expect(
+ tooltipVisible,
+ "Train Face tooltip popped after dropdown closed — focus-restore regression",
+ ).toBe(false);
+ });
+
+ test("second dropdown open accepts typeahead keyboard input", async ({
+ frigateApp,
+ }) => {
+ // Migrated from radix-overlay-regressions.spec.ts.
+ const dialog = await openGroupedFaceDialog(frigateApp);
+ const triggers = dialog.locator('[aria-haspopup="menu"]');
+ await expect(triggers).toHaveCount(2);
+
+ await triggers.first().click();
+ let menu = frigateApp.page
+ .locator('[role="menu"], [data-radix-menu-content]')
+ .first();
+ await expect(menu).toBeVisible({ timeout: 5_000 });
+ await menu.getByRole("menuitem", { name: /^bob$/i }).click();
+ await expect(menu).not.toBeVisible({ timeout: 3_000 });
+
+ await triggers.nth(1).click();
+ menu = frigateApp.page
+ .locator('[role="menu"], [data-radix-menu-content]')
+ .first();
+ await expect(menu).toBeVisible({ timeout: 5_000 });
+
+ await frigateApp.page.keyboard.press("c");
+ await expect
+ .poll(
+ async () =>
+ frigateApp.page.evaluate(
+ () =>
+ document.activeElement?.textContent?.trim().toLowerCase() ?? "",
+ ),
+ { timeout: 2_000 },
+ )
+ .toMatch(/^charlie/);
+
+ await frigateApp.page.keyboard.press("Escape");
+ await expect(menu).not.toBeVisible({ timeout: 3_000 });
+ });
+
+ test("classifying the last image in a group leaves body interactive", async ({
+ frigateApp,
+ }) => {
+ // Regression guard for the stuck body pointer-events bug when the
+ // last image in a grouped-recognition detail Dialog is classified.
+ // Tracked upstream at radix-ui/primitives#3445.
+ //
+ // Root cause: when the user clicks a FaceSelectionDialog menu item,
+ // the modal DropdownMenu enters its exit animation (Radix's Presence
+ // keeps it in the DOM with data-state="closed" until animationend).
+ // While that is in flight the classify axios resolves, SWR removes
+ // the image from /api/faces, the parent's map no longer renders the
+ // grouped card, and React unmounts the subtree — including the still-
+ // animating DropdownMenu's Presence container. DismissableLayer's
+ // shared modal-layer stack can't reconcile the interrupted exit, so
+ // the `body { pointer-events: none }` entry it put on mount is never
+ // popped and the rest of the UI becomes unclickable.
+ //
+ // The fix is `modal={false}` on the FaceSelectionDialog's
+ // DropdownMenu (desktop path only). With modal=false the DropdownMenu
+ // never puts an entry on DismissableLayer's body-pointer-events stack
+ // in the first place, so there's nothing to leak when its Presence is
+ // torn down mid-animation. The Radix-community-documented workaround
+ // for #3445.
+ //
+ // The bug only reproduces when the mock resolves fast enough that
+ // the parent unmounts before the dropdown's exit animation finishes.
+ // Measured window via a 3x sweep on the pre-fix build: 0–200 ms
+ // triggers it; 300 ms+ no longer reproduces. Production LAN networks
+ // sit comfortably inside the bad window, while `npm run dev` seems
+ // to mask it via React StrictMode's double-effect scheduling.
+ const EVENT_ID = "1775487131.3863528-race";
+ const initialFaces = withGroupedTrainingAttempt(basicFacesMock(), {
+ eventId: EVENT_ID,
+ attempts: [
+ { timestamp: 1775487131.3863528, label: "unknown", score: 0.95 },
+ ],
+ });
+
+ let classified = false;
+
+ await frigateApp.installDefaults({
+ faces: initialFaces,
+ events: [
+ {
+ id: EVENT_ID,
+ label: "person",
+ sub_label: null,
+ camera: "front_door",
+ start_time: 1775487131.3863528,
+ end_time: 1775487161.3863528,
+ false_positive: false,
+ zones: ["front_yard"],
+ thumbnail: null,
+ has_clip: true,
+ has_snapshot: true,
+ retain_indefinitely: false,
+ plus_id: null,
+ model_hash: "abc123",
+ detector_type: "cpu",
+ model_type: "ssd",
+ data: {
+ top_score: 0.92,
+ score: 0.92,
+ region: [0.1, 0.1, 0.5, 0.8],
+ box: [0.2, 0.15, 0.45, 0.75],
+ area: 0.18,
+ ratio: 0.6,
+ type: "object",
+ path_data: [],
+ },
+ },
+ ],
+ });
+
+ // Re-route /api/faces to flip to the "train empty" payload once the
+ // classify POST has been received. Registered AFTER installDefaults so
+ // Playwright's LIFO route matching hits this handler first.
+ await frigateApp.page.route("**/api/faces", async (route) => {
+ const payload = classified ? basicFacesMock() : initialFaces;
+ await route.fulfill({ json: payload });
+ });
+
+ // Hold the classify POST briefly. The race opens when the parent
+ // unmounts before the dropdown's exit animation finishes (~200ms
+ // in Radix). 100ms keeps us comfortably inside that window and
+ // reliably triggered the bug in a 3x sweep across 0/50/100/200ms
+ // on the pre-fix build. CLASSIFY_DELAY_MS overrides for local sweeps.
+ const delayMs = Number(
+ (globalThis as { process?: { env?: Record } }).process
+ ?.env?.CLASSIFY_DELAY_MS ?? "100",
+ );
+ await frigateApp.page.route(
+ "**/api/faces/train/*/classify",
+ async (route) => {
+ classified = true;
+ if (delayMs > 0) {
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
+ }
+ await route.fulfill({ json: { success: true } });
+ },
+ );
+
await frigateApp.goto("/faces");
- await frigateApp.page.waitForTimeout(2000);
- const buttons = frigateApp.page.locator("#pageRoot button");
- const count = await buttons.count();
- expect(count).toBeGreaterThanOrEqual(0);
+
+ // Open the grouped detail Dialog.
+ const groupedImage = frigateApp.page
+ .locator('img[src*="clips/faces/train/"]')
+ .first();
+ await expect(groupedImage).toBeVisible({ timeout: 5_000 });
+ await groupedImage.locator("xpath=..").click();
+ const dialog = frigateApp.page
+ .getByRole("dialog")
+ .filter({
+ has: frigateApp.page.locator('img[src*="clips/faces/train/"]'),
+ })
+ .first();
+ await expect(dialog).toBeVisible({ timeout: 5_000 });
+
+ // Single attempt → single `+` trigger.
+ const triggers = dialog.locator('[aria-haspopup="menu"]');
+ await expect(triggers).toHaveCount(1);
+ await triggers.first().click();
+
+ const menu = frigateApp.page
+ .locator('[role="menu"], [data-radix-menu-content]')
+ .first();
+ await expect(menu).toBeVisible({ timeout: 5_000 });
+ await menu.getByRole("menuitem", { name: /^alice$/i }).click();
+
+ // The Dialog must leave the tree cleanly, and body must recover.
+ await expect(dialog).not.toBeVisible({ timeout: 5_000 });
+
+ // Give Radix's exit animation + cleanup a comfortable margin on top of
+ // the ~300ms simulated network delay.
+ await waitForBodyInteractive(frigateApp.page, 5_000);
+ await expectBodyInteractive(frigateApp.page);
+
+ // User-visible confirmation: click something outside the dialog
+ // and assert it actually responds.
+ const librarySelector = frigateApp.page
+ .getByRole("button")
+ .filter({ hasText: /\(\d+\)/ })
+ .first();
+ await librarySelector.click();
+ await expect(
+ frigateApp.page
+ .locator('[role="menu"], [data-radix-menu-content]')
+ .first(),
+ ).toBeVisible({ timeout: 3_000 });
+ });
+});
+
+test.describe("Face Library — mobile @high @mobile", () => {
+ test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
+
+ test("mobile library selector dropdown closes cleanly on Escape", async ({
+ frigateApp,
+ }) => {
+ // Migrated from radix-overlay-regressions.spec.ts.
+ await installGroupedFaces(frigateApp);
+ await frigateApp.goto("/faces");
+
+ const selector = frigateApp.page
+ .getByRole("button")
+ .filter({ hasText: /\(\d+\)/ })
+ .first();
+ await expect(selector).toBeVisible({ timeout: 5_000 });
+ await selector.click();
+
+ const menu = frigateApp.page
+ .locator('[role="menu"], [data-radix-menu-content]')
+ .first();
+ await expect(menu).toBeVisible({ timeout: 3_000 });
+
+ await frigateApp.page.keyboard.press("Escape");
+ await expect(menu).not.toBeVisible({ timeout: 3_000 });
+ await waitForBodyInteractive(frigateApp.page);
+ await expectBodyInteractive(frigateApp.page);
});
});
diff --git a/web/e2e/specs/live.spec.ts b/web/e2e/specs/live.spec.ts
index e355984b33..af28f00307 100644
--- a/web/e2e/specs/live.spec.ts
+++ b/web/e2e/specs/live.spec.ts
@@ -1,59 +1,47 @@
/**
* Live page tests -- CRITICAL tier.
*
- * Tests camera dashboard rendering, camera card clicks, single camera view
- * with named controls, feature toggle behavior, context menu, and mobile layout.
+ * Dashboard grid, single-camera controls, feature toggles (with WS
+ * frame assertions), context menu, birdseye, and mobile layout.
+ * Also absorbs the PTZ preset-dropdown regression tests from the
+ * now-deleted ptz-overlay.spec.ts.
*/
import { test, expect } from "../fixtures/frigate-test";
+import { LivePage } from "../pages/live.page";
+import { installWsFrameCapture, waitForWsFrame } from "../helpers/ws-frames";
+import {
+ expectBodyInteractive,
+ waitForBodyInteractive,
+} from "../helpers/overlay-interaction";
+
+const PTZ_CAMERA = "front_door";
+const PRESET_NAMES = ["home", "driveway", "front_porch"];
test.describe("Live Dashboard @critical", () => {
- test("dashboard renders all configured cameras by name", async ({
+ test("every configured camera renders on the dashboard", async ({
frigateApp,
}) => {
await frigateApp.goto("/");
+ const live = new LivePage(frigateApp.page, !frigateApp.isMobile);
for (const cam of ["front_door", "backyard", "garage"]) {
- await expect(
- frigateApp.page.locator(`[data-camera='${cam}']`),
- ).toBeVisible({ timeout: 10_000 });
+ await expect(live.cameraCard(cam)).toBeVisible({ timeout: 10_000 });
}
});
- test("clicking camera card opens single camera view via hash", async ({
+ test("clicking a camera card opens the single-camera view via hash", async ({
frigateApp,
}) => {
await frigateApp.goto("/");
- const card = frigateApp.page.locator("[data-camera='front_door']").first();
- await card.click({ timeout: 10_000 });
+ const live = new LivePage(frigateApp.page, !frigateApp.isMobile);
+ await live.cameraCard("front_door").first().click({ timeout: 10_000 });
await expect(frigateApp.page).toHaveURL(/#front_door/);
});
- test("back button returns from single camera to dashboard", async ({
- frigateApp,
- }) => {
- // First navigate to dashboard so there's history to go back to
- await frigateApp.goto("/");
- await frigateApp.page.waitForTimeout(1000);
- // Click a camera to enter single view
- const card = frigateApp.page.locator("[data-camera='front_door']").first();
- await card.click({ timeout: 10_000 });
- await frigateApp.page.waitForTimeout(2000);
- // Now click Back to return to dashboard
- const backBtn = frigateApp.page.getByText("Back", { exact: true });
- if (await backBtn.isVisible().catch(() => false)) {
- await backBtn.click();
- await frigateApp.page.waitForTimeout(1000);
- }
- // Should be back on the dashboard with cameras visible
- await expect(
- frigateApp.page.locator("[data-camera='front_door']"),
- ).toBeVisible({ timeout: 10_000 });
- });
-
- test("birdseye view loads without crash", async ({ frigateApp }) => {
+ test("birdseye route renders without crash", async ({ frigateApp }) => {
await frigateApp.goto("/#birdseye");
- await frigateApp.page.waitForTimeout(2000);
await expect(frigateApp.page.locator("body")).toBeVisible();
+ await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
});
test("empty group shows fallback content", async ({ frigateApp }) => {
@@ -63,191 +51,239 @@ test.describe("Live Dashboard @critical", () => {
});
});
-test.describe("Live Single Camera - Controls @critical", () => {
- test("single camera view shows Back and History buttons (desktop)", async ({
+test.describe("Live Single Camera — desktop controls @critical", () => {
+ test.skip(
+ ({ frigateApp }) => frigateApp.isMobile,
+ "Desktop-only header controls",
+ );
+
+ test("single-camera view shows Back and History buttons", async ({
frigateApp,
}) => {
- if (frigateApp.isMobile) {
- test.skip(); // On mobile, buttons may show icons only
- return;
- }
await frigateApp.goto("/#front_door");
- await frigateApp.page.waitForTimeout(2000);
- // Back and History are visible text buttons in the header
- await expect(
- frigateApp.page.getByText("Back", { exact: true }),
- ).toBeVisible({ timeout: 5_000 });
- await expect(
- frigateApp.page.getByText("History", { exact: true }),
- ).toBeVisible();
+ const live = new LivePage(frigateApp.page, true);
+ await expect(live.backButton).toBeVisible({ timeout: 5_000 });
+ await expect(live.historyButton).toBeVisible();
});
- test("single camera view shows feature toggle icons (desktop)", async ({
- frigateApp,
- }) => {
- if (frigateApp.isMobile) {
- test.skip();
- return;
- }
+ test("feature toggles render (at least 3)", async ({ frigateApp }) => {
await frigateApp.goto("/#front_door");
- await frigateApp.page.waitForTimeout(2000);
- // Feature toggles are CameraFeatureToggle components rendered as divs
- // with bg-selected (active) or bg-secondary (inactive) classes
- // Count the toggles - should have at least detect, recording, snapshots
- const toggles = frigateApp.page.locator(
- ".flex.flex-col.items-center.justify-center.bg-selected, .flex.flex-col.items-center.justify-center.bg-secondary",
- );
- const count = await toggles.count();
+ const live = new LivePage(frigateApp.page, true);
+ // Wait for the single-camera header to render before counting toggles.
+ await expect(live.backButton).toBeVisible({ timeout: 5_000 });
+ await expect(live.featureToggles.first()).toBeVisible({ timeout: 5_000 });
+ const count = await live.featureToggles.count();
expect(count).toBeGreaterThanOrEqual(3);
});
- test("clicking a feature toggle changes its visual state (desktop)", async ({
+ test("clicking a feature toggle sends the matching WS frame", async ({
frigateApp,
}) => {
- if (frigateApp.isMobile) {
- test.skip();
- return;
- }
+ await installWsFrameCapture(frigateApp.page);
await frigateApp.goto("/#front_door");
- await frigateApp.page.waitForTimeout(2000);
- // Find active toggles (bg-selected class = feature is ON)
- const activeToggles = frigateApp.page.locator(
- ".flex.flex-col.items-center.justify-center.bg-selected",
+ const live = new LivePage(frigateApp.page, true);
+ // Wait for feature toggles to render (WS camera_activity must arrive first).
+ await expect(live.activeFeatureToggles.first()).toBeVisible({
+ timeout: 5_000,
+ });
+ const activeBefore = await live.activeFeatureToggles.count();
+ expect(activeBefore).toBeGreaterThan(0);
+
+ await live.activeFeatureToggles.first().click();
+
+ // The toggle dispatches a frame on //set — match on
+ // front_door/ prefix + /set suffix (any feature).
+ await waitForWsFrame(
+ frigateApp.page,
+ (frame) => frame.includes("front_door/") && frame.includes("/set"),
+ {
+ message:
+ "feature toggle should dispatch a //set frame",
+ },
);
- const initialCount = await activeToggles.count();
- if (initialCount > 0) {
- // Click the first active toggle to disable it
- await activeToggles.first().click();
- await frigateApp.page.waitForTimeout(1000);
- // After WS mock echoes back new state, count should decrease
- const newCount = await activeToggles.count();
- expect(newCount).toBeLessThan(initialCount);
- }
});
- test("settings gear button opens dropdown (desktop)", async ({
- frigateApp,
- }) => {
- if (frigateApp.isMobile) {
- test.skip();
- return;
- }
- await frigateApp.goto("/#front_door");
- await frigateApp.page.waitForTimeout(2000);
- // Find the gear icon button (last button-like element in header)
- // The settings gear opens a dropdown with Stream, Play in background, etc.
- const gearButtons = frigateApp.page.locator("button:has(svg)");
- const count = await gearButtons.count();
- // Click the last one (gear icon is typically last in the header)
- if (count > 0) {
- await gearButtons.last().click();
- await frigateApp.page.waitForTimeout(500);
- // A dropdown or drawer should appear
- const overlay = frigateApp.page.locator(
- '[role="menu"], [data-radix-menu-content], [role="dialog"]',
- );
- const visible = await overlay
- .first()
- .isVisible()
- .catch(() => false);
- if (visible) {
- await frigateApp.page.keyboard.press("Escape");
- }
- }
- });
-
- test("keyboard shortcut f does not crash on desktop", async ({
- frigateApp,
- }) => {
- if (frigateApp.isMobile) {
- test.skip();
- return;
- }
+ test("keyboard shortcut f does not crash", async ({ frigateApp }) => {
await frigateApp.goto("/");
await frigateApp.page.keyboard.press("f");
- await frigateApp.page.waitForTimeout(500);
+ await expect(frigateApp.page.locator("body")).toBeVisible();
+ // Note: headless Chromium rejects fullscreen requests without a user
+ // gesture, so document.fullscreenElement cannot be asserted reliably
+ // in e2e. We assert the keypress doesn't crash the app; real
+ // fullscreen behavior is covered by manual testing.
+ });
+
+ test("settings gear opens a dropdown with Stream/Play menu items", async ({
+ frigateApp,
+ }) => {
+ await frigateApp.goto("/#front_door");
+ // Wait for the single-camera view to render — use the Back button
+ // as a deterministic marker.
+ const live = new LivePage(frigateApp.page, true);
+ await expect(live.backButton).toBeVisible({ timeout: 10_000 });
+
+ // The gear icon button is the last button-like element in the
+ // single-camera header. Clicking it opens a Radix dropdown.
+ const gearButtons = frigateApp.page.locator("button:has(svg)");
+ const count = await gearButtons.count();
+ expect(count).toBeGreaterThan(0);
+ await gearButtons.last().click();
+
+ const menu = frigateApp.page
+ .locator('[role="menu"], [data-radix-menu-content]')
+ .first();
+ await expect(menu).toBeVisible({ timeout: 3_000 });
+ await frigateApp.page.keyboard.press("Escape");
+ await expect(menu).not.toBeVisible({ timeout: 3_000 });
+ });
+});
+
+test.describe("Live Context Menu (desktop) @critical", () => {
+ test.skip(
+ ({ frigateApp }) => frigateApp.isMobile,
+ "Right-click is desktop-only",
+ );
+
+ test("right-click opens the context menu", async ({ frigateApp }) => {
+ await frigateApp.goto("/");
+ const live = new LivePage(frigateApp.page, true);
+ const menu = await live.openContextMenuOn("front_door");
+ await expect(menu).toBeVisible({ timeout: 5_000 });
+ });
+
+ test("context menu closes on Escape and leaves body interactive", async ({
+ frigateApp,
+ }) => {
+ await frigateApp.goto("/");
+ const live = new LivePage(frigateApp.page, true);
+ const menu = await live.openContextMenuOn("front_door");
+ await expect(menu).toBeVisible({ timeout: 5_000 });
+ await frigateApp.page.keyboard.press("Escape");
+ await expect(menu).not.toBeVisible();
+ await waitForBodyInteractive(frigateApp.page);
+ await expectBodyInteractive(frigateApp.page);
+ });
+});
+
+test.describe("Live PTZ preset dropdown @critical", () => {
+ // Migrated from ptz-overlay.spec.ts. Guards:
+ // 1. After selecting a preset, the "Presets" tooltip must not re-pop.
+ // 2. Keyboard shortcuts after close should not re-open the dropdown.
+
+ test("selecting a preset closes menu cleanly and does not re-open on keyboard", async ({
+ frigateApp,
+ }) => {
+ test.skip(frigateApp.isMobile, "PTZ preset dropdown is desktop-only");
+
+ await frigateApp.api.install({
+ config: {
+ cameras: {
+ [PTZ_CAMERA]: { onvif: { host: "10.0.0.50" } },
+ },
+ },
+ });
+ await frigateApp.page.route(`**/api/${PTZ_CAMERA}/ptz/info`, (route) =>
+ route.fulfill({
+ json: {
+ name: PTZ_CAMERA,
+ features: ["pt", "zoom"],
+ presets: PRESET_NAMES,
+ profiles: [],
+ },
+ }),
+ );
+
+ await installWsFrameCapture(frigateApp.page);
+ await frigateApp.goto(`/#${PTZ_CAMERA}`);
+
+ const presetTrigger = frigateApp.page.getByRole("button", {
+ name: /presets/i,
+ });
+ await expect(presetTrigger.first()).toBeVisible({ timeout: 5_000 });
+ await presetTrigger.first().click();
+
+ const menu = frigateApp.page
+ .locator('[role="menu"], [data-radix-menu-content]')
+ .first();
+ await expect(menu).toBeVisible({ timeout: 3_000 });
+
+ await menu.getByRole("menuitem", { name: PRESET_NAMES[0] }).first().click();
+ await expect(menu).not.toBeVisible({ timeout: 3_000 });
+
+ await waitForWsFrame(
+ frigateApp.page,
+ (frame) =>
+ frame.includes(`"${PTZ_CAMERA}/ptz"`) &&
+ frame.includes(`preset_${PRESET_NAMES[0]}`),
+ );
+
+ await waitForBodyInteractive(frigateApp.page);
+ await expectBodyInteractive(frigateApp.page);
+
+ await expect
+ .poll(
+ async () =>
+ frigateApp.page
+ .locator('[role="tooltip"]')
+ .filter({ hasText: /presets/i })
+ .isVisible()
+ .catch(() => false),
+ { timeout: 1_000 },
+ )
+ .toBe(false);
+
+ await frigateApp.page.keyboard.press("ArrowUp");
+ await frigateApp.page.keyboard.press("Space");
+ await frigateApp.page.keyboard.press("Enter");
+ await expect
+ .poll(() => menu.isVisible().catch(() => false), { timeout: 1_000 })
+ .toBe(false);
+ });
+});
+
+test.describe("Live mobile layout @critical @mobile", () => {
+ test("mobile dashboard has no sidebar and renders cameras", async ({
+ frigateApp,
+ }) => {
+ test.skip(!frigateApp.isMobile, "Mobile-only");
+ await frigateApp.goto("/");
+ await expect(frigateApp.page.locator("aside")).toHaveCount(0);
+ const live = new LivePage(frigateApp.page, false);
+ await expect(live.cameraCard("front_door")).toBeVisible({
+ timeout: 10_000,
+ });
+ });
+
+ test("mobile camera tap opens single view", async ({ frigateApp }) => {
+ test.skip(!frigateApp.isMobile, "Mobile-only");
+ await frigateApp.goto("/");
+ const live = new LivePage(frigateApp.page, false);
+ await live.cameraCard("front_door").first().click({ timeout: 10_000 });
+ await expect(frigateApp.page).toHaveURL(/#front_door/);
+ });
+
+ test("mobile onvif single-camera view loads without freezing body", async ({
+ frigateApp,
+ }) => {
+ test.skip(!frigateApp.isMobile, "Mobile-only");
+ // Migrated from ptz-overlay.spec.ts — dismissable-layer dedupe smoke test.
+ await frigateApp.api.install({
+ config: {
+ cameras: { [PTZ_CAMERA]: { onvif: { host: "10.0.0.50" } } },
+ },
+ });
+ await frigateApp.page.route(`**/api/${PTZ_CAMERA}/ptz/info`, (route) =>
+ route.fulfill({
+ json: {
+ name: PTZ_CAMERA,
+ features: ["pt", "zoom"],
+ presets: PRESET_NAMES,
+ profiles: [],
+ },
+ }),
+ );
+ await frigateApp.goto(`/#${PTZ_CAMERA}`);
+ await expectBodyInteractive(frigateApp.page);
await expect(frigateApp.page.locator("body")).toBeVisible();
});
});
-
-test.describe("Live Single Camera - Mobile Controls @critical", () => {
- test("mobile camera view has settings drawer trigger", async ({
- frigateApp,
- }) => {
- if (!frigateApp.isMobile) {
- test.skip();
- return;
- }
- await frigateApp.goto("/#front_door");
- await frigateApp.page.waitForTimeout(2000);
- // On mobile, settings gear opens a drawer
- // The button has aria-label with the camera name like "front_door Settings"
- const buttons = frigateApp.page.locator("button:has(svg)");
- const count = await buttons.count();
- expect(count).toBeGreaterThan(0);
- });
-});
-
-test.describe("Live Context Menu @critical", () => {
- test("right-click on camera opens context menu on desktop", async ({
- frigateApp,
- }) => {
- if (frigateApp.isMobile) {
- test.skip();
- return;
- }
- await frigateApp.goto("/");
- const card = frigateApp.page.locator("[data-camera='front_door']").first();
- await card.waitFor({ state: "visible", timeout: 10_000 });
- await card.click({ button: "right" });
- const contextMenu = frigateApp.page.locator(
- '[role="menu"], [data-radix-menu-content]',
- );
- await expect(contextMenu.first()).toBeVisible({ timeout: 5_000 });
- });
-
- test("context menu closes on escape", async ({ frigateApp }) => {
- if (frigateApp.isMobile) {
- test.skip();
- return;
- }
- await frigateApp.goto("/");
- const card = frigateApp.page.locator("[data-camera='front_door']").first();
- await card.waitFor({ state: "visible", timeout: 10_000 });
- await card.click({ button: "right" });
- await frigateApp.page.waitForTimeout(500);
- await frigateApp.page.keyboard.press("Escape");
- await frigateApp.page.waitForTimeout(300);
- const contextMenu = frigateApp.page.locator(
- '[role="menu"], [data-radix-menu-content]',
- );
- await expect(contextMenu).not.toBeVisible();
- });
-});
-
-test.describe("Live Mobile Layout @critical", () => {
- test("mobile renders cameras without sidebar", async ({ frigateApp }) => {
- if (!frigateApp.isMobile) {
- test.skip();
- return;
- }
- await frigateApp.goto("/");
- await expect(frigateApp.page.locator("aside")).not.toBeVisible();
- await expect(
- frigateApp.page.locator("[data-camera='front_door']"),
- ).toBeVisible({ timeout: 10_000 });
- });
-
- test("mobile camera click opens single camera view", async ({
- frigateApp,
- }) => {
- if (!frigateApp.isMobile) {
- test.skip();
- return;
- }
- await frigateApp.goto("/");
- const card = frigateApp.page.locator("[data-camera='front_door']").first();
- await card.click({ timeout: 10_000 });
- await expect(frigateApp.page).toHaveURL(/#front_door/);
- });
-});
diff --git a/web/e2e/specs/logs.spec.ts b/web/e2e/specs/logs.spec.ts
index 1f6af36aeb..0b74acfa11 100644
--- a/web/e2e/specs/logs.spec.ts
+++ b/web/e2e/specs/logs.spec.ts
@@ -1,75 +1,222 @@
/**
* Logs page tests -- MEDIUM tier.
*
- * Tests service tab switching by name, copy/download buttons,
- * and websocket message feed tab.
+ * Service tabs (with real /logs/ JSON contract),
+ * log content render, Copy (clipboard), Download (assert
+ * ?download=true request fired), mobile tab selector.
*/
import { test, expect } from "../fixtures/frigate-test";
+import { grantClipboardPermissions, readClipboard } from "../helpers/clipboard";
-test.describe("Logs Page - Service Tabs @medium", () => {
- test("logs page renders with named service tabs", async ({ frigateApp }) => {
+function logsJsonBody(lines: string[]) {
+ return { lines, totalLines: lines.length };
+}
+
+test.describe("Logs — service tabs @medium", () => {
+ test("frigate tab renders by default with mocked log lines", async ({
+ frigateApp,
+ }) => {
+ await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) =>
+ route.fulfill({
+ json: logsJsonBody([
+ "[2026-04-06 10:00:00] INFO: Frigate started",
+ "[2026-04-06 10:00:01] INFO: Cameras loaded",
+ ]),
+ }),
+ );
+ // Silence the streaming fetch so it doesn't hang the test.
+ await frigateApp.page.route(/\/api\/logs\/frigate\?stream=true/, (route) =>
+ route.fulfill({ status: 200, body: "" }),
+ );
+ await frigateApp.goto("/logs");
+ await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({
+ timeout: 5_000,
+ });
+ await expect(frigateApp.page.getByText(/Frigate started/)).toBeVisible({
+ timeout: 10_000,
+ });
+ });
+
+ test("switching to go2rtc fires a GET to /logs/go2rtc", async ({
+ frigateApp,
+ }) => {
+ let go2rtcCalled = false;
+ await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) =>
+ route.fulfill({ json: logsJsonBody(["frigate line"]) }),
+ );
+ await frigateApp.page.route(/\/api\/logs\/go2rtc(\?|$)/, (route) => {
+ if (!route.request().url().includes("stream=true")) {
+ go2rtcCalled = true;
+ }
+ return route.fulfill({ json: logsJsonBody(["go2rtc line"]) });
+ });
+ await frigateApp.page.route(/\/api\/logs\/.*\?stream=true/, (route) =>
+ route.fulfill({ status: 200, body: "" }),
+ );
+
+ await frigateApp.goto("/logs");
+ await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({
+ timeout: 5_000,
+ });
+ const go2rtcTab = frigateApp.page.getByLabel("Select go2rtc");
+ await expect(go2rtcTab).toBeVisible();
+ await go2rtcTab.click();
+ await expect.poll(() => go2rtcCalled, { timeout: 5_000 }).toBe(true);
+ await expect(go2rtcTab).toHaveAttribute("data-state", "on");
+ });
+});
+
+test.describe("Logs — actions @medium", () => {
+ test("Copy button writes current logs to clipboard", async ({
+ frigateApp,
+ context,
+ }) => {
+ await grantClipboardPermissions(context);
+ await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) =>
+ route.fulfill({
+ json: logsJsonBody([
+ "[2026-04-06 10:00:00] INFO: Frigate started",
+ "[2026-04-06 10:00:01] INFO: Cameras loaded",
+ ]),
+ }),
+ );
+ await frigateApp.page.route(/\/api\/logs\/frigate\?stream=true/, (route) =>
+ route.fulfill({ status: 200, body: "" }),
+ );
+ await frigateApp.goto("/logs");
+ await expect(frigateApp.page.getByText(/Frigate started/)).toBeVisible({
+ timeout: 10_000,
+ });
+
+ const copyBtn = frigateApp.page.getByLabel("Copy to Clipboard");
+ await expect(copyBtn).toBeVisible({ timeout: 5_000 });
+ await copyBtn.click();
+ await expect
+ .poll(() => readClipboard(frigateApp.page), { timeout: 5_000 })
+ .toContain("Frigate started");
+ });
+
+ test("Download button fires GET /logs/?download=true", async ({
+ frigateApp,
+ }) => {
+ let downloadCalled = false;
+ await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) => {
+ if (route.request().url().includes("download=true")) {
+ downloadCalled = true;
+ }
+ return route.fulfill({ json: logsJsonBody(["frigate line"]) });
+ });
+ await frigateApp.page.route(/\/api\/logs\/frigate\?stream=true/, (route) =>
+ route.fulfill({ status: 200, body: "" }),
+ );
+
+ await frigateApp.goto("/logs");
+ const downloadBtn = frigateApp.page.getByLabel("Download Logs");
+ await expect(downloadBtn).toBeVisible({ timeout: 5_000 });
+ await downloadBtn.click();
+ await expect.poll(() => downloadCalled, { timeout: 5_000 }).toBe(true);
+ });
+});
+
+test.describe("Logs — websocket tab @medium", () => {
+ test("switching to websocket tab renders WsMessageFeed container", async ({
+ frigateApp,
+ }) => {
+ await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) =>
+ route.fulfill({ json: logsJsonBody(["frigate line"]) }),
+ );
+ await frigateApp.page.route(/\/api\/logs\/frigate\?stream=true/, (route) =>
+ route.fulfill({ status: 200, body: "" }),
+ );
+ await frigateApp.goto("/logs");
+ const wsTab = frigateApp.page.getByLabel("Select websocket");
+ await expect(wsTab).toBeVisible({ timeout: 5_000 });
+ await wsTab.click();
+ await expect(wsTab).toHaveAttribute("data-state", "on", { timeout: 5_000 });
+ });
+});
+
+test.describe("Logs — streaming @medium", () => {
+ test("streamed log lines appear in the viewport", async ({ frigateApp }) => {
+ await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) => {
+ if (route.request().url().includes("stream=true")) {
+ // Intercepted below via addInitScript fetch override.
+ return route.fallback();
+ }
+ return route.fulfill({
+ json: logsJsonBody(["[2026-04-06 10:00:00] INFO: initial batch line"]),
+ });
+ });
+
+ // Override window.fetch so the /api/logs/frigate?stream=true request
+ // resolves with a real ReadableStream that emits chunks over time.
+ // This is the only way to validate streaming-append behavior through
+ // Playwright — route.fulfill() cannot return a stream.
+ // NOTE: The app calls fetch('api/logs/...') with a relative URL (no
+ // leading slash), so we match both relative and absolute forms.
+ await frigateApp.page.addInitScript(() => {
+ const origFetch = window.fetch;
+ window.fetch = async (input, init) => {
+ const url =
+ typeof input === "string"
+ ? input
+ : input instanceof URL
+ ? input.toString()
+ : (input as Request).url;
+ if (url.includes("api/logs/frigate") && url.includes("stream=true")) {
+ const encoder = new TextEncoder();
+ const stream = new ReadableStream({
+ async start(controller) {
+ await new Promise((r) => setTimeout(r, 30));
+ controller.enqueue(
+ encoder.encode(
+ "[2026-04-06 10:00:02] INFO: streamed line one\n",
+ ),
+ );
+ await new Promise((r) => setTimeout(r, 30));
+ controller.enqueue(
+ encoder.encode(
+ "[2026-04-06 10:00:03] INFO: streamed line two\n",
+ ),
+ );
+ controller.close();
+ },
+ });
+ return new Response(stream, { status: 200 });
+ }
+ return origFetch.call(window, input as RequestInfo, init);
+ };
+ });
+
+ await frigateApp.goto("/logs");
+ // The initial batch line is parsed by LogLineData and its content is
+ // rendered in a .log-content cell — assert against that element.
+ await expect(frigateApp.page.getByText("initial batch line")).toBeVisible({
+ timeout: 10_000,
+ });
+ await expect(frigateApp.page.getByText(/streamed line one/)).toBeVisible({
+ timeout: 10_000,
+ });
+ await expect(frigateApp.page.getByText(/streamed line two/)).toBeVisible({
+ timeout: 10_000,
+ });
+ });
+});
+
+test.describe("Logs — mobile @medium @mobile", () => {
+ test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
+
+ test("service tabs render at mobile viewport", async ({ frigateApp }) => {
+ await frigateApp.page.route(/\/api\/logs\/frigate(\?|$)/, (route) =>
+ route.fulfill({ json: logsJsonBody(["frigate line"]) }),
+ );
+ await frigateApp.page.route(/\/api\/logs\/frigate\?stream=true/, (route) =>
+ route.fulfill({ status: 200, body: "" }),
+ );
await frigateApp.goto("/logs");
- await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
- // Service tabs have aria-label="Select {service}"
await expect(frigateApp.page.getByLabel("Select frigate")).toBeVisible({
timeout: 5_000,
});
});
-
- test("switching to go2rtc tab changes active tab", async ({ frigateApp }) => {
- await frigateApp.goto("/logs");
- await frigateApp.page.waitForTimeout(1000);
- const go2rtcTab = frigateApp.page.getByLabel("Select go2rtc");
- if (await go2rtcTab.isVisible().catch(() => false)) {
- await go2rtcTab.click();
- await frigateApp.page.waitForTimeout(1000);
- await expect(go2rtcTab).toHaveAttribute("data-state", "on");
- }
- });
-
- test("switching to websocket tab shows message feed", async ({
- frigateApp,
- }) => {
- await frigateApp.goto("/logs");
- await frigateApp.page.waitForTimeout(1000);
- const wsTab = frigateApp.page.getByLabel("Select websocket");
- if (await wsTab.isVisible().catch(() => false)) {
- await wsTab.click();
- await frigateApp.page.waitForTimeout(1000);
- await expect(wsTab).toHaveAttribute("data-state", "on");
- }
- });
-});
-
-test.describe("Logs Page - Actions @medium", () => {
- test("copy to clipboard button is present and clickable", async ({
- frigateApp,
- }) => {
- await frigateApp.goto("/logs");
- await frigateApp.page.waitForTimeout(1000);
- const copyBtn = frigateApp.page.getByLabel("Copy to Clipboard");
- if (await copyBtn.isVisible().catch(() => false)) {
- await copyBtn.click();
- await frigateApp.page.waitForTimeout(500);
- // Should trigger clipboard copy (toast may appear)
- }
- await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
- });
-
- test("download logs button is present", async ({ frigateApp }) => {
- await frigateApp.goto("/logs");
- await frigateApp.page.waitForTimeout(1000);
- const downloadBtn = frigateApp.page.getByLabel("Download Logs");
- if (await downloadBtn.isVisible().catch(() => false)) {
- await expect(downloadBtn).toBeVisible();
- }
- });
-
- test("logs page displays log content text", async ({ frigateApp }) => {
- await frigateApp.goto("/logs");
- await frigateApp.page.waitForTimeout(2000);
- const text = await frigateApp.page.textContent("#pageRoot");
- expect(text?.length).toBeGreaterThan(0);
- });
});
diff --git a/web/e2e/specs/navigation.spec.ts b/web/e2e/specs/navigation.spec.ts
index e049b6f7e5..e14887c157 100644
--- a/web/e2e/specs/navigation.spec.ts
+++ b/web/e2e/specs/navigation.spec.ts
@@ -1,227 +1,175 @@
/**
* Navigation tests -- CRITICAL tier.
*
- * Tests sidebar (desktop) and bottombar (mobile) navigation,
- * conditional nav items, settings menus, and their actual behaviors.
+ * Covers sidebar (desktop) / bottombar (mobile) link set, conditional
+ * nav items (faces, chat, classification), settings menu navigation,
+ * unknown-route redirect to /, and mobile-specific nav behaviors.
*/
import { test, expect } from "../fixtures/frigate-test";
import { BasePage } from "../pages/base.page";
-test.describe("Navigation @critical", () => {
- test("app loads and renders page root", async ({ frigateApp }) => {
- await frigateApp.goto("/");
- await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
- });
+const PRIMARY_ROUTES = ["/review", "/explore", "/export"] as const;
- test("logo is visible and links to home", async ({ frigateApp }) => {
- if (frigateApp.isMobile) {
- test.skip();
- return;
- }
- await frigateApp.goto("/");
- const base = new BasePage(frigateApp.page, true);
- const logo = base.sidebar.locator('a[href="/"]').first();
- await expect(logo).toBeVisible();
- });
-
- test("all primary nav links are present and navigate", async ({
+test.describe("Navigation — primary links @critical", () => {
+ test("every primary link is visible and navigates", async ({
frigateApp,
}) => {
await frigateApp.goto("/");
- const routes = ["/review", "/explore", "/export"];
- for (const route of routes) {
+ for (const route of PRIMARY_ROUTES) {
await expect(
frigateApp.page.locator(`a[href="${route}"]`).first(),
).toBeVisible();
}
- // Verify clicking each one actually navigates
const base = new BasePage(frigateApp.page, !frigateApp.isMobile);
- for (const route of routes) {
+ for (const route of PRIMARY_ROUTES) {
await base.navigateTo(route);
await expect(frigateApp.page).toHaveURL(new RegExp(route));
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
}
});
- test("desktop sidebar is visible, mobile bottombar is visible", async ({
- frigateApp,
- }) => {
- await frigateApp.goto("/");
- const base = new BasePage(frigateApp.page, !frigateApp.isMobile);
- if (!frigateApp.isMobile) {
- await expect(base.sidebar).toBeVisible();
- } else {
- await expect(base.sidebar).not.toBeVisible();
- }
+ test("logo links home on desktop", async ({ frigateApp }) => {
+ test.skip(frigateApp.isMobile, "Sidebar logo is desktop-only");
+ await frigateApp.goto("/review");
+ await frigateApp.page.locator("aside a[href='/']").first().click();
+ await expect(frigateApp.page).toHaveURL(/\/$/);
});
- test("navigate between all main pages without crash", async ({
- frigateApp,
- }) => {
- await frigateApp.goto("/");
- const base = new BasePage(frigateApp.page, !frigateApp.isMobile);
- const pageRoot = frigateApp.page.locator("#pageRoot");
-
- await base.navigateTo("/review");
- await expect(pageRoot).toBeVisible({ timeout: 10_000 });
- await base.navigateTo("/explore");
- await expect(pageRoot).toBeVisible({ timeout: 10_000 });
- await base.navigateTo("/export");
- await expect(pageRoot).toBeVisible({ timeout: 10_000 });
- await base.navigateTo("/review");
- await expect(pageRoot).toBeVisible({ timeout: 10_000 });
- });
-
- test("unknown route redirects to home", async ({ frigateApp }) => {
+ test("unknown route redirects to /", async ({ frigateApp }) => {
await frigateApp.page.goto("/nonexistent-route");
- await frigateApp.page.waitForTimeout(2000);
- const url = frigateApp.page.url();
- const hasPageRoot = await frigateApp.page
- .locator("#pageRoot")
- .isVisible()
- .catch(() => false);
- expect(url.endsWith("/") || hasPageRoot).toBeTruthy();
+ await frigateApp.page.waitForSelector("#pageRoot", { timeout: 10_000 });
+ await expect(frigateApp.page).toHaveURL(/\/$/);
+ await expect(
+ frigateApp.page.locator("[data-camera='front_door']"),
+ ).toBeVisible({ timeout: 10_000 });
});
});
-test.describe("Navigation - Conditional Items @critical", () => {
- test("Faces nav hidden when face_recognition disabled", async ({
+test.describe("Navigation — conditional items @critical", () => {
+ test("/faces is hidden when face_recognition.enabled is false", async ({
frigateApp,
}) => {
await frigateApp.goto("/");
- await expect(frigateApp.page.locator('a[href="/faces"]')).not.toBeVisible();
+ await expect(
+ frigateApp.page.locator('a[href="/faces"]').first(),
+ ).toHaveCount(0);
});
- test("Chat nav hidden when genai model is none", async ({ frigateApp }) => {
- if (frigateApp.isMobile) {
- test.skip();
- return;
- }
- await frigateApp.installDefaults({
- config: {
- genai: {
- enabled: false,
- provider: "ollama",
- model: "none",
- base_url: "",
- },
- },
- });
- await frigateApp.goto("/");
- await expect(frigateApp.page.locator('a[href="/chat"]')).not.toBeVisible();
- });
-
- test("Faces nav visible when face_recognition enabled on desktop", async ({
+ test("/faces is visible when face_recognition.enabled is true (desktop)", async ({
frigateApp,
- page,
}) => {
- if (frigateApp.isMobile) {
- test.skip();
- return;
- }
+ test.skip(frigateApp.isMobile, "Desktop sidebar");
await frigateApp.installDefaults({
config: { face_recognition: { enabled: true } },
});
await frigateApp.goto("/");
- await expect(page.locator('a[href="/faces"]')).toBeVisible();
+ await expect(
+ frigateApp.page.locator('a[href="/faces"]').first(),
+ ).toBeVisible();
});
- test("Chat nav visible when genai model set on desktop", async ({
+ test("/chat is hidden when no agent has the chat role (desktop)", async ({
frigateApp,
- page,
}) => {
- if (frigateApp.isMobile) {
- test.skip();
- return;
- }
+ test.skip(frigateApp.isMobile, "Desktop sidebar");
await frigateApp.installDefaults({
- config: { genai: { enabled: true, model: "llava" } },
+ config: {
+ genai: {
+ descriptions_only: {
+ provider: "ollama",
+ model: "llava",
+ roles: ["descriptions"],
+ },
+ },
+ },
});
await frigateApp.goto("/");
- await expect(page.locator('a[href="/chat"]')).toBeVisible();
+ await expect(
+ frigateApp.page.locator('a[href="/chat"]').first(),
+ ).toHaveCount(0);
});
- test("Classification nav visible for admin on desktop", async ({
+ test("/chat is visible when an agent has the chat role (desktop)", async ({
frigateApp,
- page,
}) => {
- if (frigateApp.isMobile) {
- test.skip();
- return;
- }
+ test.skip(frigateApp.isMobile, "Desktop sidebar");
+ await frigateApp.installDefaults({
+ config: {
+ genai: {
+ chat_agent: {
+ provider: "ollama",
+ model: "llava",
+ roles: ["chat"],
+ },
+ },
+ },
+ });
await frigateApp.goto("/");
- await expect(page.locator('a[href="/classification"]')).toBeVisible();
+ await expect(
+ frigateApp.page.locator('a[href="/chat"]').first(),
+ ).toBeVisible();
+ });
+
+ test("/classification is visible for admin on desktop", async ({
+ frigateApp,
+ }) => {
+ test.skip(frigateApp.isMobile, "Desktop sidebar");
+ await frigateApp.goto("/");
+ await expect(
+ frigateApp.page.locator('a[href="/classification"]').first(),
+ ).toBeVisible();
});
});
-test.describe("Navigation - Settings Menu @critical", () => {
- test("settings gear opens menu with navigation items (desktop)", async ({
- frigateApp,
- }) => {
- if (frigateApp.isMobile) {
- test.skip();
- return;
- }
- await frigateApp.goto("/");
- // Settings gear is in the sidebar bottom section, a div with cursor-pointer
- const sidebarBottom = frigateApp.page.locator("aside .mb-8");
- const gearIcon = sidebarBottom
- .locator("div[class*='cursor-pointer']")
- .first();
- await expect(gearIcon).toBeVisible({ timeout: 5_000 });
- await gearIcon.click();
- // Menu should open - look for the "Settings" menu item by aria-label
- await expect(frigateApp.page.getByLabel("Settings")).toBeVisible({
- timeout: 3_000,
- });
- });
+test.describe("Navigation — settings menu (desktop) @critical", () => {
+ test.skip(
+ ({ frigateApp }) => frigateApp.isMobile,
+ "Sidebar settings menu is desktop-only",
+ );
- test("settings menu items navigate to correct routes (desktop)", async ({
- frigateApp,
- }) => {
- if (frigateApp.isMobile) {
- test.skip();
- return;
- }
- const targets = [
- { label: "Settings", url: "/settings" },
- { label: "System metrics", url: "/system" },
- { label: "System logs", url: "/logs" },
- { label: "Configuration Editor", url: "/config" },
- ];
- for (const target of targets) {
+ const TARGETS = [
+ { label: "Settings", url: /\/settings/ },
+ { label: "System metrics", url: /\/system/ },
+ { label: "System logs", url: /\/logs/ },
+ { label: "Configuration Editor", url: /\/config/ },
+ ];
+
+ for (const target of TARGETS) {
+ test(`menu → ${target.label} navigates`, async ({ frigateApp }) => {
await frigateApp.goto("/");
- const gearIcon = frigateApp.page
+ const gear = frigateApp.page
.locator("aside .mb-8 div[class*='cursor-pointer']")
.first();
- await gearIcon.click();
- await frigateApp.page.waitForTimeout(300);
- const menuItem = frigateApp.page.getByLabel(target.label);
- if (await menuItem.isVisible().catch(() => false)) {
- await menuItem.click();
- await expect(frigateApp.page).toHaveURL(
- new RegExp(target.url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
- );
- }
+ await gear.click();
+ await frigateApp.page.getByLabel(target.label).click();
+ await expect(frigateApp.page).toHaveURL(target.url);
+ });
+ }
+});
+
+test.describe("Navigation — mobile @critical @mobile", () => {
+ test("mobile bottombar visible, sidebar not rendered", async ({
+ frigateApp,
+ }) => {
+ test.skip(!frigateApp.isMobile, "Mobile-only");
+ await frigateApp.goto("/");
+ await expect(frigateApp.page.locator("aside")).toHaveCount(0);
+ for (const route of PRIMARY_ROUTES) {
+ await expect(
+ frigateApp.page.locator(`a[href="${route}"]`).first(),
+ ).toBeVisible();
}
});
- test("account button in sidebar is clickable (desktop)", async ({
- frigateApp,
- }) => {
- if (frigateApp.isMobile) {
- test.skip();
- return;
- }
+ test("mobile nav survives route change", async ({ frigateApp }) => {
+ test.skip(!frigateApp.isMobile, "Mobile-only");
await frigateApp.goto("/");
- const sidebarBottom = frigateApp.page.locator("aside .mb-8");
- const items = sidebarBottom.locator("div[class*='cursor-pointer']");
- const count = await items.count();
- if (count >= 2) {
- await items.nth(1).click();
- await frigateApp.page.waitForTimeout(500);
- }
- await expect(frigateApp.page.locator("body")).toBeVisible();
+ const reviewLink = frigateApp.page.locator('a[href="/review"]').first();
+ await reviewLink.click();
+ await expect(frigateApp.page).toHaveURL(/\/review/);
+ await expect(
+ frigateApp.page.locator('a[href="/review"]').first(),
+ ).toBeVisible();
});
});
diff --git a/web/e2e/specs/replay.spec.ts b/web/e2e/specs/replay.spec.ts
index c506fec5ad..51a42737f0 100644
--- a/web/e2e/specs/replay.spec.ts
+++ b/web/e2e/specs/replay.spec.ts
@@ -1,23 +1,310 @@
/**
- * Replay page tests -- LOW tier.
+ * Replay page tests -- MEDIUM tier.
*
- * Tests replay page rendering and basic interactivity.
+ * /replay is the admin debug replay page (not a recordings player).
+ * Polls /api/debug_replay/status, renders a no-session state when
+ * inactive, and a live camera image + debug toggles + Stop controls
+ * when active.
*/
import { test, expect } from "../fixtures/frigate-test";
+import {
+ activeSessionStatus,
+ noSessionStatus,
+} from "../fixtures/mock-data/debug-replay";
-test.describe("Replay Page @low", () => {
- test("replay page renders without crash", async ({ frigateApp }) => {
+async function installStatusRoute(
+ app: { page: import("@playwright/test").Page },
+ body: unknown,
+) {
+ await app.page.route("**/api/debug_replay/status", (route) =>
+ route.fulfill({ json: body }),
+ );
+}
+
+test.describe("Replay — no active session @medium", () => {
+ test("empty state renders heading + Go to History button", async ({
+ frigateApp,
+ }) => {
+ await installStatusRoute(frigateApp, noSessionStatus());
await frigateApp.goto("/replay");
- await frigateApp.page.waitForTimeout(2000);
- await expect(frigateApp.page.locator("body")).toBeVisible();
+ await expect(
+ frigateApp.page.getByRole("heading", {
+ level: 2,
+ name: /No Active Debug Replay Session/i,
+ }),
+ ).toBeVisible({ timeout: 10_000 });
+ const goButton = frigateApp.page.getByRole("button", {
+ name: /Go to History|Go to Recordings/i,
+ });
+ await expect(goButton).toBeVisible();
});
- test("replay page has interactive controls", async ({ frigateApp }) => {
+ test("clicking Go to History navigates to /review", async ({
+ frigateApp,
+ }) => {
+ await installStatusRoute(frigateApp, noSessionStatus());
await frigateApp.goto("/replay");
- await frigateApp.page.waitForTimeout(2000);
- const buttons = frigateApp.page.locator("button");
- const count = await buttons.count();
- expect(count).toBeGreaterThan(0);
+ await expect(
+ frigateApp.page.getByRole("heading", {
+ level: 2,
+ name: /No Active Debug Replay Session/i,
+ }),
+ ).toBeVisible({ timeout: 10_000 });
+ await frigateApp.page
+ .getByRole("button", { name: /Go to History|Go to Recordings/i })
+ .click();
+ await expect(frigateApp.page).toHaveURL(/\/review/);
+ });
+});
+
+test.describe("Replay — active session @medium", () => {
+ test("active status renders the Debug Replay side panel", async ({
+ frigateApp,
+ }) => {
+ await installStatusRoute(frigateApp, activeSessionStatus());
+ await frigateApp.goto("/replay");
+ await expect(
+ frigateApp.page.getByRole("heading", { level: 3, name: /Debug Replay/i }),
+ ).toBeVisible({ timeout: 10_000 });
+ // Three tabs (Debug / Objects / Messages) in TabsList
+ await expect(frigateApp.page.locator('[role="tab"]')).toHaveCount(3);
+ });
+
+ test("debug toggles render with bbox ON by default", async ({
+ frigateApp,
+ }) => {
+ await installStatusRoute(frigateApp, activeSessionStatus());
+ await frigateApp.goto("/replay");
+ const bbox = frigateApp.page.locator("#debug-bbox");
+ await expect(bbox).toBeVisible({ timeout: 10_000 });
+ await expect(bbox).toHaveAttribute("aria-checked", "true");
+ });
+
+ test("clicking bbox toggle flips aria-checked", async ({ frigateApp }) => {
+ await installStatusRoute(frigateApp, activeSessionStatus());
+ await frigateApp.goto("/replay");
+ const bbox = frigateApp.page.locator("#debug-bbox");
+ await expect(bbox).toBeVisible({ timeout: 10_000 });
+ await expect(bbox).toHaveAttribute("aria-checked", "true");
+ await bbox.click();
+ await expect(bbox).toHaveAttribute("aria-checked", "false");
+ });
+
+ test("Configuration button opens the configuration dialog (desktop)", async ({
+ frigateApp,
+ }) => {
+ test.skip(frigateApp.isMobile, "Desktop: button has visible text label");
+ await installStatusRoute(frigateApp, activeSessionStatus());
+ await frigateApp.goto("/replay");
+ await expect(
+ frigateApp.page.getByRole("heading", { level: 3, name: /Debug Replay/i }),
+ ).toBeVisible({ timeout: 10_000 });
+
+ // On desktop the span is visible and gives the button an accessible name.
+ await frigateApp.page
+ .getByRole("button", { name: /configuration/i })
+ .first()
+ .click();
+
+ const dialog = frigateApp.page.getByRole("dialog");
+ await expect(dialog).toBeVisible({ timeout: 5_000 });
+ });
+
+ test("Configuration button opens the configuration dialog (mobile)", async ({
+ frigateApp,
+ }) => {
+ test.skip(!frigateApp.isMobile, "Mobile: button is icon-only");
+ await installStatusRoute(frigateApp, activeSessionStatus());
+ await frigateApp.goto("/replay");
+ await expect(
+ frigateApp.page.getByRole("heading", { level: 3, name: /Debug Replay/i }),
+ ).toBeVisible({ timeout: 10_000 });
+
+ // On mobile the Configuration button text span is hidden (md:inline).
+ // It is the first button inside the right-side action group div
+ // (the flex container that holds Config + Stop, sibling of the Back button).
+ const actionGroup = frigateApp.page.locator(
+ ".flex.items-center.gap-2 button",
+ );
+ await actionGroup.first().click();
+
+ // On mobile PlatformAwareSheet renders a MobilePage (full-screen panel)
+ // instead of a Radix Dialog, so assert the panel title heading is visible.
+ await expect(
+ frigateApp.page.getByRole("heading", {
+ level: 2,
+ name: /^Configuration$/i,
+ }),
+ ).toBeVisible({ timeout: 5_000 });
+ });
+
+ test("Objects tab renders with the camera_activity objects list", async ({
+ frigateApp,
+ }) => {
+ await installStatusRoute(frigateApp, activeSessionStatus());
+ await frigateApp.goto("/replay");
+
+ await expect(
+ frigateApp.page.getByRole("heading", { level: 3, name: /Debug Replay/i }),
+ ).toBeVisible({ timeout: 10_000 });
+
+ // Send an activity payload with a person object on front_door.
+ // Must be called after goto() so the WS connection is established.
+ await frigateApp.ws.sendCameraActivity({
+ front_door: {
+ objects: [
+ {
+ label: "person",
+ score: 0.95,
+ box: [0.1, 0.1, 0.5, 0.8],
+ area: 0.2,
+ ratio: 0.6,
+ region: [0.05, 0.05, 0.6, 0.85],
+ current_zones: [],
+ id: "obj-person-1",
+ },
+ ],
+ },
+ });
+
+ // Switch to Objects tab (labelled "Object List" in i18n).
+ const objectsTab = frigateApp.page.getByRole("tab", {
+ name: /object/i,
+ });
+ await objectsTab.click();
+ await expect(objectsTab).toHaveAttribute("data-state", "active", {
+ timeout: 3_000,
+ });
+
+ // The object row renders the label.
+ await expect(frigateApp.page.getByText(/person/i).first()).toBeVisible({
+ timeout: 5_000,
+ });
+ });
+
+ test("Messages tab renders WsMessageFeed container", async ({
+ frigateApp,
+ }) => {
+ await installStatusRoute(frigateApp, activeSessionStatus());
+ await frigateApp.goto("/replay");
+ await expect(
+ frigateApp.page.getByRole("heading", { level: 3, name: /Debug Replay/i }),
+ ).toBeVisible({ timeout: 10_000 });
+
+ const messagesTab = frigateApp.page.getByRole("tab", {
+ name: /messages/i,
+ });
+ await messagesTab.click();
+ await expect(messagesTab).toHaveAttribute("data-state", "active", {
+ timeout: 3_000,
+ });
+ });
+
+ test("bbox info popover opens and closes cleanly", async ({ frigateApp }) => {
+ await installStatusRoute(frigateApp, activeSessionStatus());
+ await frigateApp.goto("/replay");
+ // The bbox row has an info icon popover trigger next to its label.
+ // The trigger is a div (not button) wrapping LuInfo with an sr-only
+ // "Info" span. Target it by the sr-only text content.
+ const infoTrigger = frigateApp.page
+ .locator("span.sr-only", { hasText: /info/i })
+ .first();
+ await expect(infoTrigger).toBeVisible({ timeout: 10_000 });
+ // Click the parent div (the actual trigger)
+ await infoTrigger.locator("..").click();
+
+ const popover = frigateApp.page.locator(
+ "[data-radix-popper-content-wrapper]",
+ );
+ await expect(popover.first()).toBeVisible({ timeout: 3_000 });
+ await frigateApp.page.keyboard.press("Escape");
+ await expect(popover.first()).not.toBeVisible({ timeout: 3_000 });
+ });
+});
+
+test.describe("Replay — stop flow (desktop) @medium", () => {
+ test.skip(
+ ({ frigateApp }) => frigateApp.isMobile,
+ "Desktop button has accessible 'Stop Replay' name",
+ );
+
+ test("Stop Replay opens confirm dialog; confirm POSTs debug_replay/stop", async ({
+ frigateApp,
+ }) => {
+ await installStatusRoute(frigateApp, activeSessionStatus());
+ let stopCalled = false;
+ await frigateApp.page.route("**/api/debug_replay/stop", async (route) => {
+ if (route.request().method() === "POST") stopCalled = true;
+ await route.fulfill({ json: { success: true } });
+ });
+
+ await frigateApp.goto("/replay");
+ await expect(
+ frigateApp.page.getByRole("heading", { level: 3, name: /Debug Replay/i }),
+ ).toBeVisible({ timeout: 10_000 });
+
+ await frigateApp.page
+ .getByRole("button", { name: /stop replay/i })
+ .first()
+ .click();
+
+ const dialog = frigateApp.page.getByRole("alertdialog");
+ await expect(dialog).toBeVisible({ timeout: 3_000 });
+ await dialog
+ .getByRole("button", { name: /stop|confirm/i })
+ .first()
+ .click();
+ await expect.poll(() => stopCalled, { timeout: 5_000 }).toBe(true);
+ });
+});
+
+test.describe("Replay — stop button (mobile) @medium @mobile", () => {
+ test.skip(
+ ({ frigateApp }) => !frigateApp.isMobile,
+ "Mobile-only icon-button variant",
+ );
+
+ test("tapping the icon-only stop button opens the confirm dialog", async ({
+ frigateApp,
+ }) => {
+ await installStatusRoute(frigateApp, activeSessionStatus());
+ await frigateApp.goto("/replay");
+ await expect(
+ frigateApp.page.getByRole("heading", { level: 3, name: /Debug Replay/i }),
+ ).toBeVisible({ timeout: 10_000 });
+
+ // On mobile the Stop button is an icon (LuSquare) inside an
+ // AlertDialogTrigger. It's the last button in the top bar's
+ // right-side action group (Back is on the left). Target by
+ // position within the top-bar flex container.
+ const topRightButtons = frigateApp.page
+ .locator(".min-h-12 button, .md\\:min-h-16 button")
+ .filter({ hasNot: frigateApp.page.getByLabel("Back") });
+ const lastButton = topRightButtons.last();
+ await expect(lastButton).toBeVisible({ timeout: 10_000 });
+ await lastButton.click();
+
+ const dialog = frigateApp.page.getByRole("alertdialog");
+ await expect(dialog).toBeVisible({ timeout: 3_000 });
+ await dialog.getByRole("button", { name: /cancel/i }).click();
+ await expect(dialog).not.toBeVisible({ timeout: 3_000 });
+ });
+});
+
+test.describe("Replay — mobile @medium @mobile", () => {
+ test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
+
+ test("no-session state renders at mobile viewport", async ({
+ frigateApp,
+ }) => {
+ await installStatusRoute(frigateApp, noSessionStatus());
+ await frigateApp.goto("/replay");
+ await expect(
+ frigateApp.page.getByRole("heading", {
+ level: 2,
+ name: /No Active Debug Replay Session/i,
+ }),
+ ).toBeVisible({ timeout: 10_000 });
});
});
diff --git a/web/e2e/specs/review.spec.ts b/web/e2e/specs/review.spec.ts
index 166f32c44b..8d52e083a8 100644
--- a/web/e2e/specs/review.spec.ts
+++ b/web/e2e/specs/review.spec.ts
@@ -1,200 +1,230 @@
/**
* Review/Events page tests -- CRITICAL tier.
*
- * Tests severity tab switching by name (Alerts/Detections/Motion),
- * filter popover opening with camera names, show reviewed toggle,
- * calendar button, and filter button interactions.
+ * Severity tabs, filter popovers, calendar, show-reviewed toggle,
+ * timeline, and the nested-overlay regression migrated from
+ * radix-overlay-regressions.spec.ts.
*/
import { test, expect } from "../fixtures/frigate-test";
import { BasePage } from "../pages/base.page";
+import { ReviewPage } from "../pages/review.page";
+import {
+ expectBodyInteractive,
+ waitForBodyInteractive,
+} from "../helpers/overlay-interaction";
-test.describe("Review Page - Severity Tabs @critical", () => {
- test("severity tabs render with Alerts, Detections, Motion", async ({
+test.describe("Review — severity tabs @critical", () => {
+ test("tabs render with Alerts default-on", async ({ frigateApp }) => {
+ await frigateApp.goto("/review");
+ const review = new ReviewPage(frigateApp.page, !frigateApp.isMobile);
+ await expect(review.alertsTab).toBeVisible({ timeout: 10_000 });
+ await expect(review.detectionsTab).toBeVisible();
+ await expect(review.motionTab).toBeVisible();
+ await expect(review.alertsTab).toHaveAttribute("data-state", "on");
+ });
+
+ test("clicking Detections flips data-state", async ({ frigateApp }) => {
+ await frigateApp.goto("/review");
+ const review = new ReviewPage(frigateApp.page, !frigateApp.isMobile);
+ await expect(review.alertsTab).toBeVisible({ timeout: 10_000 });
+ await review.detectionsTab.click();
+ await expect(review.detectionsTab).toHaveAttribute("data-state", "on");
+ await expect(review.alertsTab).toHaveAttribute("data-state", "off");
+ });
+
+ test("clicking Motion flips data-state", async ({ frigateApp }) => {
+ await frigateApp.goto("/review");
+ const review = new ReviewPage(frigateApp.page, !frigateApp.isMobile);
+ await expect(review.alertsTab).toBeVisible({ timeout: 10_000 });
+ await review.motionTab.click();
+ await expect(review.motionTab).toHaveAttribute("data-state", "on");
+ });
+
+ test("switching back to Alerts works", async ({ frigateApp }) => {
+ await frigateApp.goto("/review");
+ const review = new ReviewPage(frigateApp.page, !frigateApp.isMobile);
+ await review.detectionsTab.click();
+ await expect(review.detectionsTab).toHaveAttribute("data-state", "on");
+ await review.alertsTab.click();
+ await expect(review.alertsTab).toHaveAttribute("data-state", "on");
+ });
+
+ test("switching tabs updates active data-state (client-side filter)", async ({
frigateApp,
}) => {
+ // The severity tabs filter the already-fetched review data client-side;
+ // they do not trigger a new /api/review network request. This test
+ // verifies the state-change assertion that the tab switch takes effect.
await frigateApp.goto("/review");
- await expect(frigateApp.page.getByLabel("Alerts")).toBeVisible({
- timeout: 10_000,
- });
- await expect(frigateApp.page.getByLabel("Detections")).toBeVisible();
- // Motion uses role="radio" to distinguish from other Motion elements
- await expect(
- frigateApp.page.getByRole("radio", { name: "Motion" }),
- ).toBeVisible();
- });
-
- test("Alerts tab is active by default", async ({ frigateApp }) => {
- await frigateApp.goto("/review");
- await frigateApp.page.waitForTimeout(1000);
- const alertsTab = frigateApp.page.getByLabel("Alerts");
- await expect(alertsTab).toHaveAttribute("data-state", "on");
- });
-
- test("clicking Detections tab makes it active and deactivates Alerts", async ({
- frigateApp,
- }) => {
- await frigateApp.goto("/review");
- await frigateApp.page.waitForTimeout(1000);
- const alertsTab = frigateApp.page.getByLabel("Alerts");
- const detectionsTab = frigateApp.page.getByLabel("Detections");
-
- await detectionsTab.click();
- await frigateApp.page.waitForTimeout(500);
-
- await expect(detectionsTab).toHaveAttribute("data-state", "on");
- await expect(alertsTab).toHaveAttribute("data-state", "off");
- });
-
- test("clicking Motion tab makes it active", async ({ frigateApp }) => {
- await frigateApp.goto("/review");
- await frigateApp.page.waitForTimeout(1000);
- const motionTab = frigateApp.page.getByRole("radio", { name: "Motion" });
- await motionTab.click();
- await frigateApp.page.waitForTimeout(500);
- await expect(motionTab).toHaveAttribute("data-state", "on");
- });
-
- test("switching back to Alerts from Detections works", async ({
- frigateApp,
- }) => {
- await frigateApp.goto("/review");
- await frigateApp.page.waitForTimeout(1000);
-
- await frigateApp.page.getByLabel("Detections").click();
- await frigateApp.page.waitForTimeout(300);
- await frigateApp.page.getByLabel("Alerts").click();
- await frigateApp.page.waitForTimeout(300);
-
- await expect(frigateApp.page.getByLabel("Alerts")).toHaveAttribute(
- "data-state",
- "on",
- );
+ const review = new ReviewPage(frigateApp.page, !frigateApp.isMobile);
+ await expect(review.alertsTab).toBeVisible({ timeout: 10_000 });
+ await expect(review.alertsTab).toHaveAttribute("data-state", "on");
+ await review.detectionsTab.click();
+ await expect(review.detectionsTab).toHaveAttribute("data-state", "on");
+ await expect(review.alertsTab).toHaveAttribute("data-state", "off");
});
});
-test.describe("Review Page - Filters @critical", () => {
- test("All Cameras filter button opens popover with camera names", async ({
+test.describe("Review — filters (desktop) @critical", () => {
+ test.skip(
+ ({ frigateApp }) => frigateApp.isMobile,
+ "Filter bar differs on mobile",
+ );
+
+ test("Cameras popover lists configured camera names", async ({
frigateApp,
}) => {
- if (frigateApp.isMobile) {
- test.skip();
- return;
- }
await frigateApp.goto("/review");
- await frigateApp.page.waitForTimeout(1000);
-
- const camerasBtn = frigateApp.page.getByRole("button", {
- name: /cameras/i,
- });
- await expect(camerasBtn).toBeVisible({ timeout: 5_000 });
- await camerasBtn.click();
- await frigateApp.page.waitForTimeout(500);
-
- // Popover should open with camera names from config
- const popover = frigateApp.page.locator(
- "[data-radix-popper-content-wrapper]",
- );
- await expect(popover.first()).toBeVisible({ timeout: 3_000 });
- // Camera names should be present
+ const review = new ReviewPage(frigateApp.page, true);
+ await expect(review.camerasFilterTrigger).toBeVisible({ timeout: 5_000 });
+ await review.camerasFilterTrigger.click();
+ await expect(review.filterOverlay).toBeVisible({ timeout: 3_000 });
await expect(frigateApp.page.getByText("Front Door")).toBeVisible();
+ });
+ test("closing the Cameras popover with Escape leaves body interactive", async ({
+ frigateApp,
+ }) => {
+ // Migrated from radix-overlay-regressions.spec.ts.
+ await frigateApp.goto("/review");
+ const review = new ReviewPage(frigateApp.page, true);
+ await review.camerasFilterTrigger.click();
+ await expect(review.filterOverlay).toBeVisible({ timeout: 3_000 });
+ await frigateApp.page.keyboard.press("Escape");
+ await expect(review.filterOverlay).not.toBeVisible({ timeout: 3_000 });
+ await waitForBodyInteractive(frigateApp.page);
+ await expectBodyInteractive(frigateApp.page);
+ });
+
+ test("Labels are shown inside the General Filter dialog", async ({
+ frigateApp,
+ }) => {
+ // Labels are surfaced inside the "Filter" button's GeneralFilterContent
+ // dialog, not as a standalone top-level button. We open that dialog and
+ // confirm labels from the camera config are listed there.
+ await frigateApp.goto("/review");
+ const filterBtn = frigateApp.page
+ .getByRole("button", { name: /^filter$/i })
+ .first();
+ await expect(filterBtn).toBeVisible({ timeout: 5_000 });
+ await filterBtn.click();
+
+ const overlay = frigateApp.page.locator(
+ "[data-radix-popper-content-wrapper], [role='dialog']",
+ );
+ await expect(overlay.first()).toBeVisible({ timeout: 3_000 });
+ // The default mock config for front_door tracks "person"
+ await expect(overlay.first().getByText(/person/i)).toBeVisible();
await frigateApp.page.keyboard.press("Escape");
});
- test("Show Reviewed toggle is clickable", async ({ frigateApp }) => {
- await frigateApp.goto("/review");
- await frigateApp.page.waitForTimeout(1000);
-
- const showReviewed = frigateApp.page.getByRole("button", {
- name: /reviewed/i,
+ test("Zones popover lists configured zones inside the General Filter dialog", async ({
+ frigateApp,
+ }) => {
+ // Override config to guarantee a known zone on front_door.
+ await frigateApp.installDefaults({
+ config: {
+ cameras: {
+ front_door: {
+ zones: {
+ front_yard: { coordinates: "0.1,0.1,0.9,0.1,0.9,0.9,0.1,0.9" },
+ },
+ },
+ },
+ },
});
- if (await showReviewed.isVisible().catch(() => false)) {
- await showReviewed.click();
- await frigateApp.page.waitForTimeout(500);
- // Toggle should change state
- await expect(frigateApp.page.locator("body")).toBeVisible();
- }
+ await frigateApp.goto("/review");
+ const filterBtn = frigateApp.page
+ .getByRole("button", { name: /^filter$/i })
+ .first();
+ await expect(filterBtn).toBeVisible({ timeout: 5_000 });
+ await filterBtn.click();
+
+ const overlay = frigateApp.page.locator(
+ "[data-radix-popper-content-wrapper], [role='dialog']",
+ );
+ await expect(overlay.first()).toBeVisible({ timeout: 3_000 });
+ await expect(overlay.first().getByText(/front.?yard/i)).toBeVisible();
+ await frigateApp.page.keyboard.press("Escape");
});
- test("Last 24 Hours calendar button opens date picker", async ({
+ test("Calendar trigger opens a date picker popover", async ({
frigateApp,
}) => {
await frigateApp.goto("/review");
- await frigateApp.page.waitForTimeout(1000);
+ const review = new ReviewPage(frigateApp.page, true);
+ await expect(review.calendarTrigger).toBeVisible({ timeout: 5_000 });
+ await review.calendarTrigger.click();
- const calendarBtn = frigateApp.page.getByRole("button", {
- name: /24 hours|calendar|date/i,
- });
- if (await calendarBtn.isVisible().catch(() => false)) {
- await calendarBtn.click();
- await frigateApp.page.waitForTimeout(500);
- // Popover should open
- const popover = frigateApp.page.locator(
- "[data-radix-popper-content-wrapper]",
- );
- if (
- await popover
- .first()
- .isVisible()
- .catch(() => false)
- ) {
- await frigateApp.page.keyboard.press("Escape");
- }
- }
+ // react-day-picker v9 renders a role="grid" calendar with day cells
+ // as buttons inside gridcells (e.g. "Wednesday, April 1st, 2026").
+ // The calendar is placed directly in the DOM (not always inside a
+ // Radix popper wrapper), so scope by the grid role instead.
+ const calendarGrid = frigateApp.page.locator('[role="grid"]').first();
+ await expect(calendarGrid).toBeVisible({ timeout: 3_000 });
+ const dayButton = calendarGrid.locator('[role="gridcell"] button').first();
+ await expect(dayButton).toBeVisible({ timeout: 3_000 });
+ await frigateApp.page.keyboard.press("Escape");
});
- test("Filter button opens filter popover", async ({ frigateApp }) => {
- await frigateApp.goto("/review");
- await frigateApp.page.waitForTimeout(1000);
-
- const filterBtn = frigateApp.page.getByRole("button", {
- name: /^filter$/i,
- });
- if (await filterBtn.isVisible().catch(() => false)) {
- await filterBtn.click();
- await frigateApp.page.waitForTimeout(500);
- // Popover or dialog should open
- const popover = frigateApp.page.locator(
- "[data-radix-popper-content-wrapper], [role='dialog']",
- );
- if (
- await popover
- .first()
- .isVisible()
- .catch(() => false)
- ) {
- await frigateApp.page.keyboard.press("Escape");
- }
- }
- });
-});
-
-test.describe("Review Page - Timeline @critical", () => {
- test("review page has timeline with time markers (desktop)", async ({
+ test("Show Reviewed switch flips its checked state", async ({
frigateApp,
}) => {
- if (frigateApp.isMobile) {
- test.skip();
- return;
- }
+ // "Show Reviewed" is a Radix Switch (role=switch), not a button.
+ // It filters review data client-side; it does not trigger a new
+ // /api/review network request. Verify the switch state toggles.
await frigateApp.goto("/review");
- await frigateApp.page.waitForTimeout(2000);
- // Timeline renders time labels like "4:30 PM"
- const pageText = await frigateApp.page.textContent("#pageRoot");
- expect(pageText).toMatch(/[AP]M/);
+ const showReviewedSwitch = frigateApp.page.getByRole("switch", {
+ name: /show reviewed/i,
+ });
+ await expect(showReviewedSwitch).toBeVisible({ timeout: 5_000 });
+
+ // Record initial checked state and click to toggle
+ const initialChecked =
+ await showReviewedSwitch.getAttribute("aria-checked");
+ await showReviewedSwitch.click();
+ const flippedChecked = initialChecked === "true" ? "false" : "true";
+ await expect(showReviewedSwitch).toHaveAttribute(
+ "aria-checked",
+ flippedChecked,
+ );
});
});
-test.describe("Review Page - Navigation @critical", () => {
- test("navigate to review from live page works", async ({ frigateApp }) => {
+test.describe("Review — timeline (desktop) @critical", () => {
+ test.skip(
+ ({ frigateApp }) => frigateApp.isMobile,
+ "Timeline not shown on mobile",
+ );
+
+ test("timeline renders time markers", async ({ frigateApp }) => {
+ await frigateApp.goto("/review");
+ await expect
+ .poll(
+ async () => (await frigateApp.page.textContent("#pageRoot")) ?? "",
+ { timeout: 10_000 },
+ )
+ .toMatch(/[AP]M|\d+:\d+/);
+ });
+});
+
+test.describe("Review — mobile @critical @mobile", () => {
+ test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
+
+ test("severity tabs render on mobile", async ({ frigateApp }) => {
+ await frigateApp.goto("/review");
+ const review = new ReviewPage(frigateApp.page, false);
+ await expect(review.alertsTab).toBeVisible({ timeout: 10_000 });
+ await expect(review.detectionsTab).toBeVisible();
+ });
+
+ test("back navigation returns to Live", async ({ frigateApp }) => {
await frigateApp.goto("/");
- const base = new BasePage(frigateApp.page, !frigateApp.isMobile);
+ const base = new BasePage(frigateApp.page, false);
await base.navigateTo("/review");
await expect(frigateApp.page).toHaveURL(/\/review/);
- // Severity tabs should be visible
- await expect(frigateApp.page.getByLabel("Alerts")).toBeVisible({
- timeout: 10_000,
- });
+ await base.navigateTo("/");
+ await expect(frigateApp.page).toHaveURL(/\/$/);
});
});
diff --git a/web/e2e/specs/settings/camera-ffmpeg-streams.spec.ts b/web/e2e/specs/settings/camera-ffmpeg-streams.spec.ts
new file mode 100644
index 0000000000..38b0f5c4a6
--- /dev/null
+++ b/web/e2e/specs/settings/camera-ffmpeg-streams.spec.ts
@@ -0,0 +1,203 @@
+/**
+ * Camera ffmpeg streams settings tests -- MEDIUM tier.
+ *
+ * Covers the input-path source toggle: each ffmpeg input can either point at a
+ * go2rtc restream (picked from a dropdown, which writes the rtsp://127.0.0.1:8554
+ * path plus the preset-rtsp-restream input_args) or use a manually typed path.
+ */
+
+import { readFileSync } from "node:fs";
+import { resolve, dirname } from "node:path";
+import { fileURLToPath } from "node:url";
+import { test, expect } from "../../fixtures/frigate-test";
+import type { Page } from "@playwright/test";
+import { configFactory } from "../../fixtures/mock-data/config";
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const CONFIG_SCHEMA = JSON.parse(
+ readFileSync(
+ resolve(__dirname, "../../fixtures/mock-data/config-schema.json"),
+ "utf-8",
+ ),
+);
+
+const GO2RTC_STREAMS = {
+ dome_main: ["rtsp://user:pass@192.168.0.20:554/Stream1"],
+ dome_sub: ["rtsp://user:pass@192.168.0.20:554/Stream2"],
+};
+
+type CameraInput = {
+ path: string;
+ roles: string[];
+ input_args?: string;
+};
+
+async function installRoutes(page: Page, frontDoorInputs: CameraInput[]) {
+ const config = configFactory({
+ go2rtc: { streams: GO2RTC_STREAMS },
+ cameras: {
+ front_door: {
+ ffmpeg: { inputs: frontDoorInputs },
+ },
+ },
+ });
+
+ let lastSavedConfig: unknown = null;
+
+ await page.route("**/api/config/schema.json", (route) =>
+ route.fulfill({ json: CONFIG_SCHEMA }),
+ );
+ await page.route("**/api/config", (route) => {
+ if (route.request().method() === "GET") {
+ return route.fulfill({ json: config });
+ }
+ return route.fulfill({ json: { success: true } });
+ });
+ await page.route("**/api/config/raw_paths", (route) =>
+ route.fulfill({
+ json: {
+ cameras: { front_door: { ffmpeg: { inputs: frontDoorInputs } } },
+ go2rtc: { streams: GO2RTC_STREAMS },
+ },
+ }),
+ );
+ await page.route("**/api/config/set", async (route) => {
+ lastSavedConfig = route.request().postDataJSON();
+ await route.fulfill({ json: { success: true, require_restart: false } });
+ });
+ await page.route("**/api/ffmpeg/presets", (route) =>
+ route.fulfill({
+ json: {
+ hwaccel_args: [],
+ input_args: ["preset-rtsp-restream", "preset-rtsp-generic"],
+ output_args: { record: [], detect: [] },
+ },
+ }),
+ );
+
+ return { capturedConfig: () => lastSavedConfig };
+}
+
+const RESTREAM_RADIO = "Restream (go2rtc)";
+const MANUAL_RADIO = "Manual input path";
+
+test.describe("camera ffmpeg input source toggle @medium", () => {
+ test("manual input defaults to the manual text field", async ({
+ frigateApp,
+ }) => {
+ await installRoutes(frigateApp.page, [
+ { path: "rtsp://10.0.0.1:554/video", roles: ["detect"] },
+ ]);
+ await frigateApp.goto("/settings?page=cameraFfmpeg&camera=front_door");
+
+ await expect(
+ frigateApp.page.getByRole("radio", { name: MANUAL_RADIO }),
+ ).toBeChecked();
+ await expect(
+ frigateApp.page.getByRole("textbox", { name: "Input path" }),
+ ).toHaveValue("rtsp://10.0.0.1:554/video");
+ });
+
+ test("an existing restream path auto-detects into restream mode", async ({
+ frigateApp,
+ }) => {
+ await installRoutes(frigateApp.page, [
+ {
+ path: "rtsp://127.0.0.1:8554/dome_main",
+ roles: ["detect"],
+ input_args: "preset-rtsp-restream",
+ },
+ ]);
+ await frigateApp.goto("/settings?page=cameraFfmpeg&camera=front_door");
+
+ await expect(
+ frigateApp.page.getByRole("radio", { name: RESTREAM_RADIO }),
+ ).toBeChecked();
+ // The dropdown is preselected to the matching go2rtc stream.
+ await expect(
+ frigateApp.page.getByRole("combobox", { name: /go2rtc stream/i }),
+ ).toContainText("dome_main");
+ });
+
+ test("selecting a restream writes the path and preset", async ({
+ frigateApp,
+ }) => {
+ const capture = await installRoutes(frigateApp.page, [
+ { path: "rtsp://10.0.0.1:554/video", roles: ["detect"] },
+ ]);
+ await frigateApp.goto("/settings?page=cameraFfmpeg&camera=front_door");
+
+ await frigateApp.page.getByRole("radio", { name: RESTREAM_RADIO }).click();
+ await frigateApp.page
+ .getByRole("combobox", { name: /go2rtc stream/i })
+ .click();
+
+ // The dropdown is searchable: typing narrows the list to matches only,
+ // with no option to enter a custom stream name.
+ await frigateApp.page.getByPlaceholder("Search streams...").fill("sub");
+ await expect(
+ frigateApp.page.getByRole("option", { name: "dome_main" }),
+ ).toBeHidden();
+ await frigateApp.page.getByRole("option", { name: "dome_sub" }).click();
+
+ await frigateApp.page.getByRole("button", { name: "Save" }).click();
+
+ await expect
+ .poll(() => capture.capturedConfig(), { timeout: 5_000 })
+ .toMatchObject({
+ config_data: {
+ cameras: {
+ front_door: {
+ ffmpeg: {
+ inputs: [
+ {
+ path: "rtsp://127.0.0.1:8554/dome_sub",
+ input_args: "preset-rtsp-restream",
+ },
+ ],
+ },
+ },
+ },
+ },
+ });
+ });
+
+ test("switching a restream back to manual reverts the preset", async ({
+ frigateApp,
+ }) => {
+ const capture = await installRoutes(frigateApp.page, [
+ {
+ path: "rtsp://127.0.0.1:8554/dome_main",
+ roles: ["detect"],
+ input_args: "preset-rtsp-restream",
+ },
+ ]);
+ await frigateApp.goto("/settings?page=cameraFfmpeg&camera=front_door");
+
+ await frigateApp.page.getByRole("radio", { name: MANUAL_RADIO }).click();
+
+ // The restream path stays editable in the manual text field.
+ await expect(
+ frigateApp.page.getByRole("textbox", { name: "Input path" }),
+ ).toHaveValue("rtsp://127.0.0.1:8554/dome_main");
+
+ await frigateApp.page.getByRole("button", { name: "Save" }).click();
+
+ await expect
+ .poll(() => capture.capturedConfig(), { timeout: 5_000 })
+ .not.toBeNull();
+
+ const payload = capture.capturedConfig() as {
+ config_data?: {
+ cameras?: {
+ front_door?: {
+ ffmpeg?: { inputs?: Array<{ input_args?: unknown }> };
+ };
+ };
+ };
+ };
+ const input =
+ payload?.config_data?.cameras?.front_door?.ffmpeg?.inputs?.[0];
+ expect(input?.input_args).not.toBe("preset-rtsp-restream");
+ });
+});
diff --git a/web/e2e/specs/settings/detectors-and-model.spec.ts b/web/e2e/specs/settings/detectors-and-model.spec.ts
new file mode 100644
index 0000000000..f697b2b2d6
--- /dev/null
+++ b/web/e2e/specs/settings/detectors-and-model.spec.ts
@@ -0,0 +1,55 @@
+/**
+ * Detectors and model settings page tests -- HIGH tier.
+ *
+ * Tests rendering of the merged page and navigation from the Frigate+ page.
+ */
+
+import { test, expect } from "../../fixtures/frigate-test";
+
+test.describe("Detectors and model Settings @high", () => {
+ test("page renders with detector and model cards", async ({ frigateApp }) => {
+ await frigateApp.goto("/settings?page=systemDetectorsAndModel");
+ await frigateApp.page.waitForTimeout(2000);
+ await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
+
+ const text = await frigateApp.page.textContent("#pageRoot");
+ expect(text).toContain("Detectors and model");
+ expect(text?.toLowerCase()).toContain("detector hardware");
+ expect(text?.toLowerCase()).toContain("detection model");
+ });
+
+ test("Frigate+ page links to the merged page", async ({ frigateApp }) => {
+ await frigateApp.goto("/settings?page=frigateplus");
+ await frigateApp.page.waitForTimeout(2000);
+
+ const button = frigateApp.page.getByRole("button", {
+ name: /Change in Detectors and model/,
+ });
+
+ // Button only appears when Frigate+ is enabled in the test config; skip
+ // the click assertion if it's not present.
+ if ((await button.count()) > 0) {
+ await button.first().click();
+ await frigateApp.page.waitForURL(/page=systemDetectorsAndModel/);
+ await expect(frigateApp.page.locator("#pageRoot")).toContainText(
+ "Detectors and model",
+ );
+ } else {
+ test.skip(
+ true,
+ "Frigate+ not enabled in this test config; skipping link assertion",
+ );
+ }
+ });
+
+ test("old systemDetectionModel deep-link no longer routes here", async ({
+ frigateApp,
+ }) => {
+ await frigateApp.goto("/settings?page=systemDetectionModel");
+ await frigateApp.page.waitForTimeout(2000);
+ // The old page key is no longer in allSettingsViews; the router
+ // falls back to its default settings page (uiSettings).
+ const text = await frigateApp.page.textContent("#pageRoot");
+ expect(text).not.toContain("Detection model");
+ });
+});
diff --git a/web/e2e/specs/settings/go2rtc-streams.spec.ts b/web/e2e/specs/settings/go2rtc-streams.spec.ts
new file mode 100644
index 0000000000..223a261bef
--- /dev/null
+++ b/web/e2e/specs/settings/go2rtc-streams.spec.ts
@@ -0,0 +1,235 @@
+/**
+ * go2rtc streams settings page tests -- MEDIUM tier.
+ *
+ * Regression coverage for the compat-mode (ffmpeg:) URL editor: unknown
+ * fragments like #timeout=10 must remain visible and editable when the
+ * stream is using compatibility mode.
+ */
+
+import { test, expect } from "../../fixtures/frigate-test";
+import type { Page } from "@playwright/test";
+
+const STREAM_NAME = "dome_sub";
+const FFMPEG_URL_WITH_TIMEOUT =
+ "ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy#timeout=10";
+
+async function installRawPathsRoute(page: Page, streamUrl: string) {
+ let lastSavedConfig: unknown = null;
+ await page.route("**/api/config/raw_paths", (route) =>
+ route.fulfill({
+ json: {
+ cameras: {},
+ go2rtc: { streams: { [STREAM_NAME]: [streamUrl] } },
+ },
+ }),
+ );
+ await page.route("**/api/config/set", async (route) => {
+ lastSavedConfig = route.request().postDataJSON();
+ await route.fulfill({ json: { success: true, require_restart: false } });
+ });
+ return {
+ capturedConfig: () => lastSavedConfig,
+ };
+}
+
+async function expandStream(page: Page, streamName: string) {
+ // Each StreamCard renders the stream name as an h4 next to a rename
+ // button, with the chevron toggle as the last button in the header row.
+ // Scope to the header row (h4's grandparent) and click that last button.
+ const headerRow = page
+ .locator(`h4:text-is("${streamName}")`)
+ .locator("xpath=../..");
+ await headerRow.getByRole("button").last().click();
+}
+
+test.describe("go2rtc streams settings — ffmpeg compat mode @medium", () => {
+ test("preserves unknown fragments like #timeout= in the URL input", async ({
+ frigateApp,
+ }) => {
+ await installRawPathsRoute(frigateApp.page, FFMPEG_URL_WITH_TIMEOUT);
+ await frigateApp.goto("/settings?page=systemGo2rtcStreams");
+
+ await expect(
+ frigateApp.page.getByRole("heading", { name: STREAM_NAME }),
+ ).toBeVisible();
+
+ await expandStream(frigateApp.page, STREAM_NAME);
+
+ const urlInput = frigateApp.page.getByPlaceholder(
+ "e.g., rtsp://user:pass@192.168.1.100/stream",
+ );
+ await expect(urlInput).toBeVisible();
+
+ // Focus the input so credential masking is bypassed and the raw value
+ // is rendered — this matches how a user would inspect the URL before
+ // editing it.
+ await urlInput.focus();
+ await expect(urlInput).toHaveValue(
+ "rtsp://user:pass@192.168.0.20:554/Stream1#timeout=10",
+ );
+ });
+
+ test("lets the user add an extra fragment in compat mode", async ({
+ frigateApp,
+ }) => {
+ const capture = await installRawPathsRoute(
+ frigateApp.page,
+ FFMPEG_URL_WITH_TIMEOUT,
+ );
+ await frigateApp.goto("/settings?page=systemGo2rtcStreams");
+ await expandStream(frigateApp.page, STREAM_NAME);
+
+ const urlInput = frigateApp.page.getByPlaceholder(
+ "e.g., rtsp://user:pass@192.168.1.100/stream",
+ );
+ await urlInput.focus();
+ await urlInput.fill(
+ "rtsp://user:pass@192.168.0.20:554/Stream1#timeout=10#backchannel=0",
+ );
+ await urlInput.blur();
+
+ // Reopen and re-focus to assert the new value round-tripped through
+ // parseFfmpegBaseAndExtras + buildFfmpegUrl back into the displayed text.
+ await urlInput.focus();
+ await expect(urlInput).toHaveValue(
+ "rtsp://user:pass@192.168.0.20:554/Stream1#timeout=10#backchannel=0",
+ );
+
+ // Save and verify the persisted URL includes both extras after the
+ // recognized video/audio directives.
+ await frigateApp.page.getByRole("button", { name: "Save" }).click();
+ await expect
+ .poll(() => capture.capturedConfig(), { timeout: 5_000 })
+ .toMatchObject({
+ config_data: {
+ go2rtc: {
+ streams: {
+ [STREAM_NAME]: [
+ "ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy#timeout=10#backchannel=0",
+ ],
+ },
+ },
+ },
+ });
+ });
+
+ test("preserves repeatable #audio= fallback chain and lets the user add another codec", async ({
+ frigateApp,
+ }) => {
+ const capture = await installRawPathsRoute(
+ frigateApp.page,
+ // Idiomatic go2rtc fallback: copy if source has the codec, else transcode
+ "ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy#audio=opus",
+ );
+ await frigateApp.goto("/settings?page=systemGo2rtcStreams");
+ await expandStream(frigateApp.page, STREAM_NAME);
+
+ // Two pre-populated audio rows — one per #audio= fragment.
+ const audioLabel = frigateApp.page.locator(`label:text-is("Audio")`);
+ const audioRowsContainer = audioLabel.locator("xpath=../..");
+ await expect(audioRowsContainer.getByRole("combobox")).toHaveCount(2);
+ await expect(audioRowsContainer.getByRole("combobox").first()).toHaveText(
+ "Copy",
+ );
+ await expect(audioRowsContainer.getByRole("combobox").nth(1)).toHaveText(
+ "Transcode to Opus",
+ );
+
+ // Add a third audio codec via the LuPlus next to the "Audio" label.
+ await audioRowsContainer
+ .getByRole("button", { name: "Add audio codec" })
+ .click();
+ await expect(audioRowsContainer.getByRole("combobox")).toHaveCount(3);
+
+ // Change the newly-added entry to AAC.
+ await audioRowsContainer.getByRole("combobox").nth(2).click();
+ await frigateApp.page
+ .getByRole("option", { name: "Transcode to AAC" })
+ .click();
+
+ await frigateApp.page.getByRole("button", { name: "Save" }).click();
+ await expect
+ .poll(() => capture.capturedConfig(), { timeout: 5_000 })
+ .toMatchObject({
+ config_data: {
+ go2rtc: {
+ streams: {
+ [STREAM_NAME]: [
+ "ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy#audio=opus#audio=aac",
+ ],
+ },
+ },
+ },
+ });
+ });
+
+ test("LuX is only shown on fallback rows and removes only that codec", async ({
+ frigateApp,
+ }) => {
+ const capture = await installRawPathsRoute(
+ frigateApp.page,
+ "ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy#audio=opus",
+ );
+ await frigateApp.goto("/settings?page=systemGo2rtcStreams");
+ await expandStream(frigateApp.page, STREAM_NAME);
+
+ const audioLabel = frigateApp.page.locator(`label:text-is("Audio")`);
+ const audioRowsContainer = audioLabel.locator("xpath=../..");
+ const removeButtons = audioRowsContainer.getByRole("button", {
+ name: "Remove codec",
+ });
+ // Primary (audio=copy) row is permanent and has no X; only the audio=opus
+ // fallback exposes a remove button.
+ await expect(removeButtons).toHaveCount(1);
+
+ await removeButtons.first().click();
+ await expect(audioRowsContainer.getByRole("combobox")).toHaveCount(1);
+ await expect(audioRowsContainer.getByRole("combobox")).toHaveText("Copy");
+
+ await frigateApp.page.getByRole("button", { name: "Save" }).click();
+ await expect
+ .poll(() => capture.capturedConfig(), { timeout: 5_000 })
+ .toMatchObject({
+ config_data: {
+ go2rtc: {
+ streams: {
+ [STREAM_NAME]: [
+ "ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy",
+ ],
+ },
+ },
+ },
+ });
+ });
+
+ test("picking Exclude on the primary row drops the #video= fragment entirely", async ({
+ frigateApp,
+ }) => {
+ const capture = await installRawPathsRoute(
+ frigateApp.page,
+ "ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy",
+ );
+ await frigateApp.goto("/settings?page=systemGo2rtcStreams");
+ await expandStream(frigateApp.page, STREAM_NAME);
+
+ const videoLabel = frigateApp.page.locator(`label:text-is("Video")`);
+ const videoRowsContainer = videoLabel.locator("xpath=../..");
+ await videoRowsContainer.getByRole("combobox").first().click();
+ await frigateApp.page.getByRole("option", { name: "Exclude" }).click();
+
+ await frigateApp.page.getByRole("button", { name: "Save" }).click();
+ await expect
+ .poll(() => capture.capturedConfig(), { timeout: 5_000 })
+ .toMatchObject({
+ config_data: {
+ go2rtc: {
+ streams: {
+ [STREAM_NAME]: [
+ "ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#audio=copy",
+ ],
+ },
+ },
+ },
+ });
+ });
+});
diff --git a/web/e2e/specs/system.spec.ts b/web/e2e/specs/system.spec.ts
index a3aa512e5b..87e4db51ea 100644
--- a/web/e2e/specs/system.spec.ts
+++ b/web/e2e/specs/system.spec.ts
@@ -1,15 +1,21 @@
/**
- * System page tests -- MEDIUM tier.
+ * System page tests -- MEDIUM tier (promoted to cover migrated
+ * RestartDialog test from radix-overlay-regressions.spec.ts).
*
- * Tests system page rendering with tabs and tab switching.
- * Navigates to /system#general explicitly so useHashState resolves
- * the tab state deterministically.
+ * Tab switching, version + last-refreshed display, and the
+ * RestartDialog cancel flow.
*/
import { test, expect } from "../fixtures/frigate-test";
+import {
+ expectBodyInteractive,
+ waitForBodyInteractive,
+} from "../helpers/overlay-interaction";
-test.describe("System Page @medium", () => {
- test("system page renders with tab buttons", async ({ frigateApp }) => {
+test.describe("System — tabs @medium", () => {
+ test("general tab is active by default via #general hash", async ({
+ frigateApp,
+ }) => {
await frigateApp.goto("/system#general");
await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute(
"data-state",
@@ -20,7 +26,7 @@ test.describe("System Page @medium", () => {
await expect(frigateApp.page.getByLabel("Select cameras")).toBeVisible();
});
- test("general tab is active when navigated via hash", async ({
+ test("Storage tab activates and deactivates General", async ({
frigateApp,
}) => {
await frigateApp.goto("/system#general");
@@ -29,18 +35,6 @@ test.describe("System Page @medium", () => {
"on",
{ timeout: 15_000 },
);
- });
-
- test("clicking Storage tab activates it and deactivates General", async ({
- frigateApp,
- }) => {
- await frigateApp.goto("/system#general");
- await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute(
- "data-state",
- "on",
- { timeout: 15_000 },
- );
-
await frigateApp.page.getByLabel("Select storage").click();
await expect(frigateApp.page.getByLabel("Select storage")).toHaveAttribute(
"data-state",
@@ -53,29 +47,22 @@ test.describe("System Page @medium", () => {
);
});
- test("clicking Cameras tab activates it and deactivates General", async ({
- frigateApp,
- }) => {
+ test("Cameras tab activates", async ({ frigateApp }) => {
await frigateApp.goto("/system#general");
await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute(
"data-state",
"on",
{ timeout: 15_000 },
);
-
await frigateApp.page.getByLabel("Select cameras").click();
await expect(frigateApp.page.getByLabel("Select cameras")).toHaveAttribute(
"data-state",
"on",
{ timeout: 5_000 },
);
- await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute(
- "data-state",
- "off",
- );
});
- test("system page shows version and last refreshed", async ({
+ test("general tab shows version and last-refreshed", async ({
frigateApp,
}) => {
await frigateApp.goto("/system#general");
@@ -87,4 +74,164 @@ test.describe("System Page @medium", () => {
await expect(frigateApp.page.getByText("0.15.0-test")).toBeVisible();
await expect(frigateApp.page.getByText(/Last refreshed/)).toBeVisible();
});
+
+ test("storage tab renders content after switching", async ({
+ frigateApp,
+ }) => {
+ await frigateApp.goto("/system#general");
+ await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute(
+ "data-state",
+ "on",
+ { timeout: 15_000 },
+ );
+ await frigateApp.page.getByLabel("Select storage").click();
+ await expect(frigateApp.page.getByLabel("Select storage")).toHaveAttribute(
+ "data-state",
+ "on",
+ { timeout: 5_000 },
+ );
+ // On desktop, tab buttons render text labels so the word "storage"
+ // always appears in #pageRoot after switching. On mobile, tabs are
+ // icon-only, so we verify the general-tab content disappears instead
+ // (the storage tab's metrics section is hidden but general is gone).
+ if (!frigateApp.isMobile) {
+ await expect
+ .poll(
+ async () => (await frigateApp.page.textContent("#pageRoot")) ?? "",
+ { timeout: 10_000 },
+ )
+ .toMatch(/storage|mount|disk|used|free/i);
+ } else {
+ // Mobile: tab activation (data-state "on") already asserted above.
+ // Additionally confirm general tab is no longer the active tab.
+ await expect(
+ frigateApp.page.getByLabel("Select general"),
+ ).toHaveAttribute("data-state", "off", { timeout: 5_000 });
+ }
+ });
+
+ test("cameras tab renders each configured camera", async ({ frigateApp }) => {
+ await frigateApp.goto("/system#general");
+ await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute(
+ "data-state",
+ "on",
+ { timeout: 15_000 },
+ );
+ await frigateApp.page.getByLabel("Select cameras").click();
+ await expect(frigateApp.page.getByLabel("Select cameras")).toHaveAttribute(
+ "data-state",
+ "on",
+ { timeout: 5_000 },
+ );
+ // Cameras tab lists every camera from config/stats. The default
+ // mock has front_door, backyard, garage.
+ for (const cam of ["front_door", "backyard", "garage"]) {
+ await expect(
+ frigateApp.page
+ .getByText(new RegExp(cam.replace("_", ".?"), "i"))
+ .first(),
+ ).toBeVisible({ timeout: 10_000 });
+ }
+ });
+
+ test("enrichments tab renders when semantic search is enabled", async ({
+ frigateApp,
+ }) => {
+ // Override config to guarantee the enrichments tab is present.
+ // System.tsx shows the tab when semantic_search.enabled === true.
+ await frigateApp.installDefaults({
+ config: { semantic_search: { enabled: true } },
+ });
+ await frigateApp.goto("/system#general");
+ await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute(
+ "data-state",
+ "on",
+ { timeout: 15_000 },
+ );
+ const enrichTab = frigateApp.page.getByLabel(/select enrichments/i).first();
+ await expect(enrichTab).toBeVisible({ timeout: 5_000 });
+ await enrichTab.click();
+ await expect(enrichTab).toHaveAttribute("data-state", "on", {
+ timeout: 5_000,
+ });
+ });
+});
+
+test.describe("System — RestartDialog @medium", () => {
+ test.skip(
+ ({ frigateApp }) => frigateApp.isMobile,
+ "Sidebar menu is desktop-only",
+ );
+
+ test("cancelling restart leaves body interactive", async ({ frigateApp }) => {
+ // Migrated from radix-overlay-regressions.spec.ts.
+ await frigateApp.goto("/");
+
+ const sidebarTriggers = frigateApp.page
+ .locator('[role="complementary"] [aria-haspopup="menu"]')
+ .or(frigateApp.page.locator('aside [aria-haspopup="menu"]'));
+ const triggerCount = await sidebarTriggers.count();
+ expect(triggerCount).toBeGreaterThan(0);
+
+ let opened = false;
+ for (let i = 0; i < triggerCount; i++) {
+ const trigger = sidebarTriggers.nth(i);
+ await trigger.click().catch(() => {});
+ const restartItem = frigateApp.page
+ .getByRole("menuitem", { name: /restart/i })
+ .first();
+ const visible = await expect(restartItem)
+ .toBeVisible({ timeout: 300 })
+ .then(() => true)
+ .catch(() => false);
+ if (visible) {
+ await restartItem.click();
+ opened = true;
+ break;
+ }
+ await frigateApp.page.keyboard.press("Escape").catch(() => {});
+ }
+ expect(opened).toBe(true);
+
+ const cancel = frigateApp.page.getByRole("button", { name: /cancel/i });
+ await expect(cancel).toBeVisible({ timeout: 3_000 });
+ await cancel.click();
+
+ await waitForBodyInteractive(frigateApp.page);
+ await expectBodyInteractive(frigateApp.page);
+
+ const postCancelTrigger = sidebarTriggers.first();
+ await postCancelTrigger.click();
+ await expect(
+ frigateApp.page
+ .locator('[role="menu"], [data-radix-menu-content]')
+ .first(),
+ ).toBeVisible({ timeout: 3_000 });
+ });
+});
+
+test.describe("System — mobile @medium @mobile", () => {
+ test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");
+
+ test("tabs render at mobile viewport", async ({ frigateApp }) => {
+ await frigateApp.goto("/system#general");
+ await expect(frigateApp.page.getByLabel("Select general")).toBeVisible({
+ timeout: 15_000,
+ });
+ });
+
+ test("switching tabs works at mobile viewport", async ({ frigateApp }) => {
+ await frigateApp.goto("/system#general");
+ await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute(
+ "data-state",
+ "on",
+ { timeout: 15_000 },
+ );
+ await frigateApp.page.getByLabel("Select storage").click();
+ await expect(frigateApp.page.getByLabel("Select storage")).toHaveAttribute(
+ "data-state",
+ "on",
+ { timeout: 5_000 },
+ );
+ });
});
diff --git a/web/index.html b/web/index.html
index be6b302f5c..bb019d4263 100644
--- a/web/index.html
+++ b/web/index.html
@@ -3,7 +3,7 @@
-
+
Frigate
-
+
Frigate
{{name}}?"
+ }
+ },
+ "name": {
+ "label": "Ime",
+ "placeholder": "Unesite ime…",
+ "errorMessage": {
+ "mustLeastCharacters": "Ime grupe kamere mora imati najmanje 2 karaktera.",
+ "exists": "Ime grupe kamere već postoji.",
+ "nameMustNotPeriod": "Ime grupe kamere ne smije sadržavati tačku.",
+ "invalid": "Neispravno ime grupe kamere."
+ }
+ },
+ "cameras": {
+ "label": "Kamere",
+ "desc": "Odaberite kamere za ovu grupu."
+ },
+ "icon": "Ikona",
+ "success": "Grupa kamere ({{name}}) je sačuvana.",
+ "camera": {
+ "birdseye": "Birdseye",
+ "setting": {
+ "label": "Postavke prenošenja kamere",
+ "title": "Postavke prenošenja {{cameraName}}",
+ "desc": "Promijenite opcije uživo prenošenja za tablicu upravljanja ove grupe kamere. Ove postavke su specifične za uređaj/pretvarač. ",
+ "audioIsAvailable": "Audio je dostupan za ovaj stream",
+ "audioIsUnavailable": "Zvuk nije dostupan za ovaj tok",
+ "audio": {
+ "tips": {
+ "title": "Audio mora biti izlaz iz vaše kamere i konfiguriran u go2rtc za ovaj stream."
+ }
+ },
+ "stream": "Tok",
+ "placeholder": "Odaberite tok",
+ "streamMethod": {
+ "label": "Način prenošenja",
+ "placeholder": "Odaberite način prenošenja",
+ "method": {
+ "noStreaming": {
+ "label": "Bez prenošenja",
+ "desc": "Slike kamere će se ažurirati samo jednom na minut i neće se dogoditi uživo prenošenje."
+ },
+ "smartStreaming": {
+ "label": "Pametno prenošenje (preporučeno)",
+ "desc": "Pametno prenošenje će ažurirati sliku kamere jednom na minut kada se ne događa detektovana aktivnost kako bi se uštedjelo na širovini i resursima. Kada se detektuje aktivnost, slika se glatko prebacuje u uživo prenošenje."
+ },
+ "continuousStreaming": {
+ "label": "Neprekidno prenošenje",
+ "desc": {
+ "title": "Slika kamere uvijek će biti živo prenošenje kada je vidljiva na ploči, čak i ako se ne detektira aktivnost.",
+ "warning": "Neprekidno prenošenje može uzrokovati visoku upotrebu širine pojasa i probleme s performansama. Koristite s oprezom."
+ }
+ }
+ }
+ },
+ "compatibilityMode": {
+ "label": "Režim kompatibilnosti",
+ "desc": "Omogućite ovu opciju samo ako se živo prenošenje vaše kamere prikazuje s bojnim artefaktima i dijagonalnom linijom na desnoj strani slike."
+ }
+ }
+ }
+ },
+ "debug": {
+ "options": {
+ "label": "Postavke",
+ "title": "Opcije",
+ "showOptions": "Prikaži opcije",
+ "hideOptions": "Sakrij opcije"
+ },
+ "boundingBox": "Okvir",
+ "timestamp": "Vremenski pečat",
+ "zones": "Zone",
+ "mask": "Maska",
+ "motion": "Kretanje",
+ "regions": "Regije",
+ "paths": "Putanje"
+ }
+}
diff --git a/web/public/locales/bs/components/dialog.json b/web/public/locales/bs/components/dialog.json
new file mode 100644
index 0000000000..95f4adad24
--- /dev/null
+++ b/web/public/locales/bs/components/dialog.json
@@ -0,0 +1,197 @@
+{
+ "restart": {
+ "title": "Sigurni li ste da želite ponovno pokrenuti Frigate?",
+ "description": "Ovo privremeno zaustavi Frigate dok se ponovno pokreće.",
+ "button": "Ponovno pokretanje",
+ "restarting": {
+ "title": "Frigate se ponovo pokreće",
+ "content": "Ova stranica će se ponovno učitati za {{countdown}} sekundi.",
+ "button": "Silovito ponovno učitavanje sada"
+ }
+ },
+ "explore": {
+ "plus": {
+ "submitToPlus": {
+ "label": "Pošalji na Frigate+",
+ "desc": "Predmeti u lokacijama koje želite izbjeći nisu lažni pozitivi. Pošiljanje ih kao lažne pozitive zbunjuje model."
+ },
+ "review": {
+ "question": {
+ "label": "Potvrdite ovu oznaku za Frigate Plus",
+ "ask_a": "Je li ovaj objekt {{label}}?",
+ "ask_an": "Je li ovaj objekt {{label}}?",
+ "ask_full": "Je li ovaj objekt {{untranslatedLabel}} ({{translatedLabel}})?"
+ },
+ "state": {
+ "submitted": "Pošlato"
+ }
+ }
+ },
+ "video": {
+ "viewInHistory": "Pregledajte u povijesti"
+ }
+ },
+ "export": {
+ "time": {
+ "fromTimeline": "Odaberite iz vremenske linije",
+ "lastHour_one": "Prošli sat",
+ "lastHour_few": "Prošla {{count}} sata",
+ "lastHour_other": "Prošlih {{count}} sati",
+ "custom": "Prilagođeno",
+ "start": {
+ "title": "Vrijeme početka",
+ "label": "Odaberite vrijeme početka"
+ },
+ "end": {
+ "title": "Vrijeme kraja",
+ "label": "Odaberite vrijeme kraja"
+ }
+ },
+ "name": {
+ "placeholder": "Nazovite izvoz"
+ },
+ "case": {
+ "newCaseOption": "Napravite novi slučaj",
+ "newCaseNamePlaceholder": "Novo ime slučaja",
+ "newCaseDescriptionPlaceholder": "Opis slučaja",
+ "label": "Slučaj",
+ "nonAdminHelp": "Za ove izvoze će se stvoriti novi slučaj.",
+ "placeholder": "Odaberite slučaj"
+ },
+ "select": "Odaberite",
+ "export": "Izvoz",
+ "queueing": "Stavljanje izvoza u red...",
+ "selectOrExport": "Odaberite ili izvozite",
+ "tabs": {
+ "export": "Jedna kamera",
+ "multiCamera": "Više kamera"
+ },
+ "multiCamera": {
+ "timeRange": "Vremenski opseg",
+ "selectFromTimeline": "Odaberite iz vremenske linije",
+ "cameraSelection": "Kamere",
+ "cameraSelectionHelp": "Kamere s praćenim objektima u ovom vremenskom opsegu su preselektirane",
+ "checkingActivity": "Provjeravamo aktivnost kamere...",
+ "noCameras": "Nema dostupnih kamera",
+ "detectionCount_one": "1 praćen objekt",
+ "detectionCount_few": "{{count}} praćena objekta",
+ "detectionCount_other": "{{count}} praćenih objekata",
+ "nameLabel": "Ime izvoza",
+ "namePlaceholder": "Nepovlačenje baznog imena za ove izvoze",
+ "queueingButton": "Stavljanje izvoza u red...",
+ "exportButton_one": "Izvoz 1 kamere",
+ "exportButton_few": "Izvoz {{count}} kamere",
+ "exportButton_other": "Izvoz {{count}} kamera"
+ },
+ "multi": {
+ "title_one": "Izvoz 1 pregleda",
+ "title_few": "Izvoz {{count}} pregleda",
+ "title_other": "Izvoz {{count}} pregleda",
+ "description": "Izvoz svakog odabranih pregleda. Svi izvozi bit će grupirani pod jedan slučaj.",
+ "descriptionNoCase": "Izvoz svakog odabranih pregleda.",
+ "caseNamePlaceholder": "Pregled izvoza - {{date}}",
+ "exportButton_one": "Izvoz 1 pregleda",
+ "exportButton_few": "Izvoz {{count}} pregleda",
+ "exportButton_other": "Izvoz {{count}} pregleda",
+ "exportingButton": "Izvoz...",
+ "toast": {
+ "started_one": "Pokrenut 1 izvoz. Otvaranje slučaja sada.",
+ "started_few": "Pokrenuta {{count}} izvoza. Otvaranje slučaja sada.",
+ "started_other": "Pokrenuto {{count}} izvoza. Otvaranje slučaja sada.",
+ "startedNoCase_one": "Pokrenut 1 izvoz.",
+ "startedNoCase_few": "Pokrenuta {{count}} izvoza.",
+ "startedNoCase_other": "Pokrenuto {{count}} izvoza.",
+ "partial": "Pokrenuto {{successful}} od {{total}} izvoza. Neuspješno: {{failedItems}}",
+ "failed": "Neuspješno pokretanje {{total}} izvoza. Neuspješno: {{failedItems}}"
+ }
+ },
+ "toast": {
+ "success": "Uspješno pokrenut izvoz. Pregledajte datoteku na stranici izvoza.",
+ "queued": "Izvoz u redu. Pregledajte napredak na stranici izvoza.",
+ "view": "Pregled",
+ "batchSuccess_one": "Pokrenut 1 izvoz. Otvaranje slučaja sada.",
+ "batchSuccess_few": "Pokrenuta {{count}} izvoza. Otvaranje slučaja sada.",
+ "batchSuccess_other": "Pokrenuto {{count}} izvoza. Otvaranje slučaja sada.",
+ "batchPartial": "Pokrenuto {{successful}} od {{total}} izvoza. Neuspješne kamere: {{failedCameras}}",
+ "batchFailed": "Neuspješno pokretanje {{total}} izvoza. Neuspješne kamere: {{failedCameras}}",
+ "batchQueuedSuccess_one": "U red stavljen 1 izvoz. Otvaranje slučaja sada.",
+ "batchQueuedSuccess_few": "U red stavljena {{count}} izvoza. Otvaranje slučaja sada.",
+ "batchQueuedSuccess_other": "U red stavljeno {{count}} izvoza. Otvaranje slučaja sada.",
+ "batchQueuedPartial": "U redu {{successful}} od {{total}} izvoza. Neuspješne kamere: {{failedCameras}}",
+ "batchQueueFailed": "Neuspješno dodavanje {{total}} izvoza. Neuspješne kamere: {{failedCameras}}",
+ "error": {
+ "failed": "Neuspješno dodavanje izvoza: {{error}}",
+ "endTimeMustAfterStartTime": "Krajnje vrijeme mora biti nakon početnog vremena",
+ "noVaildTimeSelected": "Nije odabran valjan vremenski opseg"
+ }
+ },
+ "fromTimeline": {
+ "saveExport": "Sačuvaj izvoz",
+ "queueingExport": "Kopiranje izvoza...",
+ "previewExport": "Pregled izvoza",
+ "useThisRange": "Koristi ovaj opseg"
+ }
+ },
+ "streaming": {
+ "label": "Tok",
+ "restreaming": {
+ "disabled": "Restreaming nije omogućeno za ovu kameru.",
+ "desc": {
+ "title": "Postavite go2rtc za dodatne opcije uživog pregleda i zvuk za ovu kameru."
+ }
+ },
+ "showStats": {
+ "label": "Prikaži statistiku strima",
+ "desc": "Omogući ovu opciju da prikaže statistiku prijenosa kao preklapanje na toku kamere."
+ },
+ "debugView": "Pregled za otklanjanje grešaka"
+ },
+ "search": {
+ "saveSearch": {
+ "label": "Sačuvaj pretragu",
+ "desc": "Navedite ime za ovu sačuvanu pretragu.",
+ "placeholder": "Unesite ime za svoju pretragu",
+ "overwrite": "{{searchName}} već postoji. Sačuvavanje će prebrisati postojet će vrijednost.",
+ "success": "Pretraga ({{searchName}}) je sačuvana.",
+ "button": {
+ "save": {
+ "label": "Sačuvaj ovu pretragu"
+ }
+ }
+ }
+ },
+ "recording": {
+ "shareTimestamp": {
+ "label": "Dijeli vremensku oznaku",
+ "title": "Dijeli vremensku oznaku",
+ "description": "Dijelite URL označen vremenom trenutne pozicije igrača ili odaberite prilagođenu vremensku oznaku. Napomena: ovo nije javni URL za dijeljenje i dostupan je samo korisnicima koji imaju pristup Frigate i ovoj kameri.",
+ "custom": "Prilagođena vremenska oznaka",
+ "button": "URL za dijeljenje vremenske oznake",
+ "shareTitle": "Vremenska oznaka pregleda Frigate: {{camera}}"
+ },
+ "confirmDelete": {
+ "title": "Potvrdi brisanje",
+ "desc": {
+ "selected": "Sigurni li ste da želite izbrisati sve snimljeno video povezano s ovim preglednim stavkom? Zadržite tipku Shift da biste preskočili ovaj dijalog u budućnosti."
+ },
+ "toast": {
+ "success": "Video snimke povezane s odabranim preglednim stavcima uspješno su izbrisane.",
+ "error": "Neuspješno brisanje: {{error}}"
+ }
+ },
+ "button": {
+ "export": "Izvoz",
+ "markAsReviewed": "Označi kao pregledano",
+ "markAsUnreviewed": "Označi kao nepregledano",
+ "deleteNow": "Obriši sada"
+ }
+ },
+ "imagePicker": {
+ "selectImage": "Odaberite minijaturu praćenog objekta",
+ "unknownLabel": "Sačuvana slika izazivača",
+ "search": {
+ "placeholder": "Pretraga po oznaci ili podoznaci..."
+ },
+ "noImages": "Nema mini prikaza za ovu kameru"
+ }
+}
diff --git a/web/public/locales/bs/components/filter.json b/web/public/locales/bs/components/filter.json
new file mode 100644
index 0000000000..7578235133
--- /dev/null
+++ b/web/public/locales/bs/components/filter.json
@@ -0,0 +1,140 @@
+{
+ "filter": "Filtar",
+ "classes": {
+ "label": "Klase",
+ "all": {
+ "title": "Sve klase"
+ },
+ "count_one": "{{count}} Klasa",
+ "count_other": "{{count}} Klase"
+ },
+ "labels": {
+ "label": "Oznake",
+ "all": {
+ "title": "Sve oznake",
+ "short": "Oznake"
+ },
+ "count_one": "{{count}} Oznaka",
+ "count_other": "{{count}} Oznake"
+ },
+ "zones": {
+ "label": "Zone",
+ "all": {
+ "title": "Sve zone",
+ "short": "Zone"
+ }
+ },
+ "dates": {
+ "selectPreset": "Odaberite predpostavku…",
+ "all": {
+ "title": "Svi datumi",
+ "short": "Datumi"
+ }
+ },
+ "more": "Više filtera",
+ "reset": {
+ "label": "Poništi filtere na zadane vrijednosti"
+ },
+ "timeRange": "Vremenski opseg",
+ "subLabels": {
+ "label": "Podoznake",
+ "all": "Sve podoznake"
+ },
+ "attributes": {
+ "label": "Atributi klasifikacije",
+ "all": "Svi atributi"
+ },
+ "score": "Rezultat",
+ "estimatedSpeed": "Procijenjena brzina ({{unit}})",
+ "features": {
+ "label": "Funkcije",
+ "hasSnapshot": "Ima snimak",
+ "hasVideoClip": "Ima video zapis",
+ "submittedToFrigatePlus": {
+ "label": "Predano Frigate+",
+ "tips": "Prvo morate filtrirati prateće objekte koji imaju snimak. Prateći objekti bez snimka ne mogu se poslati na Frigate+."
+ }
+ },
+ "sort": {
+ "label": "Sortiraj",
+ "dateAsc": "Datum (Uzlazno)",
+ "dateDesc": "Datum (Silazno)",
+ "scoreAsc": "Ocjena objekta (Uzlazno)",
+ "scoreDesc": "Ocjena objekta (Silazno)",
+ "speedAsc": "Procijenjena brzina (Uzlazno)",
+ "speedDesc": "Procijenjena brzina (Silazno)",
+ "relevance": "Relevantnost"
+ },
+ "cameras": {
+ "label": "Filter kamere",
+ "all": {
+ "title": "Sve Kamere",
+ "short": "Kamere"
+ }
+ },
+ "review": {
+ "showReviewed": "Prikaži pregledane"
+ },
+ "motion": {
+ "showMotionOnly": "Prikaži samo pokret"
+ },
+ "explore": {
+ "settings": {
+ "title": "Postavke",
+ "defaultView": {
+ "title": "Zadani prikaz",
+ "desc": "Kada nisu odabrani filteri, prikazuje se sažetak najnovijih pratećih objekata po oznaci, ili prikazuje se mreža bez filtriranja.",
+ "summary": "Sažetak",
+ "unfilteredGrid": "Mreža bez filtriranja"
+ },
+ "gridColumns": {
+ "title": "Kolone mreže",
+ "desc": "Odaberite broj kolona u prikazu mreže."
+ },
+ "searchSource": {
+ "label": "Izvor pretrage",
+ "desc": "Odaberite da li ćete pretraživati miniaturne slike ili opise vaših praćenih objekata.",
+ "options": {
+ "thumbnailImage": "Miniaturna slika",
+ "description": "Opis"
+ }
+ }
+ },
+ "date": {
+ "selectDateBy": {
+ "label": "Odaberite datum za filtriranje"
+ }
+ }
+ },
+ "logSettings": {
+ "label": "Filtrirajte nivo zapisa",
+ "filterBySeverity": "Filtrirajte zapise prema ozbiljnosti",
+ "loading": {
+ "title": "Učitavanje",
+ "desc": "Kada se panel zapisa pomakne do dna, novi zapisi automatski se prikazuju kada se dodaju."
+ },
+ "disableLogStreaming": "Onemogući praćenje zapisa",
+ "allLogs": "Svi zapisi"
+ },
+ "trackedObjectDelete": {
+ "title": "Potvrdi brisanje",
+ "desc": "Brisanje ovih {{objectLength}} praćenih objekata uklanja snimku, bilo koje sačuvane ugradnje, i sve povezane uloge objekata. Snimljeni materijal ovih praćenih objekata u pogledu Historija NEĆE biti obrisan. Sigurni ste da želite nastaviti? Zadržite tipku Shift da biste preskočili ovaj dijalog u budućnosti.",
+ "toast": {
+ "success": "Praćeni objekti uspješno obrisani.",
+ "error": "Neuspješno brisanje praćenih objekata: {{errorMessage}}"
+ }
+ },
+ "zoneMask": {
+ "filterBy": "Filtriraj po maski zone"
+ },
+ "recognizedLicensePlates": {
+ "title": "Prepoznate tablice",
+ "loadFailed": "Neuspješno učitavanje prepoznatih tablica.",
+ "loading": "Učitavanje prepoznatih tablica…",
+ "placeholder": "Unesite za pretragu tablica…",
+ "noLicensePlatesFound": "Nema pronađenih tablica.",
+ "selectPlatesFromList": "Odaberite jednu ili više tablica iz liste.",
+ "selectAll": "Odaberite sve",
+ "clearAll": "Očistite sve"
+ }
+}
diff --git a/web/public/locales/bs/components/icons.json b/web/public/locales/bs/components/icons.json
new file mode 100644
index 0000000000..807c1d29f6
--- /dev/null
+++ b/web/public/locales/bs/components/icons.json
@@ -0,0 +1,8 @@
+{
+ "iconPicker": {
+ "selectIcon": "Odaberite ikonu",
+ "search": {
+ "placeholder": "Pretražite ikonu…"
+ }
+ }
+}
diff --git a/web/public/locales/bs/components/input.json b/web/public/locales/bs/components/input.json
new file mode 100644
index 0000000000..03e33fb6d5
--- /dev/null
+++ b/web/public/locales/bs/components/input.json
@@ -0,0 +1,10 @@
+{
+ "button": {
+ "downloadVideo": {
+ "label": "Preuzimanje videa",
+ "toast": {
+ "success": "Vaš video stavke pregleda je započelo preuzimanje."
+ }
+ }
+ }
+}
diff --git a/web/public/locales/bs/components/player.json b/web/public/locales/bs/components/player.json
new file mode 100644
index 0000000000..3056608f08
--- /dev/null
+++ b/web/public/locales/bs/components/player.json
@@ -0,0 +1,52 @@
+{
+ "noRecordingsFoundForThisTime": "Nisu pronađeni snimci za ovo vrijeme",
+ "noPreviewFound": "Nije pronađen pregled",
+ "noPreviewFoundFor": "Nije pronađen pregled za {{cameraName}}",
+ "submitFrigatePlus": {
+ "title": "Pošalji ovaj okvir Frigate+?",
+ "submit": "Pošalji",
+ "previewError": "Nije moguće učitati prikaz snimke. Snimka možda trenutno nije dostupna."
+ },
+ "livePlayerRequiredIOSVersion": "Za ovaj tip uživo prijenosa potreban je iOS 17.1 ili noviji.",
+ "streamOffline": {
+ "title": "Prijenos je offline",
+ "desc": "Nisu primljeni okviri na {{cameraName}} detect prijenos, provjerite zapise o greškama"
+ },
+ "cameraDisabled": "Kamera je onemogućena",
+ "stats": {
+ "streamType": {
+ "title": "Tip prijenosa:",
+ "short": "Tip"
+ },
+ "bandwidth": {
+ "title": "Širina pojasa:",
+ "short": "Širina pojasa"
+ },
+ "latency": {
+ "title": "Kasnjenje:",
+ "value": "{{seconds}} sekundi",
+ "short": {
+ "title": "Kasnjenje",
+ "value": "{{seconds}} sek"
+ }
+ },
+ "totalFrames": "Ukupno okvira:",
+ "droppedFrames": {
+ "title": "Izgubljeni okviri:",
+ "short": {
+ "title": "Izgubljeni",
+ "value": "{{droppedFrames}} okvira"
+ }
+ },
+ "decodedFrames": "Dekodirani okviri:",
+ "droppedFrameRate": "Stopa izgubljenih okvira:"
+ },
+ "toast": {
+ "success": {
+ "submittedFrigatePlus": "Uspješno je poslano okvir Frigate+"
+ },
+ "error": {
+ "submitFrigatePlusFailed": "Neuspješno slanje okvira Frigate+"
+ }
+ }
+}
diff --git a/web/public/locales/bs/config/cameras.json b/web/public/locales/bs/config/cameras.json
new file mode 100644
index 0000000000..97da5a3793
--- /dev/null
+++ b/web/public/locales/bs/config/cameras.json
@@ -0,0 +1,949 @@
+{
+ "label": "KameraKonfig",
+ "zones": {
+ "label": "Zone",
+ "description": "Zona omogućava da definirate specifičnu područje okvira da biste odredili je li objekt unutar određenog područja.",
+ "friendly_name": {
+ "label": "Ime zone",
+ "description": "Korisničko ime za zonu, prikazano u UI Frigate. Ako nije postavljeno, koristi se oblikovana verzija imena zone."
+ },
+ "enabled": {
+ "label": "Omogućeno",
+ "description": "Omogući ili onemogući ovu zonu. Onemogućene zone zanemaruju se tijekom izvršavanja."
+ },
+ "enabled_in_config": {
+ "label": "Zapamti originalno stanje zone."
+ },
+ "filters": {
+ "label": "Filtri zone",
+ "description": "Filtri za primjenu na objekte unutar ove zone. Koriste se za smanjenje lažnih pozitiva ili ograničavanje kojih objekata se smatraju prisutnim u zoni.",
+ "min_area": {
+ "label": "Minimalna površina objekta",
+ "description": "Minimalna površina okvira (pikseli ili postotak) potrebna za ovaj tip objekta. Može biti pikseli (cijeli broj) ili postotak (float između 0.000001 i 0.99)."
+ },
+ "max_area": {
+ "label": "Maksimalna površina objekta",
+ "description": "Maksimalna površina okvira (pikseli ili postotak) dozvoljena za ovaj tip objekta. Može biti pikseli (cijeli broj) ili postotak (float između 0.000001 i 0.99)."
+ },
+ "min_ratio": {
+ "label": "Minimalni omjer visine/širine",
+ "description": "Minimalni omjer širine/visine potreban da bi okvir bio prihvaćen."
+ },
+ "max_ratio": {
+ "label": "Maksimalni omjer visine/širine",
+ "description": "Maksimalni omjer širine/visine dozvoljen da bi okvir bio prihvaćen."
+ },
+ "threshold": {
+ "label": "Prag pouzdanosti",
+ "description": "Prosjek pragova pouzdanosti detekcije potreban da bi objekt bio smatravan pravim pozitivom."
+ },
+ "min_score": {
+ "label": "Minimalna pouzdanost",
+ "description": "Minimalna pouzdanost detekcije po okviru potrebna da bi objekt bio brojan."
+ },
+ "mask": {
+ "label": "Maska filtriranja",
+ "description": "Koordinate poligona koje definiraju područje na kojem se ovaj filter primjenjuje unutar okvira."
+ },
+ "raw_mask": {
+ "label": "Ručna maska"
+ }
+ },
+ "coordinates": {
+ "label": "Koordinate",
+ "description": "Koordinate poligona koje definiraju područje zone. Može biti niz razdvojen zarezom ili lista nizova koordinata. Koordinate trebaju biti relativne (0-1) ili apsolutne (stariji format)."
+ },
+ "distances": {
+ "label": "Stvarne udaljenosti",
+ "description": "Nepovlačni stvarne udaljenosti za svaku stranu kvadrilateralne zone, koristi se za izračun brzine ili udaljenosti. Moraju imati tačno 4 vrijednosti ako su postavljene."
+ },
+ "inertia": {
+ "label": "Okviri inertnosti",
+ "description": "Broj uzastopnih okvira u kojima mora biti detektovan objekt u zoni da bi bio smatravan prisutnim. Pomaže u filtriranju privremenih detekcija."
+ },
+ "loitering_time": {
+ "label": "Sekunde loiteranja",
+ "description": "Broj sekundi koje objekt mora ostati u zoni da bi bio smatravan loiteranjem. Postaviti na 0 za onemogućavanje detekcije loiteranja."
+ },
+ "speed_threshold": {
+ "label": "Minimalna brzina",
+ "description": "Minimalna brzina (u stvarnim jedinicama ako su udaljenosti postavljene) potrebna da bi objekt bio smatravan prisutnim u zoni. Koristi se za zone koje se aktiviraju na osnovu brzine."
+ },
+ "objects": {
+ "label": "Objekti koji izazivaju",
+ "description": "Lista tipova objekata (iz labelmapa) koji mogu izazvati ovu zonu. Može biti niz ili lista nizova. Ako je prazna, svi objekti se uzimaju u obzir."
+ }
+ },
+ "name": {
+ "label": "Ime kamere",
+ "description": "Ime kamere je obavezno"
+ },
+ "friendly_name": {
+ "label": "Prijateljsko ime",
+ "description": "Prijateljsko ime kamere korišteno u korisničkom sučelju Frigate"
+ },
+ "enabled": {
+ "label": "Omogućeno",
+ "description": "Omogućeno"
+ },
+ "audio": {
+ "label": "Audio događaji",
+ "description": "Postavke za detekciju događaja temeljene na audio.",
+ "enabled": {
+ "label": "Omogući detekciju zvuka",
+ "description": "Omogući ili onemogući detekciju događaja temeljenu na audio za ovu kameru."
+ },
+ "max_not_heard": {
+ "label": "Vrijeme trajanja do kraja",
+ "description": "Količina sekundi bez konfiguriranog tipa zvuka prije nego što se audio događaj završi."
+ },
+ "min_volume": {
+ "label": "Minimalna zapremina",
+ "description": "Minimalni prag RMS zapremine potreban za pokretanje detekcije zvuka; niže vrijednosti povećavaju osjetljivost (npr. 200 visoko, 500 srednje, 1000 nisko)."
+ },
+ "listen": {
+ "label": "Tipovi slušanja",
+ "description": "Popis tipova audio događaja za detekciju (npr. zavijanje, požarne zvona, vrisak, govorenje, vikanje)."
+ },
+ "filters": {
+ "label": "Audio filteri",
+ "description": "Postavke filtera po tipu zvuka kao što su pragovi pouzdanosti za smanjenje lažnih pozitiva."
+ },
+ "enabled_in_config": {
+ "label": "Originalno stanje zvuka",
+ "description": "Indikuje je li detekcija zvuka izvorno omogućena u statičkoj konfiguracijskoj datoteci."
+ },
+ "num_threads": {
+ "label": "Dretve detekcije",
+ "description": "Broj dretvi za korištenje za obradu detekcije zvuka."
+ }
+ },
+ "audio_transcription": {
+ "label": "Transkripcija zvuka",
+ "description": "Postavke za transkripciju živog i govornog zvuka korištenih za događaje i žive podnaslove.",
+ "enabled": {
+ "label": "Omogući transkripciju",
+ "description": "Omogući ili onemogući transkripciju audio događaja pokrenutu ručno."
+ },
+ "enabled_in_config": {
+ "label": "Originalni stanje transkripcije"
+ },
+ "live_enabled": {
+ "label": "Uživo transkripcija",
+ "description": "Omogući streaming uživo transkripcije za audio dok se prima."
+ }
+ },
+ "birdseye": {
+ "label": "Birdseye",
+ "description": "Postavke za sastavni prikaz Birdseye koji kombinuje više snimke kamere u jedinstveni raspored.",
+ "enabled": {
+ "label": "Omogući Birdseye",
+ "description": "Omogući ili onemogući funkciju prikaza Birdseye."
+ },
+ "mode": {
+ "label": "Način praćenja",
+ "description": "Način uključivanja kamera u Birdseye: 'objekti', 'kretanje' ili 'kontinuirano'."
+ },
+ "order": {
+ "label": "Pozicija",
+ "description": "Numerička pozicija koja kontroliše redoslijed kamera u rasporedu Birdseye."
+ }
+ },
+ "detect": {
+ "label": "Detekcija objekata",
+ "description": "Postavke za ulogu detekcije/detekcija koja se koristi za pokretanje detekcije objekata i inicijalizaciju praćenja.",
+ "enabled": {
+ "label": "Omogući detekciju objekata",
+ "description": "Omogući ili onemogući detekciju objekata za ovu kameru."
+ },
+ "height": {
+ "label": "Visina detekcije",
+ "description": "Visina (pikseli) okvira korištenih za detekciju stream-a; ostavite prazno za korištenje originalne rezolucije stream-a."
+ },
+ "width": {
+ "label": "Širina detekcije",
+ "description": "Širina (pikseli) okvira korištenih za detekciju stream-a; ostavite prazno za korištenje originalne rezolucije stream-a."
+ },
+ "fps": {
+ "label": "Detekcija FPS",
+ "description": "Željeni broj okvira po sekundi za pokretanje detekcije; niže vrijednosti smanjuju upotrebu CPU-a (preporučena vrijednost je 5, postavite više - najviše 10 - samo ako praćite vrlo brze objekte)."
+ },
+ "min_initialized": {
+ "label": "Minimalni broj okvira inicijalizacije",
+ "description": "Broj uzastopnih detekcija potreban prije stvaranja praćenog objekta. Povećajte da biste smanjili lažne inicijalizacije. Zadana vrijednost je fps podijeljeno sa 2."
+ },
+ "max_disappeared": {
+ "label": "Maksimalni broj okvira koji su nestali",
+ "description": "Broj okvira bez detekcije prije nego što se praćeni objekt smatra izgubljenim."
+ },
+ "stationary": {
+ "label": "Konfiguracija stacionarnih objekata",
+ "description": "Postavke za detekciju i upravljanje objektima koji ostaju stacionarni tokom određenog vremena.",
+ "interval": {
+ "label": "Stacionarni interval",
+ "description": "Kako često (u snimcima) pokretati provjeru detekcije da biste potvrdili stacionarni objekt."
+ },
+ "threshold": {
+ "label": "Stacionarni prag",
+ "description": "Broj snimaka bez promjene pozicije potreban da bi objekt bio označen kao stacionarni."
+ },
+ "max_frames": {
+ "label": "Maksimalni snimci",
+ "description": "Ograničava koliko dugo se stacionarni objekti praćaju prije nego što se odbacuju.",
+ "default": {
+ "label": "Zadani maksimalni snimci",
+ "description": "Zadani maksimalni broj snimaka za praćenje stacionarnog objekta prije prestanka."
+ },
+ "objects": {
+ "label": "Maksimalni snimci po objektu",
+ "description": "Podešavanja po objektu za maksimalni broj snimaka za praćenje stacionarnih objekata."
+ }
+ },
+ "classifier": {
+ "label": "Omogući vizualni klasifikator",
+ "description": "Koristi vizualni klasifikator za detekciju pravozadanih stacionarnih objekata čak i kada se okviri tresu."
+ }
+ },
+ "annotation_offset": {
+ "label": "Pomak oznake",
+ "description": "Milisekunde za pomak detektiranih oznaka kako bi se bolje poravnali vremenski okviri s snimcima; može biti pozitivan ili negativan."
+ }
+ },
+ "face_recognition": {
+ "label": "Prepoznavanje lica",
+ "description": "Postavke za detekciju i prepoznavanje lica za ovu kameru.",
+ "enabled": {
+ "label": "Omogući prepoznavanje lica",
+ "description": "Omogući ili onemogući prepoznavanje lica."
+ },
+ "min_area": {
+ "label": "Minimalna površina lica",
+ "description": "Minimalna površina (pikseli) detektiranog okvira lica potrebna za pokušaj prepoznavanja."
+ }
+ },
+ "ffmpeg": {
+ "label": "FFmpeg",
+ "description": "Postavke FFmpeg uključuju putanju binarne datoteke, argumente, opcije hwaccel i izlazne argumente po ulozi.",
+ "path": {
+ "label": "Putanja do FFmpeg binarne datoteke",
+ "description": "Putanja do FFmpeg binarne datoteke ili verzija alias (\"5.0\" ili \"7.0\")."
+ },
+ "global_args": {
+ "label": "Globalni argumenti FFmpeg-a",
+ "description": "Globalni argumenti prebačeni na procese FFmpeg."
+ },
+ "hwaccel_args": {
+ "label": "Argumenti za ubrzanje hardvera",
+ "description": "Argumenti za ubrzanje hardvera za FFmpeg. Preporučuju se predložci specifični za dobavljača."
+ },
+ "input_args": {
+ "label": "Unos argumenata",
+ "description": "Ulazni argumenti primjenjeni na ulazne snimke FFmpeg."
+ },
+ "output_args": {
+ "label": "Izlazni argumenti",
+ "description": "Zadani izlazni argumenti korišteni za različite uloge FFmpeg-a poput detekcije i snimanja.",
+ "detect": {
+ "label": "Izlazni argumenti za detekciju",
+ "description": "Zadani izlazni argumenti za snimke uloga detekcije."
+ },
+ "record": {
+ "label": "Izlazni argumenti za snimanje",
+ "description": "Zadani izlazni argumenti za snimke uloga snimanja."
+ }
+ },
+ "retry_interval": {
+ "label": "Vrijeme ponovnog pokušaja FFmpeg-a",
+ "description": "Sekunde koje treba čekati prije nego što se pokuša ponovno uspostaviti veza s tokom kamere nakon neuspjeha. Zadano je 10."
+ },
+ "apple_compatibility": {
+ "label": "Kompatibilnost s Apple-om",
+ "description": "Omogući označavanje HEVC za bolju kompatibilnost s igračima Apple-a prilikom snimanja H.265."
+ },
+ "gpu": {
+ "label": "Indeks GPU-a",
+ "description": "Zadani indeks GPU-a korišten za ubrzanje hardvera ako je dostupan."
+ },
+ "inputs": {
+ "label": "Ulazni podaci kamere",
+ "description": "Popis definicija ulaznih tokova (putanje i uloge) za ovu kameru.",
+ "path": {
+ "label": "Putanja ulaza",
+ "description": "URL ili putanja ulaznog toka kamere."
+ },
+ "roles": {
+ "label": "Uloge ulaza",
+ "description": "Uloge za ovaj ulazni tok."
+ },
+ "global_args": {
+ "label": "Globalni argumenti FFmpeg-a",
+ "description": "Globalni argumenti FFmpeg-a za ovaj ulazni tok."
+ },
+ "hwaccel_args": {
+ "label": "Argumenti za ubrzanje hardvera",
+ "description": "Argumenti za ubrzanje hardvera za ovaj ulazni stream."
+ },
+ "input_args": {
+ "label": "Unos argumenata",
+ "description": "Argumeti unosa specifični za ovaj stream."
+ }
+ }
+ },
+ "live": {
+ "label": "Uživo prikaz",
+ "description": "Postavke korištenje Web UI za kontrolu izbora živog streama, rezolucije i kvalitete.",
+ "streams": {
+ "label": "Imena živih streamova",
+ "description": "Mapiranje konfiguriranih imena streamova na imena restream/go2rtc korишtena za uživo prikaz."
+ },
+ "height": {
+ "label": "Visina uživo",
+ "description": "Visina (piksela) za prikaz jsmpeg živog streama u Web UI; mora biti <= visina detektiranog streama."
+ },
+ "quality": {
+ "label": "Kvalitet uživo",
+ "description": "Kvalitet kodiranja za jsmpeg stream (1 najviši, 31 najniži)."
+ }
+ },
+ "lpr": {
+ "label": "Prepoznavanje tablice vozila",
+ "description": "Postavke prepoznavanja tablice vozila uključujući pragovi detekcije, formatiranje i poznate tablice.",
+ "enabled": {
+ "label": "Omogući LPR",
+ "description": "Omogući ili onemogući LPR na ovoj kameri."
+ },
+ "expire_time": {
+ "label": "Sekunde isteka",
+ "description": "Vrijeme u sekundama nakon kojeg nevidljiva tablica istječe iz praćenja (samo za dedikovane LPR kamere)."
+ },
+ "min_area": {
+ "label": "Minimalna površina tablice",
+ "description": "Minimalna površina tablice (piksela) potrebna za pokušaj prepoznavanja."
+ },
+ "enhancement": {
+ "label": "Nivo poboljšanja",
+ "description": "Nivo poboljšanja (0-10) za primjenu na isječke tablice prije OCR-a; veće vrijednosti ne moraju uvijek poboljšati rezultate, nivoi iznad 5 mogu raditi samo s tablicama u noćnom vremenu i trebaju se koristiti s oprezom."
+ }
+ },
+ "motion": {
+ "label": "Detekcija pokreta",
+ "description": "Zadane postavke detekcije pokreta za ovu kameru.",
+ "enabled": {
+ "label": "Omogući detekciju pokreta",
+ "description": "Omogući ili onemogući detekciju pokreta za ovu kameru."
+ },
+ "threshold": {
+ "label": "Prag pokreta",
+ "description": "Prag razlike piksela korišten za detektor pokreta; veće vrijednosti smanjuju osjetljivost (opseg 1-255)."
+ },
+ "lightning_threshold": {
+ "label": "Prag munje",
+ "description": "Prag za detekciju i zanemarivanje kratkih iskri svjetlosti (niže vrijednosti povećavaju osjetljivost, vrijednosti između 0.3 i 1.0). Ovo ne spriječava detekciju pokreta u potpunosti; jednostavno zaustavlja detektor da analizira dodatne okvire nakon što se prag premaši. Snimci temeljeni na pokretima i dalje se stvaraju tijekom ovih događaja."
+ },
+ "skip_motion_threshold": {
+ "label": "Preskoči prag pokreta",
+ "description": "Ako se postavi na vrijednost između 0.0 i 1.0, i ako se više od ovog udjela slike promijeni u jednom okviru, detektor neće vratiti kutije pokreta i odmah će se ponovno kalibrirati. Ovo može uštedjeti CPU i smanjiti lažne pozitive tijekom munje, oluje itd., ali može propustiti stvarne događaje kao što je automatsko praćenje objekta PTZ kamerom. Tržište je između izgube nekoliko megabajta snimaka i pregleda nekoliko kratkih zapisnika. Ostavite nepostavljeno (Nijedno) za onemogućavanje ove funkcije."
+ },
+ "improve_contrast": {
+ "label": "Poboljšaj kontrast",
+ "description": "Primijeni poboljšanje kontrasta na okvire prije analize pokreta kako bi pomoću detekcije."
+ },
+ "contour_area": {
+ "label": "Površina kontura",
+ "description": "Minimalna površina kontura u pikselima potrebna za brojanje kontura pokreta."
+ },
+ "delta_alpha": {
+ "label": "Delta alfa",
+ "description": "Faktor alfa spajanja korišten za razliku okvira za izračun pokreta."
+ },
+ "frame_alpha": {
+ "label": "Alfa okvira",
+ "description": "Vrijednost alfa korištena prilikom spajanja okvira za predobradbu pokreta."
+ },
+ "frame_height": {
+ "label": "Visina okvira",
+ "description": "Visina u pikselima na koju se skaliraju okviri prilikom izračuna pokreta."
+ },
+ "mask": {
+ "label": "Koordinate maska",
+ "description": "Uredno x,y koordinate koje definiraju poligon maska pokreta za uključivanje/isključivanje područja."
+ },
+ "mqtt_off_delay": {
+ "label": "MQTT zakasnjenje isključivanja",
+ "description": "Sekunde koje se čekaju nakon posljednjeg pokreta prije objave MQTT 'isključeno' stanje."
+ },
+ "enabled_in_config": {
+ "label": "Originalno stanje pokreta",
+ "description": "Indikira je li detekcija pokreta bila omogućena u originalnoj statičkoj konfiguraciji."
+ },
+ "raw_mask": {
+ "label": "Ručna maska"
+ }
+ },
+ "objects": {
+ "label": "Objekti",
+ "description": "Zadani parametri praćenja objekata uključujući koje oznake praćenja i filtre po objektu.",
+ "track": {
+ "label": "Objekti za praćenje",
+ "description": "Popis oznaka objekata za praćenje za ovu kameru."
+ },
+ "filters": {
+ "label": "Filtar objekata",
+ "description": "Filtar primijenjen na detektirane objekte kako bi se smanjila broj lažnih pozitiva (površina, omjer, pouzdanost).",
+ "min_area": {
+ "label": "Minimalna površina objekta",
+ "description": "Minimalna površina okvira (pikseli ili postotak) potrebna za ovaj tip objekta. Može biti pikseli (cijeli broj) ili postotak (float između 0.000001 i 0.99)."
+ },
+ "max_area": {
+ "label": "Maksimalna površina objekta",
+ "description": "Maksimalna površina okvira (pikseli ili postotak) dozvoljena za ovaj tip objekta. Može biti pikseli (cijeli broj) ili postotak (float između 0.000001 i 0.99)."
+ },
+ "min_ratio": {
+ "label": "Minimalni omjer visine/širine",
+ "description": "Minimalni omjer širine/visine potreban da bi okvir bio prihvaćen."
+ },
+ "max_ratio": {
+ "label": "Maksimalni omjer visine/širine",
+ "description": "Maksimalni omjer širine/visine dozvoljen da bi okvir bio prihvaćen."
+ },
+ "threshold": {
+ "label": "Prag pouzdanosti",
+ "description": "Prosjek pragova pouzdanosti detekcije potreban da bi objekt bio smatravan pravim pozitivom."
+ },
+ "min_score": {
+ "label": "Minimalna pouzdanost",
+ "description": "Minimalna pouzdanost detekcije po okviru potrebna da bi objekt bio brojan."
+ },
+ "mask": {
+ "label": "Maska filtriranja",
+ "description": "Koordinate poligona koje definiraju područje na kojem se ovaj filter primjenjuje unutar okvira."
+ },
+ "raw_mask": {
+ "label": "Ručna maska"
+ }
+ },
+ "mask": {
+ "label": "Maska objekta",
+ "description": "Poligonalna maska korištena za spriječavanje detekcije objekta u određenim područjima."
+ },
+ "raw_mask": {
+ "label": "Ručna maska"
+ },
+ "genai": {
+ "label": "Konfiguracija GenAI objekta",
+ "description": "Opcije GenAI za opisivanje praćenih objekata i slanje okvira za generisanje.",
+ "enabled": {
+ "label": "Omogući GenAI",
+ "description": "Omogući generisanje opisa za praćene objekte po zadanim postavkama."
+ },
+ "use_snapshot": {
+ "label": "Koristi snimke",
+ "description": "Koristi snimke objekata umjesto miniaturnih slika za generisanje opisa GenAI."
+ },
+ "prompt": {
+ "label": "Naslovni prompt",
+ "description": "Zadani šablon upita korišten za generisanje opisa pomoću GenAI."
+ },
+ "object_prompts": {
+ "label": "Prompti za objekte",
+ "description": "Prompti po objektu za prilagođavanje izlaza GenAI za specifične oznake."
+ },
+ "objects": {
+ "label": "GenAI objekti",
+ "description": "Popis oznaka objekata koje se po defaultu šalju GenAI."
+ },
+ "required_zones": {
+ "label": "Potrebne zone",
+ "description": "Zone koje moraju biti unesene za objekte da bi se kvalifikovali za generisanje opisa GenAI."
+ },
+ "debug_save_thumbnails": {
+ "label": "Sačuvajte miniaturne slike",
+ "description": "Sačuvaj miniaturne slike koje se šalju GenAI za ispravljanje i pregled."
+ },
+ "send_triggers": {
+ "label": "GenAI izazivači",
+ "description": "Definiše kada bi se trebale slati okvir za GenAI (na kraju, nakon ažuriranja, itd.).",
+ "tracked_object_end": {
+ "label": "Pošalji na kraju",
+ "description": "Pošalji zahtjev GenAI kada praćeni objekt završi."
+ },
+ "after_significant_updates": {
+ "label": "Raniji GenAI izazivač",
+ "description": "Pošalji zahtjev GenAI nakon određenog broja značajnih ažuriranja za praćeni objekt."
+ }
+ },
+ "enabled_in_config": {
+ "label": "Originalno stanje GenAI",
+ "description": "Pokazuje je li GenAI bio omogućen u originalnoj statičkoj konfiguraciji."
+ }
+ }
+ },
+ "record": {
+ "label": "Snimanje",
+ "description": "Postavke snimanja i zadržavanja za ovu kameru.",
+ "enabled": {
+ "label": "Omogući snimanje",
+ "description": "Omogući ili onemogući snimanje za ovu kameru."
+ },
+ "expire_interval": {
+ "label": "Interval čišćenja snimanja",
+ "description": "Minute između čišćenja koja uklanjaju istekle segmente snimaka."
+ },
+ "continuous": {
+ "label": "Neprekidna retencija",
+ "description": "Broj dana za čuvanje snimaka bez obzira na praćene objekte ili pokret. Postavite na 0 ako želite da čuvate samo snimke upozorenja i detekcije.",
+ "days": {
+ "label": "Dane zadržavanja",
+ "description": "Dana za čuvanje snimaka."
+ }
+ },
+ "motion": {
+ "label": "Retencija pokreta",
+ "description": "Broj dana za čuvanje snimaka izazvanih pokretom bez obzira na praćene objekte. Postavite na 0 ako želite da čuvate samo snimke upozorenja i detekcije.",
+ "days": {
+ "label": "Dane zadržavanja",
+ "description": "Dana za čuvanje snimaka."
+ }
+ },
+ "detections": {
+ "label": "Retencija detekcije",
+ "description": "Postavke retencije snimaka za događaje detekcije uključujući trajanje pre/post snimanja.",
+ "pre_capture": {
+ "label": "Sekundi pre snimanja",
+ "description": "Broj sekundi prije događaja detekcije koje treba uključiti u snimak."
+ },
+ "post_capture": {
+ "label": "Sekunde nakon snimanja",
+ "description": "Broj sekundi nakon događaja detekcije koje se uključuju u snimanje."
+ },
+ "retain": {
+ "label": "Zadržavanje događaja",
+ "description": "Postavke zadržavanja za snimke događaja detekcije.",
+ "days": {
+ "label": "Dane zadržavanja",
+ "description": "Broj dana za koje se zadržavaju snimke događaja detekcije."
+ },
+ "mode": {
+ "label": "Način zadržavanja",
+ "description": "Način zadržavanja: sve (sačuvati sve segmente), pokret (sačuvati segmente s pokretom), ili aktivni_objekti (sačuvati segmente s aktivnim objektima)."
+ }
+ }
+ },
+ "alerts": {
+ "label": "Retencija upozorenja",
+ "description": "Postavke retencije snimaka za događaje upozorenja uključujući trajanje pre/post snimanja.",
+ "pre_capture": {
+ "label": "Sekundi pre snimanja",
+ "description": "Broj sekundi prije događaja detekcije koje treba uključiti u snimak."
+ },
+ "post_capture": {
+ "label": "Sekunde nakon snimanja",
+ "description": "Broj sekundi nakon događaja detekcije koje se uključuju u snimanje."
+ },
+ "retain": {
+ "label": "Zadržavanje događaja",
+ "description": "Postavke zadržavanja za snimke događaja detekcije.",
+ "days": {
+ "label": "Dane zadržavanja",
+ "description": "Broj dana za koje se zadržavaju snimke događaja detekcije."
+ },
+ "mode": {
+ "label": "Način zadržavanja",
+ "description": "Način zadržavanja: sve (sačuvati sve segmente), pokret (sačuvati segmente s pokretom), ili aktivni_objekti (sačuvati segmente s aktivnim objektima)."
+ }
+ }
+ },
+ "export": {
+ "label": "Konfiguracija izvoza",
+ "description": "Postavke koje se koriste prilikom izvoza snimaka kao što su timelapse i ubrzavanje dretve.",
+ "hwaccel_args": {
+ "label": "Argumeti ubrzavanja dretve za izvoz",
+ "description": "Argumeti ubrzavanja dretve za operacije izvoza/prenosa."
+ },
+ "max_concurrent": {
+ "label": "Maksimalan broj istovremenih izvoza",
+ "description": "Maksimalan broj poslova izvoza koji se obrađuju istovremeno."
+ }
+ },
+ "preview": {
+ "label": "Konfiguracija pregleda",
+ "description": "Postavke koje kontrolišu kvalitet pregleda snimanja prikazanih u UI.",
+ "quality": {
+ "label": "Kvaliteta pregleda",
+ "description": "Nivo kvalitete pregleda (vrlo_nizak, nizak, srednji, visok, vrlo_visok)."
+ }
+ },
+ "enabled_in_config": {
+ "label": "Originalno stanje snimanja",
+ "description": "Pokazuje je li snimanje bilo omogućeno u originalnoj statičkoj konfiguraciji."
+ }
+ },
+ "review": {
+ "label": "Pregled",
+ "description": "Postavke koje kontrolišu upozorenja, detekcije i sažetke pregleda GenAI korišteni od strane UI i skladišta za ovu kameru.",
+ "alerts": {
+ "label": "Konfiguracija upozorenja",
+ "description": "Postavke za koje objekti praćeni generišu upozorenja i kako se upozorenja zadržavaju.",
+ "enabled": {
+ "label": "Omogući upozorenja",
+ "description": "Omogući ili onemogući generisanje upozorenja za ovu kameru."
+ },
+ "labels": {
+ "label": "Oznake upozorenja",
+ "description": "Lista oznaka objekata koje se smatraju upozorenjima (npr. automobil, osoba)."
+ },
+ "required_zones": {
+ "label": "Potrebne zone",
+ "description": "Zone koje objekt mora ući da bi se smatrao upozorenjem; ostavite prazno da omogućite bilo koju zonu."
+ },
+ "enabled_in_config": {
+ "label": "Originalno stanje upozorenja",
+ "description": "Pratiti je li upozorenja izvorno omogućena u statičkoj konfiguraciji."
+ },
+ "cutoff_time": {
+ "label": "Vrijeme prekida upozorenja",
+ "description": "Sekunde koje treba čekati nakon što nema aktivnosti koja uzrokuje upozorenje prije nego se prekine upozorenje."
+ }
+ },
+ "detections": {
+ "label": "Konfiguracija detekcija",
+ "description": "Postavke koje objekti koje se praćenje generišu detekcije (nepozornja) i kako se detekcije čuvaju.",
+ "enabled": {
+ "label": "Omogući detekcije",
+ "description": "Omogući ili onemogući događaje detekcije za ovu kameru."
+ },
+ "labels": {
+ "label": "Oznake detekcije",
+ "description": "Popis oznaka objekata koje kvalifikuju kao događaji detekcije."
+ },
+ "required_zones": {
+ "label": "Potrebne zone",
+ "description": "Zone koje objekt mora ući da bi se smatrao detekcijom; ostavite prazno da omogućite bilo koju zonu."
+ },
+ "cutoff_time": {
+ "label": "Vrijeme prekida detekcija",
+ "description": "Sekunde koje treba čekati nakon što nema aktivnosti koja uzrokuje detekciju prije nego se prekine detekcija."
+ },
+ "enabled_in_config": {
+ "label": "Originalno stanje detekcija",
+ "description": "Pratiti je li detekcije izvorno omogućene u statičkoj konfiguraciji."
+ }
+ },
+ "genai": {
+ "label": "Konfiguracija GenAI",
+ "description": "Kontrolira korištenje generativne AI za proizvodnju opisa i sažetaka stavki za pregled.",
+ "enabled": {
+ "label": "Omogući opise GenAI",
+ "description": "Omogući ili onemogući opise i sažetke generirane GenAI za stavke za pregled."
+ },
+ "alerts": {
+ "label": "Omogući GenAI za upozorenja",
+ "description": "Koristi GenAI za generiranje opisa stavki upozorenja."
+ },
+ "detections": {
+ "label": "Omogući GenAI za detekcije",
+ "description": "Koristite GenAI za generiranje opisa predmeta detekcije."
+ },
+ "image_source": {
+ "label": "Pregledajte izvor slike",
+ "description": "Izvor slika poslatih GenAIJ-u ('preview' ili 'recordings'); 'recordings' koristi kvalitetnije okvire, ali više tokena."
+ },
+ "additional_concerns": {
+ "label": "Dodatne brige",
+ "description": "Popis dodatnih briga ili napomena koje GenAI treba uzeti u obzir prilikom procjene aktivnosti na ovoj kameri."
+ },
+ "debug_save_thumbnails": {
+ "label": "Sačuvajte miniaturne slike",
+ "description": "Sačuvajte miniaturne slike koje se šalju GenAI provajderu za ispravljanje grešaka i pregled."
+ },
+ "enabled_in_config": {
+ "label": "Originalno stanje GenAI",
+ "description": "Pratiti je li pregled GenAI izvorno omogućen u statičkoj konfiguraciji."
+ },
+ "preferred_language": {
+ "label": "Preferirani jezik",
+ "description": "Preferirani jezik za zahtijevanje od GenAI provajdera za generirane odgovore."
+ },
+ "activity_context_prompt": {
+ "label": "Prompt konteksta aktivnosti",
+ "description": "Prilagođeni prompt koji opisuje što je i što nije sumnjivo ponašanje kako bi pružio kontekst za sažetke GenAI."
+ }
+ }
+ },
+ "semantic_search": {
+ "label": "Semantička pretraga",
+ "description": "Postavke za semantičku pretragu koja konstruira i upita uključivanje objekata kako bi pronašla slične stavke.",
+ "triggers": {
+ "label": "Pokretači",
+ "description": "Akcije i kriteriji za usklađivanje za pokretače semantičke pretrage specifične za kameru.",
+ "friendly_name": {
+ "label": "Prijateljsko ime",
+ "description": "Nepovlačno prijateljsko ime prikazano u korisničkom sučelju za ovaj pokretač."
+ },
+ "enabled": {
+ "label": "Omogući ovaj pokretač",
+ "description": "Omogući ili onemogući ovaj pokretač semantičke pretrage."
+ },
+ "type": {
+ "label": "Tip pokretača",
+ "description": "Tip pokretača: 'thumbnail' (uspoređivanje slikom) ili 'description' (uspoređivanje teksta)."
+ },
+ "data": {
+ "label": "Sadržaj pokretača",
+ "description": "Tekstualni izraz ili ID miniaturne slike za uspoređivanje s praćenim objektima."
+ },
+ "threshold": {
+ "label": "Prag aktivacije",
+ "description": "Minimalna ocjena sličnosti (0-1) potrebna za aktivaciju ovog izazivača."
+ },
+ "actions": {
+ "label": "Akcije izazivača",
+ "description": "Popis akcija koje se izvršavaju kada izazivač odgovara (obavijest, pod_naziv, atribute)."
+ }
+ }
+ },
+ "snapshots": {
+ "label": "Snimci",
+ "description": "Postavke za snimke generirane preko API-ja za praćene objekte za ovu kameru.",
+ "enabled": {
+ "label": "Omogući snimke",
+ "description": "Omogući ili onemogući snimanje snimaka za ovu kameru."
+ },
+ "timestamp": {
+ "label": "Preklapanje vremenske oznake",
+ "description": "Preklopiti vremensku oznaku na snimke iz API-ja."
+ },
+ "bounding_box": {
+ "label": "Preklapanje okvira",
+ "description": "Crtanje okvira za praćene objekte na snimke iz API-ja."
+ },
+ "crop": {
+ "label": "Izrezivanje snimke",
+ "description": "Izrezivanje snimki iz API-ja do okvira detektiranog objekta."
+ },
+ "required_zones": {
+ "label": "Potrebne zone",
+ "description": "Zone koje objekt mora ući da bi snimka bila sačuvana."
+ },
+ "height": {
+ "label": "Visina snimke",
+ "description": "Visina (pikseli) za promjenu veličine snimki iz API-ja; ostavite prazno da biste sačuvali originalnu veličinu."
+ },
+ "retain": {
+ "label": "Zadržavanje snimki",
+ "description": "Postavke zadržavanja snimki uključujući zadane dane i prekriženja po objektu.",
+ "default": {
+ "label": "Zadano zadržavanje",
+ "description": "Zadani broj dana za zadržavanje snimki."
+ },
+ "mode": {
+ "label": "Način zadržavanja",
+ "description": "Način zadržavanja: sve (sačuvati sve segmente), pokret (sačuvati segmente s pokretom), ili aktivni_objekti (sačuvati segmente s aktivnim objektima)."
+ },
+ "objects": {
+ "label": "Zadržavanje objekata",
+ "description": "Prekriženja po objektu za dane zadržavanja snimki."
+ }
+ },
+ "quality": {
+ "label": "Kvaliteta snimka",
+ "description": "Kvaliteta kodiranja za sačuvane snimke (0-100)."
+ }
+ },
+ "timestamp_style": {
+ "label": "Stil vremenske oznake",
+ "description": "Opcije stilizacije za vremenske oznake u snimcima i snimcima.",
+ "position": {
+ "label": "Pozicija vremenske oznake",
+ "description": "Pozicija vremenske oznake na slici (tl/tr/bl/br)."
+ },
+ "format": {
+ "label": "Format vremenske oznake",
+ "description": "String formata datuma i vremena korišten za vremenske oznake (Python format koda za datum i vrijeme)."
+ },
+ "color": {
+ "label": "Boja vremenske oznake",
+ "description": "RGB vrijednosti boja za tekst vremenske oznake (sve vrijednosti 0-255).",
+ "red": {
+ "label": "Crvena",
+ "description": "Crveni komponent (0-255) za boju vremenske oznake."
+ },
+ "green": {
+ "label": "Zelena",
+ "description": "Zeleni komponent (0-255) za boju vremenske oznake."
+ },
+ "blue": {
+ "label": "Plava",
+ "description": "Plavi komponent (0-255) za boju vremenske oznake."
+ }
+ },
+ "thickness": {
+ "label": "Debljina vremenske oznake",
+ "description": "Debljina linije teksta vremenske oznake."
+ },
+ "effect": {
+ "label": "Efekt vremenske oznake",
+ "description": "Vizualni efekt za tekst vremenske oznake (none, solid, shadow)."
+ }
+ },
+ "best_image_timeout": {
+ "label": "Vrijeme čekanja za najbolju sliku",
+ "description": "Koliko dugo čekati na sliku s najvišim stupnjem pouzdanosti."
+ },
+ "mqtt": {
+ "label": "MQTT",
+ "description": "Postavke objave slika preko MQTT.",
+ "enabled": {
+ "label": "Pošalji sliku",
+ "description": "Omogući objavljivanje snimaka slika za objekte na MQTT teme za ovu kameru."
+ },
+ "timestamp": {
+ "label": "Dodaj vremensku oznaku",
+ "description": "Preklopiti vremensku oznaku na slike objavljene preko MQTT."
+ },
+ "bounding_box": {
+ "label": "Dodaj okvir",
+ "description": "Crtaj okvire na slikama objavljenim preko MQTT."
+ },
+ "crop": {
+ "label": "Iscijepi sliku",
+ "description": "Iscijepi slike objavljene preko MQTT na okvir detektiranog objekta."
+ },
+ "height": {
+ "label": "Visina slike",
+ "description": "Visina (piksela) za promjenu veličine slika objavljenih preko MQTT."
+ },
+ "required_zones": {
+ "label": "Potrebne zone",
+ "description": "Zone koje objekt mora ući da bi se slika preko MQTT objavila."
+ },
+ "quality": {
+ "label": "Kvaliteta JPEG",
+ "description": "Kvaliteta JPEG za slike objavljene preko MQTT (0-100)."
+ }
+ },
+ "notifications": {
+ "label": "Obavještenja",
+ "description": "Postavke za omogućavanje i kontrolu obavijesti za ovu kameru.",
+ "enabled": {
+ "label": "Omogući obavijesti",
+ "description": "Omogući ili onemogući obavijesti za ovu kameru."
+ },
+ "email": {
+ "label": "E-mail za obavijesti",
+ "description": "Adresa e-maila koja se koristi za obavijesti putem push-a ili je potrebna određenim dobavljačima obavijesti."
+ },
+ "cooldown": {
+ "label": "Period hlađenja",
+ "description": "Period hlađenja (sekunde) između obavijesti kako bi se izbjeglo spaming primateljima."
+ },
+ "enabled_in_config": {
+ "label": "Originalno stanje obavijesti",
+ "description": "Pokazuje je li obavijesti bile omogućene u originalnoj statičkoj konfiguraciji."
+ }
+ },
+ "onvif": {
+ "label": "ONVIF",
+ "description": "Postavke povezivanja preko ONVIF i automatskog praćenja PTZ za ovu kameru.",
+ "host": {
+ "label": "Gost ONVIF",
+ "description": "Gost (i opcionalni shema) za uslugu ONVIF za ovu kameru."
+ },
+ "port": {
+ "label": "Port ONVIF",
+ "description": "Broj porta za uslugu ONVIF."
+ },
+ "user": {
+ "label": "Korisničko ime za ONVIF",
+ "description": "Korisničko ime za autentifikaciju ONVIF; neki uređaji zahtijevaju korisnika admin za ONVIF."
+ },
+ "password": {
+ "label": "Lozinka za ONVIF",
+ "description": "Lozinka za autentifikaciju ONVIF."
+ },
+ "tls_insecure": {
+ "label": "Onemogući provjeru TLS",
+ "description": "Preskoči provjeru TLS i onemogući digest autentifikaciju za ONVIF (nebezbedno; koristiti samo u sigurnim mrežama)."
+ },
+ "profile": {
+ "label": "ONVIF profil",
+ "description": "Specifičan ONVIF medij profil za korištenje za kontrolu PTZ, prilagođen tokenom ili imenom. Ako nije postavljen, prvi profil s važećom konfiguracijom PTZ automatski se odabire."
+ },
+ "autotracking": {
+ "label": "Autotračenje",
+ "description": "Automatski praćenje pokretanja objekata i držanje ih u sredini okvira korištenjem pokreta kamere PTZ.",
+ "enabled": {
+ "label": "Omogući automatsko praćenje",
+ "description": "Omogući ili onemogući automatsko praćenje kamere PTZ detektiranih objekata."
+ },
+ "calibrate_on_startup": {
+ "label": "Kalibriraj na početku",
+ "description": "Mjeri brzine motora PTZ pri pokretanju kako bi poboljšao preciznost praćenja. Frigate će ažurirati konfiguraciju s težinama pokreta nakon kalibracije."
+ },
+ "zooming": {
+ "label": "Režim zumiranja",
+ "description": "Kontrola ponašanja zumiranja: onemogućeno (samo pan/tilt), apsolutno (najkompatibilnije) ili relativno (konkurentno pan/tilt/zum)."
+ },
+ "zoom_factor": {
+ "label": "Faktor zumiranja",
+ "description": "Kontrola razine zumiranja na praćenim objektima. Niže vrijednosti drže više scene u pogledu; više vrijednosti zumiraju bliže, ali mogu izgubiti praćenje. Vrijednosti između 0.1 i 0.75."
+ },
+ "track": {
+ "label": "Praćeni objekti",
+ "description": "Popis vrsta objekata koji trebaju pokrenuti automatsko praćenje."
+ },
+ "required_zones": {
+ "label": "Potrebne zone",
+ "description": "Objekti moraju ući u jednu od ovih zona prije nego što započne automatsko praćenje."
+ },
+ "return_preset": {
+ "label": "Povratak na predpostavku",
+ "description": "Ime predpostavke konfigurirano u firmware kamere za povratak nakon završetka praćenja."
+ },
+ "timeout": {
+ "label": "Vrijeme čekanja povratka",
+ "description": "Čekajte ovaj broj sekundi nakon gubitka praćenja prije povratka kamere na predpostavljeno mjesto."
+ },
+ "movement_weights": {
+ "label": "Težine pokreta",
+ "description": "Vrijednosti kalibracije automatski generirane kroz kalibraciju kamere. Ne mijenjajte ručno."
+ },
+ "enabled_in_config": {
+ "label": "Originalni stanje autotračenja",
+ "description": "Unutarnje polje za praćenje je li autotračenje bilo omogućeno u konfiguraciji."
+ }
+ },
+ "ignore_time_mismatch": {
+ "label": "Zanemari razliku u vremenu",
+ "description": "Zanemari razlike u sinhronizaciji vremena između kamere i Frigate servera za komunikaciju ONVIF."
+ }
+ },
+ "type": {
+ "label": "Tip kamere",
+ "description": "Tip kamere"
+ },
+ "ui": {
+ "label": "Korisnički interfejs kamere",
+ "description": "Prikaz redoslijeda i vidljivosti za ovu kameru u UI. Redoslijed utječe na zadani nadzorno pločo. Za detaljniju kontrolu koristite grupe kamere.",
+ "order": {
+ "label": "Redoslijed UI",
+ "description": "Numerički redoslijed koristi se za sortiranje kamere u UI (zadani nadzorno pločo i popisi); veći brojevi pojavljuju se kasnije."
+ },
+ "dashboard": {
+ "label": "Prikaži u UI",
+ "description": "Prekidač je li ova kamera vidljiva svuda u UI Frigate. Onemogućavanje ovoga zahtijeva ručno uređivanje konfiguracije za ponovno prikazivanje ove kamere u UI."
+ }
+ },
+ "webui_url": {
+ "label": "URL kamere",
+ "description": "URL za pristup kamere izravno iz stranice sustava"
+ },
+ "profiles": {
+ "label": "Profili",
+ "description": "Imenovane konfiguracijske profile s parcijalnim preklopima koji se mogu aktivirati tijekom izvršavanja."
+ },
+ "enabled_in_config": {
+ "label": "Originalno stanje kamere",
+ "description": "Pratite originalno stanje kamere."
+ }
+}
diff --git a/web/public/locales/bs/config/global.json b/web/public/locales/bs/config/global.json
new file mode 100644
index 0000000000..84e370a6cc
--- /dev/null
+++ b/web/public/locales/bs/config/global.json
@@ -0,0 +1,1596 @@
+{
+ "version": {
+ "label": "Trenutna verzija konfiguracije",
+ "description": "Numerička ili string verzija aktivne konfiguracije za pomoć pri otkrivanju migracija ili promjena formata."
+ },
+ "audio": {
+ "label": "Audio događaji",
+ "enabled": {
+ "label": "Omogući detekciju zvuka",
+ "description": "Omogući ili onemogući detekciju zvučnih događaja za sve kamere; mogu se prekrivati po kameri."
+ },
+ "max_not_heard": {
+ "label": "Vrijeme trajanja do kraja",
+ "description": "Količina sekundi bez konfiguriranog tipa zvuka prije nego što se audio događaj završi."
+ },
+ "min_volume": {
+ "label": "Minimalna zapremina",
+ "description": "Minimalni prag RMS zapremine potreban za pokretanje detekcije zvuka; niže vrijednosti povećavaju osjetljivost (npr. 200 visoko, 500 srednje, 1000 nisko)."
+ },
+ "listen": {
+ "label": "Tipovi slušanja",
+ "description": "Popis tipova audio događaja za detekciju (npr. zavijanje, požarne zvona, vrisak, govorenje, vikanje)."
+ },
+ "filters": {
+ "label": "Audio filteri",
+ "description": "Postavke filtera po tipu zvuka kao što su pragovi pouzdanosti za smanjenje lažnih pozitiva."
+ },
+ "enabled_in_config": {
+ "label": "Originalno stanje zvuka",
+ "description": "Indikuje je li detekcija zvuka izvorno omogućena u statičkoj konfiguracijskoj datoteci."
+ },
+ "num_threads": {
+ "label": "Dretve detekcije",
+ "description": "Broj dretvi za korištenje za obradu detekcije zvuka."
+ },
+ "description": "Postavke za detekciju događaja na osnovu zvuka za sve kamere; mogu se prekrivati po kameri."
+ },
+ "audio_transcription": {
+ "label": "Transkripcija zvuka",
+ "description": "Postavke za transkripciju živog i govornog zvuka korištenih za događaje i žive podnaslove.",
+ "live_enabled": {
+ "label": "Uživo transkripcija",
+ "description": "Omogući streaming uživo transkripcije za audio dok se prima."
+ },
+ "enabled": {
+ "label": "Omogući transkripciju zvuka",
+ "description": "Omogući ili onemogući automatsku transkripciju zvuka za sve kamere; može se prekrimiti po kamere."
+ },
+ "language": {
+ "label": "Jezik za transkripciju",
+ "description": "Kod jezika korišten za transkripciju/prevod (npr. 'en' za engleski). Pogledajte https://whisper-api.com/docs/languages/ za podržane kodove jezika."
+ },
+ "device": {
+ "label": "Uređaj za transkripciju",
+ "description": "Ključ uređaja (CPU/GPU) za izvršavanje modela transkripcije. Trenutno se podržavaju samo NVIDIA CUDA GPU-ovi za transkripciju."
+ },
+ "model_size": {
+ "label": "Veličina modela",
+ "description": "Veličina modela za korištenje za offline transkripciju zvučnih događaja."
+ }
+ },
+ "birdseye": {
+ "label": "Birdseye",
+ "description": "Postavke za sastavni prikaz Birdseye koji kombinuje više snimke kamere u jedinstveni raspored.",
+ "enabled": {
+ "label": "Omogući Birdseye",
+ "description": "Omogući ili onemogući funkciju prikaza Birdseye."
+ },
+ "mode": {
+ "label": "Način praćenja",
+ "description": "Način uključivanja kamera u Birdseye: 'objekti', 'kretanje' ili 'kontinuirano'."
+ },
+ "order": {
+ "label": "Pozicija",
+ "description": "Numerička pozicija koja kontroliše redoslijed kamera u rasporedu Birdseye."
+ },
+ "restream": {
+ "label": "Ponovno prenos RTSP",
+ "description": "Ponovno prenos izlaza Birdseye kao RTSP tok; uključivanje ovoga će održavati Birdseye u neprekidnom radu."
+ },
+ "width": {
+ "label": "Širina",
+ "description": "Širina izlaza (piksela) sastavljenog okvira Birdseye."
+ },
+ "height": {
+ "label": "Visina",
+ "description": "Visina izlaza (piksela) sastavljenog okvira Birdseye."
+ },
+ "quality": {
+ "label": "Kvalitet kodiranja",
+ "description": "Kvalitet kodiranja za Birdseye mpeg1 tok (1 najviši kvalitet, 31 najniži)."
+ },
+ "inactivity_threshold": {
+ "label": "Prag neaktivnosti",
+ "description": "Sekunde neaktivnosti nakon kojih će kamera prestati da se prikazuje u Birdseye."
+ },
+ "layout": {
+ "label": "Razmještaj",
+ "description": "Opcije razmještaja za sastavljanje Birdseye.",
+ "scaling_factor": {
+ "label": "Faktor skaliranja",
+ "description": "Faktor skaliranja korišten od strane računala za razmještaj (opseg 1.0 do 5.0)."
+ },
+ "max_cameras": {
+ "label": "Maksimalan broj kamera",
+ "description": "Maksimalan broj kamera koje se mogu prikazati istovremeno u Birdseye; prikazuje najnovije kamere."
+ }
+ },
+ "idle_heartbeat_fps": {
+ "label": "Neaktivno srčanog udaraca FPS",
+ "description": "Broj okvira po sekundi za ponovno slanje posljednjeg sastavljenog Birdseye okvira kada je neaktivno; postavite na 0 za onemogućavanje."
+ }
+ },
+ "detect": {
+ "label": "Detekcija objekata",
+ "description": "Postavke za ulogu detekcije/detekcija koja se koristi za pokretanje detekcije objekata i inicijalizaciju praćenja.",
+ "enabled": {
+ "label": "Omogući detekciju objekata",
+ "description": "Omogući ili onemogući detekciju objekata za sve kamere; može se prekrimiti po kamere."
+ },
+ "height": {
+ "label": "Visina detekcije",
+ "description": "Visina (pikseli) okvira korištenih za detekciju stream-a; ostavite prazno za korištenje originalne rezolucije stream-a."
+ },
+ "width": {
+ "label": "Širina detekcije",
+ "description": "Širina (pikseli) okvira korištenih za detekciju stream-a; ostavite prazno za korištenje originalne rezolucije stream-a."
+ },
+ "fps": {
+ "label": "Detekcija FPS",
+ "description": "Željeni broj okvira po sekundi za pokretanje detekcije; niže vrijednosti smanjuju upotrebu CPU-a (preporučena vrijednost je 5, postavite više - najviše 10 - samo ako praćite vrlo brze objekte)."
+ },
+ "min_initialized": {
+ "label": "Minimalni broj okvira inicijalizacije",
+ "description": "Broj uzastopnih detekcija potreban prije stvaranja praćenog objekta. Povećajte da biste smanjili lažne inicijalizacije. Zadana vrijednost je fps podijeljeno sa 2."
+ },
+ "max_disappeared": {
+ "label": "Maksimalni broj okvira koji su nestali",
+ "description": "Broj okvira bez detekcije prije nego što se praćeni objekt smatra izgubljenim."
+ },
+ "stationary": {
+ "label": "Konfiguracija stacionarnih objekata",
+ "description": "Postavke za detekciju i upravljanje objektima koji ostaju stacionarni tokom određenog vremena.",
+ "interval": {
+ "label": "Stacionarni interval",
+ "description": "Kako često (u snimcima) pokretati provjeru detekcije da biste potvrdili stacionarni objekt."
+ },
+ "threshold": {
+ "label": "Stacionarni prag",
+ "description": "Broj snimaka bez promjene pozicije potreban da bi objekt bio označen kao stacionarni."
+ },
+ "max_frames": {
+ "label": "Maksimalni snimci",
+ "description": "Ograničava koliko dugo se stacionarni objekti praćaju prije nego što se odbacuju.",
+ "default": {
+ "label": "Zadani maksimalni snimci",
+ "description": "Zadani maksimalni broj snimaka za praćenje stacionarnog objekta prije prestanka."
+ },
+ "objects": {
+ "label": "Maksimalni snimci po objektu",
+ "description": "Podešavanja po objektu za maksimalni broj snimaka za praćenje stacionarnih objekata."
+ }
+ },
+ "classifier": {
+ "label": "Omogući vizualni klasifikator",
+ "description": "Koristi vizualni klasifikator za detekciju pravozadanih stacionarnih objekata čak i kada se okviri tresu."
+ }
+ },
+ "annotation_offset": {
+ "label": "Pomak oznake",
+ "description": "Milisekunde za pomak detektiranih oznaka kako bi se bolje poravnali vremenski okviri s snimcima; može biti pozitivan ili negativan."
+ }
+ },
+ "face_recognition": {
+ "label": "Prepoznavanje lica",
+ "enabled": {
+ "label": "Omogući prepoznavanje lica",
+ "description": "Omogući ili onemogući prepoznavanje lica za sve kamere; mogu se preklopiti po kameri."
+ },
+ "min_area": {
+ "label": "Minimalna površina lica",
+ "description": "Minimalna površina (pikseli) detektiranog okvira lica potrebna za pokušaj prepoznavanja."
+ },
+ "description": "Postavke za detekciju i prepoznavanje lica za sve kamere; mogu se preklopiti po kameri.",
+ "model_size": {
+ "label": "Veličina modela",
+ "description": "Veličina modela za korištenje za ugradnje lica (small/large); veće može zahtijevati GPU."
+ },
+ "unknown_score": {
+ "label": "Prag neznatnog rezultata",
+ "description": "Prag udaljenosti ispod kojeg se lice smatra potencijalnim odgovarajućim (viši = stroži)."
+ },
+ "detection_threshold": {
+ "label": "Prag detekcije",
+ "description": "Minimalni prag pouzdanosti potreban za razmatranje detekcije lica kao važeće."
+ },
+ "recognition_threshold": {
+ "label": "Prag prepoznavanja",
+ "description": "Prag udaljenosti ugradnje lica za razmatranje dva lica kao odgovarajuća."
+ },
+ "min_faces": {
+ "label": "Minimalan broj lica",
+ "description": "Minimalan broj prepoznavanja lica potreban prije nego što se primijeni prepoznati podnaziv za osobu."
+ },
+ "save_attempts": {
+ "label": "Pokušaji sačuvanja",
+ "description": "Broj pokušaja prepoznavanja lica koje se treba sačuvati za korisnički sučelje najnovijih prepoznavanja."
+ },
+ "blur_confidence_filter": {
+ "label": "Filter pouzdanosti za zamagljenost",
+ "description": "Prilagodite ocjene pouzdanosti na temelju zamagljenosti slike kako biste smanjili lažne pozitive za loše kvalitete lica."
+ },
+ "device": {
+ "label": "Uređaj",
+ "description": "Ovo je prekršaj, da biste ciljali specifičan uređaj. Pogledajte https://onnxruntime.ai/docs/execution-providers/ za više informacija"
+ }
+ },
+ "ffmpeg": {
+ "label": "FFmpeg",
+ "description": "Postavke FFmpeg uključuju putanju binarne datoteke, argumente, opcije hwaccel i izlazne argumente po ulozi.",
+ "path": {
+ "label": "Putanja do FFmpeg binarne datoteke",
+ "description": "Putanja do FFmpeg binarne datoteke ili verzija alias (\"5.0\" ili \"7.0\")."
+ },
+ "global_args": {
+ "label": "Globalni argumenti FFmpeg-a",
+ "description": "Globalni argumenti prebačeni na procese FFmpeg."
+ },
+ "hwaccel_args": {
+ "label": "Argumenti za ubrzanje hardvera",
+ "description": "Argumenti za ubrzanje hardvera za FFmpeg. Preporučuju se predložci specifični za dobavljača."
+ },
+ "input_args": {
+ "label": "Unos argumenata",
+ "description": "Ulazni argumenti primjenjeni na ulazne snimke FFmpeg."
+ },
+ "output_args": {
+ "label": "Izlazni argumenti",
+ "description": "Zadani izlazni argumenti korišteni za različite uloge FFmpeg-a poput detekcije i snimanja.",
+ "detect": {
+ "label": "Izlazni argumenti za detekciju",
+ "description": "Zadani izlazni argumenti za snimke uloga detekcije."
+ },
+ "record": {
+ "label": "Izlazni argumenti za snimanje",
+ "description": "Zadani izlazni argumenti za snimke uloga snimanja."
+ }
+ },
+ "retry_interval": {
+ "label": "Vrijeme ponovnog pokušaja FFmpeg-a",
+ "description": "Sekunde koje treba čekati prije nego što se pokuša ponovno uspostaviti veza s tokom kamere nakon neuspjeha. Zadano je 10."
+ },
+ "apple_compatibility": {
+ "label": "Kompatibilnost s Apple-om",
+ "description": "Omogući označavanje HEVC za bolju kompatibilnost s igračima Apple-a prilikom snimanja H.265."
+ },
+ "gpu": {
+ "label": "Indeks GPU-a",
+ "description": "Zadani indeks GPU-a korišten za ubrzanje hardvera ako je dostupan."
+ },
+ "inputs": {
+ "label": "Ulazni podaci kamere",
+ "description": "Popis definicija ulaznih tokova (putanje i uloge) za ovu kameru.",
+ "path": {
+ "label": "Putanja ulaza",
+ "description": "URL ili putanja ulaznog toka kamere."
+ },
+ "roles": {
+ "label": "Uloge ulaza",
+ "description": "Uloge za ovaj ulazni tok."
+ },
+ "global_args": {
+ "label": "Globalni argumenti FFmpeg-a",
+ "description": "Globalni argumenti FFmpeg-a za ovaj ulazni tok."
+ },
+ "hwaccel_args": {
+ "label": "Argumenti za ubrzanje hardvera",
+ "description": "Argumenti za ubrzanje hardvera za ovaj ulazni stream."
+ },
+ "input_args": {
+ "label": "Unos argumenata",
+ "description": "Argumeti unosa specifični za ovaj stream."
+ }
+ }
+ },
+ "live": {
+ "label": "Uživo prikaz",
+ "streams": {
+ "label": "Imena živih streamova",
+ "description": "Mapiranje konfiguriranih imena streamova na imena restream/go2rtc korишtena za uživo prikaz."
+ },
+ "height": {
+ "label": "Visina uživo",
+ "description": "Visina (piksela) za prikaz jsmpeg živog streama u Web UI; mora biti <= visina detektiranog streama."
+ },
+ "quality": {
+ "label": "Kvalitet uživo",
+ "description": "Kvalitet kodiranja za jsmpeg stream (1 najviši, 31 najniži)."
+ },
+ "description": "Postavke za kontrolu rezolucije i kvalitete žive struje jsmpeg. Ovo ne utiče na kamere koje koriste go2rtc za živi pregled."
+ },
+ "lpr": {
+ "label": "Prepoznavanje tablice vozila",
+ "description": "Postavke prepoznavanja tablice vozila uključujući pragovi detekcije, formatiranje i poznate tablice.",
+ "enabled": {
+ "label": "Omogući LPR",
+ "description": "Omogući ili onemogući prepoznavanje tablice za sve kamere; može se prekršiti po kamere."
+ },
+ "expire_time": {
+ "label": "Sekunde isteka",
+ "description": "Vrijeme u sekundama nakon kojeg nevidljiva tablica istječe iz praćenja (samo za dedikovane LPR kamere)."
+ },
+ "min_area": {
+ "label": "Minimalna površina tablice",
+ "description": "Minimalna površina tablice (piksela) potrebna za pokušaj prepoznavanja."
+ },
+ "enhancement": {
+ "label": "Nivo poboljšanja",
+ "description": "Nivo poboljšanja (0-10) za primjenu na isječke tablice prije OCR-a; veće vrijednosti ne moraju uvijek poboljšati rezultate, nivoi iznad 5 mogu raditi samo s tablicama u noćnom vremenu i trebaju se koristiti s oprezom."
+ },
+ "model_size": {
+ "label": "Veličina modela",
+ "description": "Veličina modela korištena za detekciju/pretvorbu teksta. Većina korisnika treba koristiti 'small'."
+ },
+ "detection_threshold": {
+ "label": "Prag detekcije",
+ "description": "Prag pouzdanosti detekcije za početak izvršavanja OCR na sumnjivim pločama."
+ },
+ "recognition_threshold": {
+ "label": "Prag prepoznavanja",
+ "description": "Prag pouzdanosti potreban za prepoznati tekst ploče da bi se priložio kao podnaziv."
+ },
+ "min_plate_length": {
+ "label": "Minimalna dužina ploče",
+ "description": "Minimalan broj znakova koje prepoznata ploča mora sadržavati da bi se smatrala važećom."
+ },
+ "format": {
+ "label": "Regex formata ploče",
+ "description": "Nepovlačen regex za provjeru prepoznatih nizova ploča protiv očekivanog formata."
+ },
+ "match_distance": {
+ "label": "Razlika u odgovaranju",
+ "description": "Broj nepravilnih znakova dopuštenih pri uspoređivanju detektiranih ploča s poznatim pločama."
+ },
+ "known_plates": {
+ "label": "Poznate ploče",
+ "description": "Popis ploča ili regexa za posebno praćenje ili upozorenje."
+ },
+ "debug_save_plates": {
+ "label": "Sačuvaj tablice za debagovanje",
+ "description": "Sačuvaj slike izrezaka tablica za debagovanje performansi LPR."
+ },
+ "device": {
+ "label": "Uređaj",
+ "description": "Ovo je preklop za ciljanje specifičnog uređaja. Vidi https://onnxruntime.ai/docs/execution-providers/ za više informacija"
+ },
+ "replace_rules": {
+ "label": "Pravila zamjene",
+ "description": "Pravila zamjene regex korишtena za normalizaciju detektiranih stringova ploča prije uspoređivanja.",
+ "pattern": {
+ "label": "Regex uzorak"
+ },
+ "replacement": {
+ "label": "Zamjenski string"
+ }
+ }
+ },
+ "motion": {
+ "label": "Detekcija pokreta",
+ "enabled": {
+ "label": "Omogući detekciju pokreta",
+ "description": "Omogući ili onemogući detekciju pokreta za sve kamere; može se prekrimiti po kamere."
+ },
+ "threshold": {
+ "label": "Prag pokreta",
+ "description": "Prag razlike piksela korišten za detektor pokreta; veće vrijednosti smanjuju osjetljivost (opseg 1-255)."
+ },
+ "lightning_threshold": {
+ "label": "Prag munje",
+ "description": "Prag za detekciju i zanemarivanje kratkih iskri svjetlosti (niže vrijednosti povećavaju osjetljivost, vrijednosti između 0.3 i 1.0). Ovo ne spriječava detekciju pokreta u potpunosti; jednostavno zaustavlja detektor da analizira dodatne okvire nakon što se prag premaši. Snimci temeljeni na pokretima i dalje se stvaraju tijekom ovih događaja."
+ },
+ "skip_motion_threshold": {
+ "label": "Preskoči prag pokreta",
+ "description": "Ako se postavi na vrijednost između 0.0 i 1.0, i ako se više od ovog udjela slike promijeni u jednom okviru, detektor neće vratiti kutije pokreta i odmah će se ponovno kalibrirati. Ovo može uštedjeti CPU i smanjiti lažne pozitive tijekom munje, oluje itd., ali može propustiti stvarne događaje kao što je automatsko praćenje objekta PTZ kamerom. Tržište je između izgube nekoliko megabajta snimaka i pregleda nekoliko kratkih zapisnika. Ostavite nepostavljeno (Nijedno) za onemogućavanje ove funkcije."
+ },
+ "improve_contrast": {
+ "label": "Poboljšaj kontrast",
+ "description": "Primijeni poboljšanje kontrasta na okvire prije analize pokreta kako bi pomoću detekcije."
+ },
+ "contour_area": {
+ "label": "Površina kontura",
+ "description": "Minimalna površina kontura u pikselima potrebna za brojanje kontura pokreta."
+ },
+ "delta_alpha": {
+ "label": "Delta alfa",
+ "description": "Faktor alfa spajanja korišten za razliku okvira za izračun pokreta."
+ },
+ "frame_alpha": {
+ "label": "Alfa okvira",
+ "description": "Vrijednost alfa korištena prilikom spajanja okvira za predobradbu pokreta."
+ },
+ "frame_height": {
+ "label": "Visina okvira",
+ "description": "Visina u pikselima na koju se skaliraju okviri prilikom izračuna pokreta."
+ },
+ "mask": {
+ "label": "Koordinate maska",
+ "description": "Uredno x,y koordinate koje definiraju poligon maska pokreta za uključivanje/isključivanje područja."
+ },
+ "mqtt_off_delay": {
+ "label": "MQTT zakasnjenje isključivanja",
+ "description": "Sekunde koje se čekaju nakon posljednjeg pokreta prije objave MQTT 'isključeno' stanje."
+ },
+ "enabled_in_config": {
+ "label": "Originalno stanje pokreta",
+ "description": "Indikira je li detekcija pokreta bila omogućena u originalnoj statičkoj konfiguraciji."
+ },
+ "raw_mask": {
+ "label": "Ručna maska"
+ },
+ "description": "Zadane postavke detekcije pokreta primjenjene na kamere osim ako se prekrivaju po kamere."
+ },
+ "objects": {
+ "label": "Objekti",
+ "description": "Zadani parametri praćenja objekata uključujući koje oznake praćenja i filtre po objektu.",
+ "track": {
+ "label": "Objekti za praćenje",
+ "description": "Popis oznaka objekata za praćenje za sve kamere; može se prekrimiti po kamere."
+ },
+ "filters": {
+ "label": "Filtar objekata",
+ "description": "Filtar primijenjen na detektirane objekte kako bi se smanjila broj lažnih pozitiva (površina, omjer, pouzdanost).",
+ "min_area": {
+ "label": "Minimalna površina objekta",
+ "description": "Minimalna površina okvira (pikseli ili postotak) potrebna za ovaj tip objekta. Može biti pikseli (cijeli broj) ili postotak (float između 0.000001 i 0.99)."
+ },
+ "max_area": {
+ "label": "Maksimalna površina objekta",
+ "description": "Maksimalna površina okvira (pikseli ili postotak) dozvoljena za ovaj tip objekta. Može biti pikseli (cijeli broj) ili postotak (float između 0.000001 i 0.99)."
+ },
+ "min_ratio": {
+ "label": "Minimalni omjer visine/širine",
+ "description": "Minimalni omjer širine/visine potreban da bi okvir bio prihvaćen."
+ },
+ "max_ratio": {
+ "label": "Maksimalni omjer visine/širine",
+ "description": "Maksimalni omjer širine/visine dozvoljen da bi okvir bio prihvaćen."
+ },
+ "threshold": {
+ "label": "Prag pouzdanosti",
+ "description": "Prosjek pragova pouzdanosti detekcije potreban da bi objekt bio smatravan pravim pozitivom."
+ },
+ "min_score": {
+ "label": "Minimalna pouzdanost",
+ "description": "Minimalna pouzdanost detekcije po okviru potrebna da bi objekt bio brojan."
+ },
+ "mask": {
+ "label": "Maska filtriranja",
+ "description": "Koordinate poligona koje definiraju područje na kojem se ovaj filter primjenjuje unutar okvira."
+ },
+ "raw_mask": {
+ "label": "Ručna maska"
+ }
+ },
+ "mask": {
+ "label": "Maska objekta",
+ "description": "Poligonalna maska korištena za spriječavanje detekcije objekta u određenim područjima."
+ },
+ "raw_mask": {
+ "label": "Ručna maska"
+ },
+ "genai": {
+ "label": "Konfiguracija GenAI objekta",
+ "description": "Opcije GenAI za opisivanje praćenih objekata i slanje okvira za generisanje.",
+ "enabled": {
+ "label": "Omogući GenAI",
+ "description": "Omogući generisanje opisa za praćene objekte po zadanim postavkama."
+ },
+ "use_snapshot": {
+ "label": "Koristi snimke",
+ "description": "Koristi snimke objekata umjesto miniaturnih slika za generisanje opisa GenAI."
+ },
+ "prompt": {
+ "label": "Naslovni prompt",
+ "description": "Zadani šablon upita korišten za generisanje opisa pomoću GenAI."
+ },
+ "object_prompts": {
+ "label": "Prompti za objekte",
+ "description": "Prompti po objektu za prilagođavanje izlaza GenAI za specifične oznake."
+ },
+ "objects": {
+ "label": "GenAI objekti",
+ "description": "Popis oznaka objekata koje se po defaultu šalju GenAI."
+ },
+ "required_zones": {
+ "label": "Potrebne zone",
+ "description": "Zone koje moraju biti unesene za objekte da bi se kvalifikovali za generisanje opisa GenAI."
+ },
+ "debug_save_thumbnails": {
+ "label": "Sačuvajte miniaturne slike",
+ "description": "Sačuvaj miniaturne slike koje se šalju GenAI za ispravljanje i pregled."
+ },
+ "send_triggers": {
+ "label": "GenAI izazivači",
+ "description": "Definiše kada bi se trebale slati okvir za GenAI (na kraju, nakon ažuriranja, itd.).",
+ "tracked_object_end": {
+ "label": "Pošalji na kraju",
+ "description": "Pošalji zahtjev GenAI kada praćeni objekt završi."
+ },
+ "after_significant_updates": {
+ "label": "Raniji GenAI izazivač",
+ "description": "Pošalji zahtjev GenAI nakon određenog broja značajnih ažuriranja za praćeni objekt."
+ }
+ },
+ "enabled_in_config": {
+ "label": "Originalno stanje GenAI",
+ "description": "Pokazuje je li GenAI bio omogućen u originalnoj statičkoj konfiguraciji."
+ }
+ }
+ },
+ "record": {
+ "label": "Snimanje",
+ "enabled": {
+ "label": "Omogući snimanje",
+ "description": "Omogući ili onemogući snimanje za sve kamere; može se prekrimiti po kamere."
+ },
+ "expire_interval": {
+ "label": "Interval čišćenja snimanja",
+ "description": "Minute između čišćenja koja uklanjaju istekle segmente snimaka."
+ },
+ "continuous": {
+ "label": "Neprekidna retencija",
+ "description": "Broj dana za čuvanje snimaka bez obzira na praćene objekte ili pokret. Postavite na 0 ako želite da čuvate samo snimke upozorenja i detekcije.",
+ "days": {
+ "label": "Dane zadržavanja",
+ "description": "Dana za čuvanje snimaka."
+ }
+ },
+ "motion": {
+ "label": "Retencija pokreta",
+ "description": "Broj dana za čuvanje snimaka izazvanih pokretom bez obzira na praćene objekte. Postavite na 0 ako želite da čuvate samo snimke upozorenja i detekcije.",
+ "days": {
+ "label": "Dane zadržavanja",
+ "description": "Dana za čuvanje snimaka."
+ }
+ },
+ "detections": {
+ "label": "Retencija detekcije",
+ "description": "Postavke retencije snimaka za događaje detekcije uključujući trajanje pre/post snimanja.",
+ "pre_capture": {
+ "label": "Sekundi pre snimanja",
+ "description": "Broj sekundi prije događaja detekcije koje treba uključiti u snimak."
+ },
+ "post_capture": {
+ "label": "Sekunde nakon snimanja",
+ "description": "Broj sekundi nakon događaja detekcije koje se uključuju u snimanje."
+ },
+ "retain": {
+ "label": "Zadržavanje događaja",
+ "description": "Postavke zadržavanja za snimke događaja detekcije.",
+ "days": {
+ "label": "Dane zadržavanja",
+ "description": "Broj dana za koje se zadržavaju snimke događaja detekcije."
+ },
+ "mode": {
+ "label": "Način zadržavanja",
+ "description": "Način zadržavanja: sve (sačuvati sve segmente), pokret (sačuvati segmente s pokretom), ili aktivni_objekti (sačuvati segmente s aktivnim objektima)."
+ }
+ }
+ },
+ "alerts": {
+ "label": "Retencija upozorenja",
+ "description": "Postavke retencije snimaka za događaje upozorenja uključujući trajanje pre/post snimanja.",
+ "pre_capture": {
+ "label": "Sekundi pre snimanja",
+ "description": "Broj sekundi prije događaja detekcije koje treba uključiti u snimak."
+ },
+ "post_capture": {
+ "label": "Sekunde nakon snimanja",
+ "description": "Broj sekundi nakon događaja detekcije koje se uključuju u snimanje."
+ },
+ "retain": {
+ "label": "Zadržavanje događaja",
+ "description": "Postavke zadržavanja za snimke događaja detekcije.",
+ "days": {
+ "label": "Dane zadržavanja",
+ "description": "Broj dana za koje se zadržavaju snimke događaja detekcije."
+ },
+ "mode": {
+ "label": "Način zadržavanja",
+ "description": "Način zadržavanja: sve (sačuvati sve segmente), pokret (sačuvati segmente s pokretom), ili aktivni_objekti (sačuvati segmente s aktivnim objektima)."
+ }
+ }
+ },
+ "export": {
+ "label": "Konfiguracija izvoza",
+ "description": "Postavke koje se koriste prilikom izvoza snimaka kao što su timelapse i ubrzavanje dretve.",
+ "hwaccel_args": {
+ "label": "Argumeti ubrzavanja dretve za izvoz",
+ "description": "Argumeti ubrzavanja dretve za operacije izvoza/prenosa."
+ },
+ "max_concurrent": {
+ "label": "Maksimalan broj istovremenih izvoza",
+ "description": "Maksimalan broj poslova izvoza koji se obrađuju istovremeno."
+ }
+ },
+ "preview": {
+ "label": "Konfiguracija pregleda",
+ "description": "Postavke koje kontrolišu kvalitet pregleda snimanja prikazanih u UI.",
+ "quality": {
+ "label": "Kvaliteta pregleda",
+ "description": "Nivo kvalitete pregleda (vrlo_nizak, nizak, srednji, visok, vrlo_visok)."
+ }
+ },
+ "enabled_in_config": {
+ "label": "Originalno stanje snimanja",
+ "description": "Pokazuje je li snimanje bilo omogućeno u originalnoj statičkoj konfiguraciji."
+ },
+ "description": "Postavke za snimanje i zadržavanje primjenjene na kamere osim ako se prekrivaju po kamere."
+ },
+ "review": {
+ "label": "Pregled",
+ "alerts": {
+ "label": "Konfiguracija upozorenja",
+ "description": "Postavke za koje objekti praćeni generišu upozorenja i kako se upozorenja zadržavaju.",
+ "enabled": {
+ "label": "Omogući upozorenja",
+ "description": "Omogući ili onemogući generisanje upozorenja za sve kamere; može se prekrimiti po kamere."
+ },
+ "labels": {
+ "label": "Oznake upozorenja",
+ "description": "Lista oznaka objekata koje se smatraju upozorenjima (npr. automobil, osoba)."
+ },
+ "required_zones": {
+ "label": "Potrebne zone",
+ "description": "Zone koje objekt mora ući da bi se smatrao upozorenjem; ostavite prazno da omogućite bilo koju zonu."
+ },
+ "enabled_in_config": {
+ "label": "Originalno stanje upozorenja",
+ "description": "Pratiti je li upozorenja izvorno omogućena u statičkoj konfiguraciji."
+ },
+ "cutoff_time": {
+ "label": "Vrijeme prekida upozorenja",
+ "description": "Sekunde koje treba čekati nakon što nema aktivnosti koja uzrokuje upozorenje prije nego se prekine upozorenje."
+ }
+ },
+ "detections": {
+ "label": "Konfiguracija detekcija",
+ "description": "Postavke koje objekti koje se praćenje generišu detekcije (nepozornja) i kako se detekcije čuvaju.",
+ "enabled": {
+ "label": "Omogući detekcije",
+ "description": "Omogući ili onemogući događaje detekcije za sve kamere; može se prekrimiti po kamere."
+ },
+ "labels": {
+ "label": "Oznake detekcije",
+ "description": "Popis oznaka objekata koje kvalifikuju kao događaji detekcije."
+ },
+ "required_zones": {
+ "label": "Potrebne zone",
+ "description": "Zone koje objekt mora ući da bi se smatrao detekcijom; ostavite prazno da omogućite bilo koju zonu."
+ },
+ "cutoff_time": {
+ "label": "Vrijeme prekida detekcija",
+ "description": "Sekunde koje treba čekati nakon što nema aktivnosti koja uzrokuje detekciju prije nego se prekine detekcija."
+ },
+ "enabled_in_config": {
+ "label": "Originalno stanje detekcija",
+ "description": "Pratiti je li detekcije izvorno omogućene u statičkoj konfiguraciji."
+ }
+ },
+ "genai": {
+ "label": "Konfiguracija GenAI",
+ "description": "Kontrolira korištenje generativne AI za proizvodnju opisa i sažetaka stavki za pregled.",
+ "enabled": {
+ "label": "Omogući opise GenAI",
+ "description": "Omogući ili onemogući opise i sažetke generirane GenAI za stavke za pregled."
+ },
+ "alerts": {
+ "label": "Omogući GenAI za upozorenja",
+ "description": "Koristi GenAI za generiranje opisa stavki upozorenja."
+ },
+ "detections": {
+ "label": "Omogući GenAI za detekcije",
+ "description": "Koristite GenAI za generiranje opisa predmeta detekcije."
+ },
+ "image_source": {
+ "label": "Pregledajte izvor slike",
+ "description": "Izvor slika poslatih GenAIJ-u ('preview' ili 'recordings'); 'recordings' koristi kvalitetnije okvire, ali više tokena."
+ },
+ "additional_concerns": {
+ "label": "Dodatne brige",
+ "description": "Popis dodatnih briga ili napomena koje GenAI treba uzeti u obzir prilikom procjene aktivnosti na ovoj kameri."
+ },
+ "debug_save_thumbnails": {
+ "label": "Sačuvajte miniaturne slike",
+ "description": "Sačuvajte miniaturne slike koje se šalju GenAI provajderu za ispravljanje grešaka i pregled."
+ },
+ "enabled_in_config": {
+ "label": "Originalno stanje GenAI",
+ "description": "Pratiti je li pregled GenAI izvorno omogućen u statičkoj konfiguraciji."
+ },
+ "preferred_language": {
+ "label": "Preferirani jezik",
+ "description": "Preferirani jezik za zahtijevanje od GenAI provajdera za generirane odgovore."
+ },
+ "activity_context_prompt": {
+ "label": "Prompt konteksta aktivnosti",
+ "description": "Prilagođeni prompt koji opisuje što je i što nije sumnjivo ponašanje kako bi pružio kontekst za sažetke GenAI."
+ }
+ },
+ "description": "Postavke koje kontrolišu upozorenja, detekcije i GenAI pregledne sažetke korišteni od strane UI i skladišta."
+ },
+ "semantic_search": {
+ "label": "Semantička pretraga",
+ "triggers": {
+ "label": "Pokretači",
+ "description": "Akcije i kriteriji za usklađivanje za pokretače semantičke pretrage specifične za kameru.",
+ "friendly_name": {
+ "label": "Prijateljsko ime",
+ "description": "Nepovlačno prijateljsko ime prikazano u korisničkom sučelju za ovaj pokretač."
+ },
+ "enabled": {
+ "label": "Omogući ovaj pokretač",
+ "description": "Omogući ili onemogući ovaj pokretač semantičke pretrage."
+ },
+ "type": {
+ "label": "Tip pokretača",
+ "description": "Tip pokretača: 'thumbnail' (uspoređivanje slikom) ili 'description' (uspoređivanje teksta)."
+ },
+ "data": {
+ "label": "Sadržaj pokretača",
+ "description": "Tekstualni izraz ili ID miniaturne slike za uspoređivanje s praćenim objektima."
+ },
+ "threshold": {
+ "label": "Prag aktivacije",
+ "description": "Minimalna ocjena sličnosti (0-1) potrebna za aktivaciju ovog izazivača."
+ },
+ "actions": {
+ "label": "Akcije izazivača",
+ "description": "Popis akcija koje se izvršavaju kada izazivač odgovara (obavijest, pod_naziv, atribute)."
+ }
+ },
+ "description": "Postavke za semantičku pretragu koja građi i upita objektne ugradnje da bi pronašla slične stavke.",
+ "enabled": {
+ "label": "Omogući semantičku pretragu",
+ "description": "Omogući ili onemogući funkciju semantičke pretrage."
+ },
+ "reindex": {
+ "label": "Ponovno indeksiranje pri pokretanju",
+ "description": "Pokrenite puno ponovno indeksiranje povijesnih praćenih objekata u bazu ugradnji."
+ },
+ "model": {
+ "label": "Ime modela za semantičku pretragu ili dobavljača GenAI",
+ "description": "Model ugradnje koji se koristi za semantičku pretragu (npr. 'jinav1'), ili ime dobavljača GenAI s ulogom ugradnje."
+ },
+ "model_size": {
+ "label": "Veličina modela",
+ "description": "Izaberite veličinu modela; 'small' radi na CPU i 'large' obično zahtijeva GPU."
+ },
+ "device": {
+ "label": "Uređaj",
+ "description": "Ovo je preklop za ciljanje specifičnog uređaja. Vidi https://onnxruntime.ai/docs/execution-providers/ za više informacija"
+ }
+ },
+ "snapshots": {
+ "label": "Snimci",
+ "enabled": {
+ "label": "Omogući snimke",
+ "description": "Omogući ili onemogući sačuvanje snimaka za sve kamere; može se prekrimiti po kamere."
+ },
+ "timestamp": {
+ "label": "Preklapanje vremenske oznake",
+ "description": "Preklopiti vremensku oznaku na snimke iz API-ja."
+ },
+ "bounding_box": {
+ "label": "Preklapanje okvira",
+ "description": "Crtanje okvira za praćene objekte na snimke iz API-ja."
+ },
+ "crop": {
+ "label": "Izrezivanje snimke",
+ "description": "Izrezivanje snimki iz API-ja do okvira detektiranog objekta."
+ },
+ "required_zones": {
+ "label": "Potrebne zone",
+ "description": "Zone koje objekt mora ući da bi snimka bila sačuvana."
+ },
+ "height": {
+ "label": "Visina snimke",
+ "description": "Visina (pikseli) za promjenu veličine snimki iz API-ja; ostavite prazno da biste sačuvali originalnu veličinu."
+ },
+ "retain": {
+ "label": "Zadržavanje snimki",
+ "description": "Postavke zadržavanja snimki uključujući zadane dane i prekriženja po objektu.",
+ "default": {
+ "label": "Zadano zadržavanje",
+ "description": "Zadani broj dana za zadržavanje snimki."
+ },
+ "mode": {
+ "label": "Način zadržavanja",
+ "description": "Način zadržavanja: sve (sačuvati sve segmente), pokret (sačuvati segmente s pokretom), ili aktivni_objekti (sačuvati segmente s aktivnim objektima)."
+ },
+ "objects": {
+ "label": "Zadržavanje objekata",
+ "description": "Prekriženja po objektu za dane zadržavanja snimki."
+ }
+ },
+ "quality": {
+ "label": "Kvaliteta snimka",
+ "description": "Kvaliteta kodiranja za sačuvane snimke (0-100)."
+ },
+ "description": "Postavke za API generisane snimke praćenih objekata za sve kamere; može se prekrimiti po kamere."
+ },
+ "timestamp_style": {
+ "label": "Stil vremenske oznake",
+ "position": {
+ "label": "Pozicija vremenske oznake",
+ "description": "Pozicija vremenske oznake na slici (tl/tr/bl/br)."
+ },
+ "format": {
+ "label": "Format vremenske oznake",
+ "description": "String formata datuma i vremena korišten za vremenske oznake (Python format koda za datum i vrijeme)."
+ },
+ "color": {
+ "label": "Boja vremenske oznake",
+ "description": "RGB vrijednosti boja za tekst vremenske oznake (sve vrijednosti 0-255).",
+ "red": {
+ "label": "Crvena",
+ "description": "Crveni komponent (0-255) za boju vremenske oznake."
+ },
+ "green": {
+ "label": "Zelena",
+ "description": "Zeleni komponent (0-255) za boju vremenske oznake."
+ },
+ "blue": {
+ "label": "Plava",
+ "description": "Plavi komponent (0-255) za boju vremenske oznake."
+ }
+ },
+ "thickness": {
+ "label": "Debljina vremenske oznake",
+ "description": "Debljina linije teksta vremenske oznake."
+ },
+ "effect": {
+ "label": "Efekt vremenske oznake",
+ "description": "Vizualni efekt za tekst vremenske oznake (none, solid, shadow)."
+ },
+ "description": "Opcije stilizacije vremenskih oznaka u toku prikaza primjenjene na debug prikaz i snimke."
+ },
+ "mqtt": {
+ "label": "MQTT",
+ "description": "Postavke za povezivanje i objavljivanje telemetrije, snimaka i detalja događaja na MQTT brokera.",
+ "enabled": {
+ "label": "Omogući MQTT",
+ "description": "Omogući ili onemogući integraciju MQTT za stanje, događaje i snimke."
+ },
+ "host": {
+ "label": "Gospodar MQTT",
+ "description": "Ime domene ili IP adresa MQTT brokera."
+ },
+ "port": {
+ "label": "Port MQTT",
+ "description": "Port MQTT brokera (obično 1883 za običan MQTT)."
+ },
+ "topic_prefix": {
+ "label": "Predfiks teme",
+ "description": "Predložak teme MQTT za sve teme Frigate; mora biti jedinstven ako pokrećete više instanci."
+ },
+ "client_id": {
+ "label": "ID klijenta",
+ "description": "Identifikator klijenta korišten pri povezivanju s MQTT brokerom; trebao bi biti jedinstven po instanci."
+ },
+ "stats_interval": {
+ "label": "Interval statistika",
+ "description": "Interval u sekundama za objavljivanje sustavnih i kamera statistika na MQTT."
+ },
+ "user": {
+ "label": "Korisničko ime MQTT",
+ "description": "Nepovlačno korisničko ime MQTT; može se pružiti putem varijabli okoline ili vjerodajnica."
+ },
+ "password": {
+ "label": "Lozinka MQTT",
+ "description": "Nepovlačna lozinka MQTT; može se pružiti putem varijabli okoline ili vjerodajnica."
+ },
+ "tls_ca_certs": {
+ "label": "TLS CA sertifikati",
+ "description": "Putanja do sertifikata CA za TLS povezivanje s brokerom (za samopotpisane sertifikate)."
+ },
+ "tls_client_cert": {
+ "label": "Klijent sertifikat",
+ "description": "Putanja do sertifikata klijenta za TLS međusobnu autentifikaciju; ne postavljajte korisničko ime/lozinku kada koristite sertifikate klijenta."
+ },
+ "tls_client_key": {
+ "label": "Klijent ključ",
+ "description": "Putanja do privatnog ključa za klijent sertifikat."
+ },
+ "tls_insecure": {
+ "label": "TLS nebezbedan",
+ "description": "Dozvoli nebezbedne TLS povezivanja preskačući provjeru imena domene (nije preporučeno)."
+ },
+ "qos": {
+ "label": "MQTT QoS",
+ "description": "Nivo kvaliteta usluge za MQTT objave/pretplate (0, 1 ili 2)."
+ }
+ },
+ "notifications": {
+ "label": "Obavještenja",
+ "enabled": {
+ "label": "Omogući obavijesti",
+ "description": "Omogući ili onemogući obavijesti za sve kamere; mogu se prekrivati po kamere."
+ },
+ "email": {
+ "label": "E-mail za obavijesti",
+ "description": "Adresa e-maila koja se koristi za obavijesti putem push-a ili je potrebna određenim dobavljačima obavijesti."
+ },
+ "cooldown": {
+ "label": "Period hlađenja",
+ "description": "Period hlađenja (sekunde) između obavijesti kako bi se izbjeglo spaming primateljima."
+ },
+ "enabled_in_config": {
+ "label": "Originalno stanje obavijesti",
+ "description": "Pokazuje je li obavijesti bile omogućene u originalnoj statičkoj konfiguraciji."
+ },
+ "description": "Postavke za omogućavanje i kontrolu obavijesti za sve kamere; mogu se prekrivati po kamere."
+ },
+ "onvif": {
+ "label": "ONVIF",
+ "description": "Postavke povezivanja preko ONVIF i automatskog praćenja PTZ za ovu kameru.",
+ "host": {
+ "label": "Gost ONVIF",
+ "description": "Gost (i opcionalni shema) za uslugu ONVIF za ovu kameru."
+ },
+ "port": {
+ "label": "Port ONVIF",
+ "description": "Broj porta za uslugu ONVIF."
+ },
+ "user": {
+ "label": "Korisničko ime za ONVIF",
+ "description": "Korisničko ime za autentifikaciju ONVIF; neki uređaji zahtijevaju korisnika admin za ONVIF."
+ },
+ "password": {
+ "label": "Lozinka za ONVIF",
+ "description": "Lozinka za autentifikaciju ONVIF."
+ },
+ "tls_insecure": {
+ "label": "Onemogući provjeru TLS",
+ "description": "Preskoči provjeru TLS i onemogući digest autentifikaciju za ONVIF (nebezbedno; koristiti samo u sigurnim mrežama)."
+ },
+ "profile": {
+ "label": "ONVIF profil",
+ "description": "Specifičan ONVIF medij profil za korištenje za kontrolu PTZ, prilagođen tokenom ili imenom. Ako nije postavljen, prvi profil s važećom konfiguracijom PTZ automatski se odabire."
+ },
+ "autotracking": {
+ "label": "Autotračenje",
+ "description": "Automatski praćenje pokretanja objekata i držanje ih u sredini okvira korištenjem pokreta kamere PTZ.",
+ "enabled": {
+ "label": "Omogući automatsko praćenje",
+ "description": "Omogući ili onemogući automatsko praćenje kamere PTZ detektiranih objekata."
+ },
+ "calibrate_on_startup": {
+ "label": "Kalibriraj na početku",
+ "description": "Mjeri brzine motora PTZ pri pokretanju kako bi poboljšao preciznost praćenja. Frigate će ažurirati konfiguraciju s težinama pokreta nakon kalibracije."
+ },
+ "zooming": {
+ "label": "Režim zumiranja",
+ "description": "Kontrola ponašanja zumiranja: onemogućeno (samo pan/tilt), apsolutno (najkompatibilnije) ili relativno (konkurentno pan/tilt/zum)."
+ },
+ "zoom_factor": {
+ "label": "Faktor zumiranja",
+ "description": "Kontrola razine zumiranja na praćenim objektima. Niže vrijednosti drže više scene u pogledu; više vrijednosti zumiraju bliže, ali mogu izgubiti praćenje. Vrijednosti između 0.1 i 0.75."
+ },
+ "track": {
+ "label": "Praćeni objekti",
+ "description": "Popis vrsta objekata koji trebaju pokrenuti automatsko praćenje."
+ },
+ "required_zones": {
+ "label": "Potrebne zone",
+ "description": "Objekti moraju ući u jednu od ovih zona prije nego što započne automatsko praćenje."
+ },
+ "return_preset": {
+ "label": "Povratak na predpostavku",
+ "description": "Ime predpostavke konfigurirano u firmware kamere za povratak nakon završetka praćenja."
+ },
+ "timeout": {
+ "label": "Vrijeme čekanja povratka",
+ "description": "Čekajte ovaj broj sekundi nakon gubitka praćenja prije povratka kamere na predpostavljeno mjesto."
+ },
+ "movement_weights": {
+ "label": "Težine pokreta",
+ "description": "Vrijednosti kalibracije automatski generirane kroz kalibraciju kamere. Ne mijenjajte ručno."
+ },
+ "enabled_in_config": {
+ "label": "Originalni stanje autotračenja",
+ "description": "Unutarnje polje za praćenje je li autotračenje bilo omogućeno u konfiguraciji."
+ }
+ },
+ "ignore_time_mismatch": {
+ "label": "Zanemari razliku u vremenu",
+ "description": "Zanemari razlike u sinhronizaciji vremena između kamere i Frigate servera za komunikaciju ONVIF."
+ }
+ },
+ "profiles": {
+ "label": "Profili",
+ "description": "Imenovane definicije profila s prijateljivim imenima. Profili kamera moraju se referirati na imena definirana ovdje.",
+ "friendly_name": {
+ "label": "Prijateljsko ime",
+ "description": "Prikazano ime za ovaj profil prikazano u UI-u."
+ }
+ },
+ "safe_mode": {
+ "label": "Sigurnosni režim",
+ "description": "Kada je omogućeno, pokrenite Frigate u sigurnosnom režimu s smanjenim funkcijama za uklanjanje problema."
+ },
+ "environment_vars": {
+ "label": "Okolinski varijable",
+ "description": "Parovi ključ/vrijednost okolinskih varijabli za postavljanje za proces Frigate u Home Assistant OS. Korisnici koji nisu HAOS moraju koristiti konfiguraciju okolinskih varijabli Docker umjesto toga."
+ },
+ "logger": {
+ "label": "Zapisi",
+ "description": "Kontrolira podrazumijevanu razinu detaljnosti zapisa i prekriženja razina detaljnosti po komponenti.",
+ "default": {
+ "label": "Razina zapisa",
+ "description": "Podrazumijevana globalna razina detaljnosti (debug, info, warning, error)."
+ },
+ "logs": {
+ "label": "Razina zapisa po procesu",
+ "description": "Prekriženja razina detaljnosti po komponenti za povećanje ili smanjenje detaljnosti za određene module."
+ }
+ },
+ "auth": {
+ "label": "Autentifikacija",
+ "description": "Postavke povezane s autentifikacijom i sesijama uključujući opcije kolačića i ograničenja brzine.",
+ "enabled": {
+ "label": "Omogući autentifikaciju",
+ "description": "Omogući nativnu autentifikaciju za korisnički sučelje Frigate."
+ },
+ "reset_admin_password": {
+ "label": "Ponovno postavljanje lozinke administratora",
+ "description": "Ako je tačno, ponovno postavite lozinku korisnika administratora pri pokretanju i ispišite novu lozinku u zapisima."
+ },
+ "cookie_name": {
+ "label": "Ime kolačića JWT",
+ "description": "Ime kolačića koji se koristi za pohranjivanje JWT tokena za nativnu autentifikaciju."
+ },
+ "cookie_secure": {
+ "label": "Sigurnosni flag kolačića",
+ "description": "Postavite sigurnosni flag na kolačić autentifikacije; trebalo bi biti tačno kada se koristi TLS."
+ },
+ "session_length": {
+ "label": "Trajanje sesije",
+ "description": "Trajanje sesije u sekundama za sesije temeljene na JWT."
+ },
+ "refresh_time": {
+ "label": "Prozor osvežavanja sesije",
+ "description": "Kada se sesija nalazi unutar ovih sekundi do isteka, ponovo je ažurirati na punu dužinu."
+ },
+ "failed_login_rate_limit": {
+ "label": "Ograničenja za neuspješne prijave",
+ "description": "Pravila ograničavanja brzine za neuspješne pokušaje prijave kako bi se smanjila napada silom."
+ },
+ "trusted_proxies": {
+ "label": "Povereni proxy-ovi",
+ "description": "Lista IP adresa poverenih proxy-ova korištena prilikom određivanja IP adrese klijenta za ograničavanje brzine."
+ },
+ "hash_iterations": {
+ "label": "Iteracije haširanja",
+ "description": "Broj iteracija PBKDF2-SHA256 koje se koriste za kriptiranje lozinki korisnika."
+ },
+ "roles": {
+ "label": "Mapiranja uloga",
+ "description": "Pridružiti uloge listama kamera. Prazna lista omogućava pristup svim kamerama za ulogu."
+ },
+ "admin_first_time_login": {
+ "label": "Zastavica za prvi put administrator",
+ "description": "Kada je istina, UI može prikazati poveznicu za pomoć na stranici prijave koja obavješćuje korisnike kako se prijaviti nakon ponovnog postavljanja lozinke administratora. "
+ }
+ },
+ "database": {
+ "label": "Baza podataka",
+ "description": "Postavke SQLite baze podataka korištene od strane Frigate za pohranjivanje metapodataka praćenih objekata i metapodataka snimaka.",
+ "path": {
+ "label": "Putanja do baze podataka",
+ "description": "Putanja datotečnog sustava gdje će se datoteka SQLite baze podataka Frigate pohraniti."
+ }
+ },
+ "go2rtc": {
+ "label": "go2rtc",
+ "description": "Postavke integrirane usluge go2rtc ponovnog prenošenja korištene za prenošenje živih streamova i prevodjenje."
+ },
+ "networking": {
+ "label": "Mrežno",
+ "description": "Postavke povezane s mrežom, kao što je omogućavanje IPv6 za Frigate krajeve.",
+ "ipv6": {
+ "label": "Konfiguracija IPv6",
+ "description": "IPv6-specifične postavke za mrežne usluge Frigate.",
+ "enabled": {
+ "label": "Omogući IPv6",
+ "description": "Omogući podršku za IPv6 za usluge Frigate (API i UI) gdje je primjenjivo."
+ }
+ },
+ "listen": {
+ "label": "Konfiguracija slušajućih porta",
+ "description": "Konfiguracija unutarnjih i vanjskih slušajućih porta. Ovo je za napredne korisnike. Za većinu slučajeva preporučuje se promijeniti sekciju porta u svojoj Docker compose datoteci.",
+ "internal": {
+ "label": "Unutarnji port",
+ "description": "Unutarnji slušajući port za Frigate (zadano 5000)."
+ },
+ "external": {
+ "label": "Vanjski port",
+ "description": "Vanjski slušajući port za Frigate (zadano 8971)."
+ }
+ }
+ },
+ "proxy": {
+ "label": "Proxy",
+ "description": "Postavke za integraciju Frigate iza obrnute proxy posrednike koji prenose zaglavlja autentificiranih korisnika.",
+ "header_map": {
+ "label": "Mapiranje zaglavlja",
+ "description": "Mapiraj dolazna zaglavlja proxy-a na polja korisnika i uloge Frigate za autentifikaciju baziranu na proxy-u.",
+ "user": {
+ "label": "Zaglavlje korisnika",
+ "description": "Zaglavlje koje sadrži autentificirano korisničko ime pruženo od strane nadolazećeg proxy-a."
+ },
+ "role": {
+ "label": "Zaglavlje uloge",
+ "description": "Zaglavlje koje sadrži ulogu ili grupe autentificiranog korisnika od strane nadolazećeg proxy-a."
+ },
+ "role_map": {
+ "label": "Mapiranje uloga",
+ "description": "Mapiraj vrijednosti grupe iznad na uloge Frigate (npr. mapiraj grupe administratora na ulogu administratora)."
+ }
+ },
+ "logout_url": {
+ "label": "URL za odjavu",
+ "description": "URL na koji će korisnici biti preusmjereni kada se odjave putem proxy-a."
+ },
+ "auth_secret": {
+ "label": "Tajna proxy",
+ "description": "Nepovlačena tajna provjeravana protiv zaglavlja X-Proxy-Secret za potvrdu pouzdanih proxy-a."
+ },
+ "default_role": {
+ "label": "Zadana uloga",
+ "description": "Zadana uloga dodijeljena korisnicima autentificiranim putem proxy-a kada neka mapiranja uloga ne vrijede (administrator ili pregledač)."
+ },
+ "separator": {
+ "label": "Znak separatora",
+ "description": "Karakter koristen za razdvajanje više vrijednosti navedenih u zaglavju proksi."
+ }
+ },
+ "telemetry": {
+ "label": "Telemetrija",
+ "description": "Opcije sistem telemetrije i statistika uključujući praćenje širine pojasa mreže i GPU.",
+ "network_interfaces": {
+ "label": "Mrežni sučelja",
+ "description": "Popis prefiksa imena mrežnih sučelja za praćenje statistika širine pojasa."
+ },
+ "stats": {
+ "label": "Sistem statistika",
+ "description": "Opcije za omogućavanje/onemogućavanje prikupljanja različitih sistem i GPU statistika.",
+ "amd_gpu_stats": {
+ "label": "AMD GPU statistika",
+ "description": "Omogući prikupljanje AMD GPU statistika ako je prisutan AMD GPU."
+ },
+ "intel_gpu_stats": {
+ "label": "Intel GPU statistika",
+ "description": "Omogući prikupljanje Intel GPU statistika ako je prisutan Intel GPU."
+ },
+ "network_bandwidth": {
+ "label": "Širina pojasa mreže",
+ "description": "Omogući praćenje širine pojasa mreže po procesu za procese kamere ffmpeg i detektore (zahtijeva mogućnosti)."
+ },
+ "intel_gpu_device": {
+ "label": "Intel GPU uređaj",
+ "description": "PCI adresa magistrale ili DRM putanja uređaja (npr. /dev/dri/card1) koja se koristi za vezivanje Intel GPU statistika za određeni uređaj kada je prisutno više njih."
+ }
+ },
+ "version_check": {
+ "label": "Provjera verzije",
+ "description": "Omogući ishodnu provjeru za otkrivanje ako je dostupnija verzija Frigate."
+ }
+ },
+ "tls": {
+ "label": "TLS",
+ "description": "Postavke TLS za web krajnje točke Frigate (port 8971).",
+ "enabled": {
+ "label": "Omogući TLS",
+ "description": "Omogući TLS za web UI i API Frigate na konfiguriranom TLS portu."
+ }
+ },
+ "ui": {
+ "label": "UI",
+ "description": "Postavke korisničkog sučelja poput vremenske zone, oblikovanja vremena/datuma i jedinica.",
+ "timezone": {
+ "label": "Vremenska zona",
+ "description": "Nepovlačena vremenska zona za prikaz kroz UI (podrazumijevano je lokalno vrijeme preglednika ako nije postavljeno)."
+ },
+ "time_format": {
+ "label": "Oblik vremena",
+ "description": "Oblik vremena za korištenje u UI (browser, 12hour, ili 24hour)."
+ },
+ "date_style": {
+ "label": "Oblik datuma",
+ "description": "Oblik datuma za korištenje u UI (full, long, medium, short)."
+ },
+ "time_style": {
+ "label": "Oblik vremena",
+ "description": "Oblik vremena za korištenje u UI (full, long, medium, short)."
+ },
+ "unit_system": {
+ "label": "Sustav jedinica",
+ "description": "Sustav jedinica za prikaz (metric ili imperial) korišten u UI i MQTT."
+ }
+ },
+ "detectors": {
+ "label": "Hardver detektora",
+ "description": "Konfiguracija za detektore objekata (CPU, GPU, ONNX backends) i bilo koje postavke modela specifične za detektor.",
+ "type": {
+ "label": "Tip"
+ },
+ "model": {
+ "label": "Konfiguracija modela specifične za detektor",
+ "description": "Opcije konfiguracije modela specifične za detektor (putanja, veličina ulaza, itd.).",
+ "path": {
+ "label": "Putanja za prilagođeni model detektora objekata",
+ "description": "Putanja do datoteke prilagođenog modela detekcije (ili plus:// za modele Frigate+)."
+ },
+ "labelmap_path": {
+ "label": "Mapa oznaka za prilagođeni detektor objekata",
+ "description": "Putanja do datoteke mape oznaka koja mapira numeričke klase na string oznake za detektor."
+ },
+ "width": {
+ "label": "Širina ulaznog tenzora modela detekcije objekata",
+ "description": "Širina ulaznog tenzora modela u pikselima."
+ },
+ "height": {
+ "label": "Visina ulaznog tenzora modela detekcije objekata",
+ "description": "Visina ulaznog tenzora modela u pikselima."
+ },
+ "labelmap": {
+ "label": "Prilagodba mape oznaka",
+ "description": "Preklop ili preslikavanje unosa za uključivanje u standardnu mapu oznaka."
+ },
+ "attributes_map": {
+ "label": "Mapa oznaka objekata na njihove atribute",
+ "description": "Preslikavanje iz oznaka objekata na atribute oznaka koje se koriste za dodavanje metapodataka (npr. 'car' -> ['license_plate'])."
+ },
+ "input_tensor": {
+ "label": "Oblik tenzora ulaza modela",
+ "description": "Format tenzora očekivan od strane modela: 'nhwc' ili 'nchw'."
+ },
+ "input_pixel_format": {
+ "label": "Format boje piksela ulaza modela",
+ "description": "Boja piksela očekivana od strane modela: 'rgb', 'bgr' ili 'yuv'."
+ },
+ "input_dtype": {
+ "label": "Tip D ulaza modela",
+ "description": "Tip podataka modela ulaznog tenzora (npr. 'float32')."
+ },
+ "model_type": {
+ "label": "Tip modela detekcije objekata",
+ "description": "Tip arhitekture modela detektora (ssd, yolox, yolonas) korišten od strane nekih detektora za optimizaciju."
+ }
+ },
+ "model_path": {
+ "label": "Putanja modela specifična za detektor",
+ "description": "Putanja datoteke do binarne datoteke modela detektora ako je potrebna odabranim detektorom."
+ },
+ "axengine": {
+ "label": "AXEngine NPU",
+ "description": "Detektor AXERA AX650N/AX8850N NPU koji pokreće prevedene .axmodel datoteke putem AXEngine runtime-a."
+ },
+ "cpu": {
+ "label": "CPU",
+ "description": "Detektor CPU TFLite koji pokreće modele TensorFlow Lite na domaćem CPU bez hardverske akceleracije. Nije preporučeno.",
+ "num_threads": {
+ "label": "Broj nitova detekcije",
+ "description": "Broj nitova korištenih za inferenciju na CPU."
+ }
+ },
+ "deepstack": {
+ "label": "DeepStack",
+ "description": "Detektor DeepStack/CodeProject.AI koji šalje slike na udaljenu DeepStack HTTP API za inferenciju. Nije preporučeno.",
+ "api_url": {
+ "label": "URL API-ja DeepStack",
+ "description": "URL API-ja DeepStack."
+ },
+ "api_timeout": {
+ "label": "Vrijeme čekanja API-ja DeepStack (u sekundama)",
+ "description": "Maksimalno dozvoljeno vrijeme za zahtjev API-ja DeepStack."
+ },
+ "api_key": {
+ "label": "Ključ API-ja DeepStack (ako je potreban)",
+ "description": "Nepovlađeni ključ API-ja za autentificirane usluge DeepStack."
+ }
+ },
+ "degirum": {
+ "label": "DeGirum",
+ "description": "Detektor DeGirum za pokretanje modela putem DeGirum oblaka ili lokalnih usluga inferencije.",
+ "location": {
+ "label": "Lokacija inferencije",
+ "description": "Lokacija DeGirim inferencije (npr. '@cloud', '127.0.0.1')."
+ },
+ "zoo": {
+ "label": "Model Zoo",
+ "description": "Putanja ili URL do DeGirum model zoo."
+ },
+ "token": {
+ "label": "Token za DeGirum Cloud",
+ "description": "Token za pristup DeGirum Cloud."
+ }
+ },
+ "edgetpu": {
+ "label": "EdgeTPU",
+ "description": "Detektor EdgeTPU koji pokreće modele TensorFlow Lite kompilirane za Coral EdgeTPU pomoću EdgeTPU delegata.",
+ "device": {
+ "label": "Tip uređaja",
+ "description": "Uređaj za korištenje EdgeTPU inferencije (npr. 'usb', 'pci')."
+ }
+ },
+ "hailo8l": {
+ "label": "Hailo-8/Hailo-8L",
+ "description": "Detektor Hailo-8/Hailo-8L koji koristi HEF modele i HailoRT SDK za inferenciju na Hailo uređaju.",
+ "device": {
+ "label": "Tip uređaja",
+ "description": "Uređaj za korištenje Hailo inferencije (npr. 'PCIe', 'M.2')."
+ }
+ },
+ "memryx": {
+ "label": "MemryX",
+ "description": "Detektor MemryX MX3 koji pokreće kompilirane modele DFP na MemryX akceleratorima.",
+ "device": {
+ "label": "Putanja uređaja",
+ "description": "Uređaj za korištenje MemryX inferencije (npr. 'PCIe')."
+ }
+ },
+ "onnx": {
+ "label": "ONNX",
+ "description": "Detektor ONNX za pokretanje ONNX modela; koristi dostupne akceleracijske backendove (CUDA/ROCm/OpenVINO) kada su dostupni.",
+ "device": {
+ "label": "Tip uređaja",
+ "description": "Uređaj za korištenje ONNX inferencije (npr. 'AUTO', 'CPU', 'GPU')."
+ }
+ },
+ "openvino": {
+ "label": "OpenVINO",
+ "description": "Detektor OpenVINO za AMD i Intel CPU-e, Intel GPU-e i Intel VPU uređaje.",
+ "device": {
+ "label": "Tip uređaja",
+ "description": "Uređaj za korištenje za inferenciju OpenVINO (npr. 'CPU', 'GPU', 'NPU')."
+ }
+ },
+ "rknn": {
+ "label": "RKNN",
+ "description": "Detektor RKNN za NPUs Rockchipa; izvršava preveđene modele RKNN na Rockchip uređaju.",
+ "num_cores": {
+ "label": "Broj jezgri NPU koje se koriste.",
+ "description": "Broj jezgri NPU koje se koriste (0 za automatsko)."
+ }
+ },
+ "synaptics": {
+ "label": "Synaptics",
+ "description": "Detektor NPU Synaptics za modele u formatu .synap pomoću SDK-a Synap na uređaju Synaptics."
+ },
+ "teflon_tfl": {
+ "label": "Teflon",
+ "description": "Detektor delegata Teflon za TFLite pomoću biblioteke Mesa Teflon delegata za ubrzanje inferencije na podržanim GPU-ima."
+ },
+ "tensorrt": {
+ "label": "TensorRT",
+ "description": "Detektor TensorRT za uređaje Nvidia Jetson koji koristi serijalizirane TensorRT motore za ubrzanu inferenciju.",
+ "device": {
+ "label": "Indeks GPU uređaja",
+ "description": "Indeks GPU uređaja za korištenje."
+ }
+ },
+ "zmq": {
+ "label": "ZMQ IPC",
+ "description": "Detektor ZMQ IPC koji prenosi inferenciju vanjskom procesu putem ZMQ IPC kraja.",
+ "endpoint": {
+ "label": "ZMQ IPC kraja",
+ "description": "Kraj ZMQ-a na koji se povezati."
+ },
+ "request_timeout_ms": {
+ "label": "ZMQ zahtjev timeout u milisekundama",
+ "description": "Timeout za ZMQ zahtjeve u milisekundama."
+ },
+ "linger_ms": {
+ "label": "ZMQ socket linger u milisekundama",
+ "description": "Period linger socketa u milisekundama."
+ }
+ }
+ },
+ "model": {
+ "label": "Model detekcije",
+ "description": "Postavke za konfiguraciju prilagođenog modela detekcije objekata i njegove ulazne oblike.",
+ "path": {
+ "label": "Put do prilagođenog modela detekcije",
+ "description": "Put do datoteke prilagođenog modela detekcije (ili plus:// za modele Frigate+)."
+ },
+ "labelmap_path": {
+ "label": "Mapa oznaka za prilagođeni detektor objekata",
+ "description": "Putanja do datoteke labelmap koja preslikava numeričke klase u string oznake za detektor."
+ },
+ "width": {
+ "label": "Širina ulaznog tenzora modela detekcije objekata",
+ "description": "Širina ulaznog tenzora modela u pikselima."
+ },
+ "height": {
+ "label": "Visina ulaznog tenzora modela detekcije objekata",
+ "description": "Visina ulaznog tenzora modela u pikselima."
+ },
+ "labelmap": {
+ "label": "Prilagodba labelmap",
+ "description": "Preklop ili preslikavanje unosa za spajanje u standardnu labelmap."
+ },
+ "attributes_map": {
+ "label": "Mapa oznaka objekata na njihove atribute oznake",
+ "description": "Preslikavanje iz oznaka objekata na atribute oznake koje se koriste za dodavanje metapodataka (npr. 'car' -> ['license_plate'])."
+ },
+ "input_tensor": {
+ "label": "Oblik ulaznog tenzora modela",
+ "description": "Format tenzora očekivan od strane modela: 'nhwc' ili 'nchw'."
+ },
+ "input_pixel_format": {
+ "label": "Format boje piksela ulaznog modela",
+ "description": "Boja prostor očekivan od strane modela: 'rgb', 'bgr' ili 'yuv'."
+ },
+ "input_dtype": {
+ "label": "D tip ulaza modela",
+ "description": "Tip podataka ulaznog tenzora modela (npr. 'float32')."
+ },
+ "model_type": {
+ "label": "Tip modela detekcije objekata",
+ "description": "Tip arhitekture modela detektora (ssd, yolox, yolonas) korišten od strane nekih detektora za optimizaciju."
+ }
+ },
+ "genai": {
+ "label": "Konfiguracija generativne AI",
+ "description": "Postavke za integrirane generativne AI provajdere korišteni za generisanje opisa objekata i pregled sažetaka.",
+ "api_key": {
+ "label": "Ključ API",
+ "description": "Ključ API potreban nekim provajderima (takođe može biti postavljen putem okruženja)."
+ },
+ "base_url": {
+ "label": "Osnovna URL",
+ "description": "Osnovna URL za samohostovane ili kompatibilne provajdere (npr. instanca Ollama)."
+ },
+ "model": {
+ "label": "Model",
+ "description": "Model koji se koristi iz ponuđača za generisanje opisa ili sažetaka."
+ },
+ "provider": {
+ "label": "Ponuđač",
+ "description": "GenAI ponuđač koji se koristi (npr. ollama, gemini, openai)."
+ },
+ "roles": {
+ "label": "Uloge",
+ "description": "GenAI uloge (razgovor, opisi, ugradnje); jedan ponuđač po ulozi."
+ },
+ "provider_options": {
+ "label": "Opcije ponuđača",
+ "description": "Dodatne opcije specifične za ponuđača koje se prosleđuju klijentu GenAI."
+ },
+ "runtime_options": {
+ "label": "Opcije izvršavanja",
+ "description": "Opcije izvršavanja koje se prosleđuju ponuđaču za svaku poziv izvođenja."
+ }
+ },
+ "classification": {
+ "label": "Klasifikacija objekata",
+ "description": "Postavke modela klasifikacije koji se koriste za poboljšanje oznaka objekata ili klasifikaciju stanja.",
+ "bird": {
+ "label": "Konfiguracija klasifikacije ptica",
+ "description": "Postavke specifične za modele klasifikacije ptica.",
+ "enabled": {
+ "label": "Klasifikacija ptica",
+ "description": "Omogući ili onemogući klasifikaciju ptica."
+ },
+ "threshold": {
+ "label": "Minimalna ocjena",
+ "description": "Minimalna ocjena klasifikacije potrebna za prihvaćanje klasifikacije ptica."
+ }
+ },
+ "custom": {
+ "label": "Prilagođeni modeli klasifikacije",
+ "description": "Konfiguracija prilagođenih modela klasifikacije korištenih za objekte ili detekciju stanja.",
+ "enabled": {
+ "label": "Omogući model",
+ "description": "Omogući ili onemogući prilagođeni model klasifikacije."
+ },
+ "name": {
+ "label": "Ime modela",
+ "description": "Identifikator za prilagođeni model klasifikacije koji se koristi."
+ },
+ "threshold": {
+ "label": "Prag ocjene",
+ "description": "Prag ocjene korišten za promjenu stanja klasifikacije."
+ },
+ "save_attempts": {
+ "label": "Snimi pokušaje",
+ "description": "Koliko pokušaja klasifikacije sačuvati za korisnički sučelje nedavnih klasifikacija."
+ },
+ "object_config": {
+ "objects": {
+ "label": "Klasificiraj objekte",
+ "description": "Popis vrsta objekata za koje se izvršava klasifikacija objekata."
+ },
+ "classification_type": {
+ "label": "Vrsta klasifikacije",
+ "description": "Vrsta klasifikacije primijenjena: 'sub_label' (dodaje sub_label) ili druge podržane vrste."
+ }
+ },
+ "state_config": {
+ "cameras": {
+ "label": "Kamere za klasifikaciju",
+ "description": "Postavke za rezanje i podešavanja po kameri za izvršavanje klasifikacije stanja.",
+ "crop": {
+ "label": "Rezanje za klasifikaciju",
+ "description": "Koordinate rezanja koje se koriste za izvršavanje klasifikacije na ovoj kameri."
+ }
+ },
+ "motion": {
+ "label": "Pokreni na pokret",
+ "description": "Ako je tačno, pokrenite klasifikaciju kada se detektuje pokret unutar navedenog krova."
+ },
+ "interval": {
+ "label": "Interval klasifikacije",
+ "description": "Interval (sekunde) između periodičnih pokretanja klasifikacije za klasifikaciju stanja."
+ }
+ }
+ }
+ },
+ "camera_groups": {
+ "label": "Grupe kamera",
+ "description": "Konfiguracija imenovanih grupa kamera koje se koriste za organizaciju kamera u UI-u.",
+ "cameras": {
+ "label": "Popis kamera",
+ "description": "Niz imena kamera uključenih u ovu grupu."
+ },
+ "icon": {
+ "label": "Ikona grupe",
+ "description": "Ikona korištena za prikaz grupe kamera u UI-u."
+ },
+ "order": {
+ "label": "Redoslijed sortiranja",
+ "description": "Numerički redoslijed korišten za sortiranje grupa kamera u UI-u; veći brojevi se pojavljuju kasnije."
+ }
+ },
+ "active_profile": {
+ "label": "Aktivni profil",
+ "description": "Trenutno aktivno ime profila. Samo za runtime, ne čuva se u YAML-u."
+ },
+ "camera_mqtt": {
+ "label": "MQTT",
+ "description": "Postavke objave slika preko MQTT.",
+ "enabled": {
+ "label": "Pošalji sliku",
+ "description": "Omogući objavljivanje snimaka slika za objekte na MQTT teme za ovu kameru."
+ },
+ "timestamp": {
+ "label": "Dodaj vremensku oznaku",
+ "description": "Preklopiti vremensku oznaku na slike objavljene preko MQTT."
+ },
+ "bounding_box": {
+ "label": "Dodaj okvir",
+ "description": "Crtaj okvire na slikama objavljenim preko MQTT."
+ },
+ "crop": {
+ "label": "Iscijepi sliku",
+ "description": "Iscijepi slike objavljene preko MQTT na okvir detektiranog objekta."
+ },
+ "height": {
+ "label": "Visina slike",
+ "description": "Visina (piksela) za promjenu veličine slika objavljenih preko MQTT."
+ },
+ "required_zones": {
+ "label": "Potrebne zone",
+ "description": "Zone koje objekt mora ući da bi se slika preko MQTT objavila."
+ },
+ "quality": {
+ "label": "Kvaliteta JPEG",
+ "description": "Kvaliteta JPEG za slike objavljene preko MQTT (0-100)."
+ }
+ },
+ "camera_ui": {
+ "label": "Kamera UI",
+ "description": "Prikaz redoslijeda i vidljivosti za ovu kameru u UI. Redoslijed utječe na zadani nadzorno pločo. Za detaljniju kontrolu koristite grupe kamere.",
+ "order": {
+ "label": "Redoslijed UI",
+ "description": "Numerički redoslijed koristi se za sortiranje kamere u UI (zadani nadzorno pločo i popisi); veći brojevi pojavljuju se kasnije."
+ },
+ "dashboard": {
+ "label": "Prikaži u UI",
+ "description": "Prekidač je li ova kamera vidljiva svuda u UI Frigate. Onemogućavanje ovoga zahtijeva ručno uređivanje konfiguracije za ponovno prikazivanje ove kamere u UI."
+ }
+ }
+}
diff --git a/web/public/locales/bs/config/groups.json b/web/public/locales/bs/config/groups.json
new file mode 100644
index 0000000000..44f91e7731
--- /dev/null
+++ b/web/public/locales/bs/config/groups.json
@@ -0,0 +1,73 @@
+{
+ "audio": {
+ "global": {
+ "detection": "Globalna detekcija",
+ "sensitivity": "Globalna osjetljivost"
+ },
+ "cameras": {
+ "detection": "Detekcija",
+ "sensitivity": "Osjetljivost"
+ }
+ },
+ "timestamp_style": {
+ "global": {
+ "appearance": "Globalno izgled"
+ },
+ "cameras": {
+ "appearance": "Izgled"
+ }
+ },
+ "motion": {
+ "global": {
+ "sensitivity": "Globalna osjetljivost",
+ "algorithm": "Globalni algoritam"
+ },
+ "cameras": {
+ "sensitivity": "Osjetljivost",
+ "algorithm": "Algoritam"
+ }
+ },
+ "snapshots": {
+ "global": {
+ "display": "Globalno prikazivanje"
+ },
+ "cameras": {
+ "display": "Prikazivanje"
+ }
+ },
+ "detect": {
+ "global": {
+ "resolution": "Globalna rezolucija",
+ "tracking": "Globalno praćenje"
+ },
+ "cameras": {
+ "resolution": "Rezolucija",
+ "tracking": "Praćenje"
+ }
+ },
+ "objects": {
+ "global": {
+ "tracking": "Globalno praćenje",
+ "filtering": "Globalno filtriranje"
+ },
+ "cameras": {
+ "tracking": "Praćenje",
+ "filtering": "Filtriranje"
+ }
+ },
+ "record": {
+ "global": {
+ "retention": "Globalno zadržavanje",
+ "events": "Globalni događaji"
+ },
+ "cameras": {
+ "retention": "Zadržavanje",
+ "events": "Događaji"
+ }
+ },
+ "ffmpeg": {
+ "cameras": {
+ "cameraFfmpeg": "Argumeni FFmpeg specifični za kameru"
+ }
+ }
+}
diff --git a/web/public/locales/bs/config/validation.json b/web/public/locales/bs/config/validation.json
new file mode 100644
index 0000000000..1ed1dd7a5a
--- /dev/null
+++ b/web/public/locales/bs/config/validation.json
@@ -0,0 +1,32 @@
+{
+ "minimum": "Bar {{limit}}",
+ "maximum": "Mora biti najviše {{limit}}",
+ "exclusiveMinimum": "Mora biti veći od {{limit}}",
+ "exclusiveMaximum": "Mora biti manje od {{limit}}",
+ "minLength": "Bar {{limit}} znak(ovi)",
+ "maxLength": "Mora biti najviše {{limit}} znak(ovi)",
+ "minItems": "Mora imati bar {{limit}} stavke",
+ "maxItems": "Mora imati najviše {{limit}} stavke",
+ "pattern": "Neispravan format",
+ "required": "Ovo polje je obavezno",
+ "type": "Neispravan tip vrijednosti",
+ "enum": "Mora biti jedan od dopuštenih vrijednosti",
+ "const": "Vrijednost se ne podudara s očekivanom konstantom",
+ "uniqueItems": "Sve stavke moraju biti jedinstvene",
+ "format": "Neispravan format",
+ "additionalProperties": "Nepoznato svojstvo nije dozvoljeno",
+ "oneOf": "Mora se podudarati s točno jednim od dopuštenih shema",
+ "anyOf": "Mora se podudarati s bar jednim od dopuštenih shema",
+ "proxy": {
+ "header_map": {
+ "roleHeaderRequired": "Zaglavlje uloge je obavezno kada su konfigurirane mapiranja uloga."
+ }
+ },
+ "ffmpeg": {
+ "inputs": {
+ "rolesUnique": "Svaka uloga može biti dodijeljena samo jednom ulaznom toku.",
+ "detectRequired": "Bar jedan ulazni tok mora biti dodijeljen ulozi 'detektirati'.",
+ "hwaccelDetectOnly": "Samo ulazni tok s ulogom detektiranja može definirati argumente ubrzavanja hardvera."
+ }
+ }
+}
diff --git a/web/public/locales/bs/objects.json b/web/public/locales/bs/objects.json
new file mode 100644
index 0000000000..cbeaacdb4e
--- /dev/null
+++ b/web/public/locales/bs/objects.json
@@ -0,0 +1,125 @@
+{
+ "person": "Ljudsko bit će",
+ "bicycle": "Kolo",
+ "animal": "Životinja",
+ "dog": "Pas",
+ "bark": "Glavu",
+ "cat": "Mačka",
+ "horse": "Konj",
+ "goat": "Koza",
+ "sheep": "Ovca",
+ "bird": "Ptica",
+ "mouse": "Miš",
+ "keyboard": "Klaviatura",
+ "vehicle": "Vozilo",
+ "boat": "Brod",
+ "car": "Automobil",
+ "bus": "Autobus",
+ "motorcycle": "Motocikl",
+ "train": "Vlak",
+ "skateboard": "Skejtbord",
+ "door": "Vrata",
+ "blender": "Miksere",
+ "sink": "Lavabo",
+ "hair_dryer": "Sušilac za kosu",
+ "toothbrush": "Šetka za zube",
+ "scissors": "Škare",
+ "clock": "Sat",
+ "airplane": "Avion",
+ "traffic_light": "Svetofor",
+ "fire_hydrant": "Vatrostaničar",
+ "street_sign": "Ulični znak",
+ "stop_sign": "Znak zaustavljanja",
+ "parking_meter": "Parkirni metar",
+ "bench": "Banko",
+ "cow": "Korova",
+ "elephant": "Slon",
+ "bear": "Medvjed",
+ "zebra": "Zebra",
+ "giraffe": "Žirafa",
+ "hat": "Kaputa",
+ "backpack": "Torba",
+ "umbrella": "Kreveta",
+ "shoe": "Cizma",
+ "eye_glasses": "Očna stakla",
+ "handbag": "Ručna torba",
+ "tie": "Kremplj",
+ "suitcase": "Kufer",
+ "frisbee": "Frizbi",
+ "skis": "Ski",
+ "snowboard": "Snjegobord",
+ "sports_ball": "Sportska lopta",
+ "kite": "Let",
+ "baseball_bat": "Batsa za baseball",
+ "baseball_glove": "Rukavica za baseball",
+ "surfboard": "Surfbord",
+ "tennis_racket": "Teniski raketa",
+ "bottle": "Bocica",
+ "plate": "Ploča",
+ "wine_glass": "Vinsko čaša",
+ "cup": "Kupa",
+ "fork": "Škarpe",
+ "knife": "Nož",
+ "spoon": "Lajna",
+ "bowl": "Tanjir",
+ "banana": "Banana",
+ "apple": "Jabuka",
+ "sandwich": "Sandučić",
+ "orange": "Portakal",
+ "broccoli": "Brobkoli",
+ "carrot": "Mahunika",
+ "hot_dog": "Hot dog",
+ "pizza": "Pica",
+ "donut": "Krofna",
+ "cake": "Torta",
+ "chair": "Stolica",
+ "couch": "Divan",
+ "potted_plant": "Ukrasna biljka",
+ "bed": "Krevet",
+ "mirror": "Zrcalo",
+ "dining_table": "Stol za ručak",
+ "window": "Prozor",
+ "desk": "Radni stol",
+ "toilet": "Toalet",
+ "tv": "TV",
+ "laptop": "Laptop",
+ "remote": "Udaljeno upravljanje",
+ "cell_phone": "Mobilni telefon",
+ "microwave": "Mikrotalasna pećnica",
+ "oven": "Pećnica",
+ "toaster": "Tostera",
+ "refrigerator": "Hladnjak",
+ "book": "Knjiga",
+ "vase": "Vaza",
+ "teddy_bear": "Biberon",
+ "hair_brush": "Kosmetička četka",
+ "squirrel": "Šumski pas",
+ "deer": "Jelen",
+ "fox": "Lisica",
+ "rabbit": "Zajac",
+ "raccoon": "Rakun",
+ "robot_lawnmower": "Robotska kosilica",
+ "waste_bin": "Kanta za otpad",
+ "on_demand": "Na zahtjev",
+ "face": "Lice",
+ "license_plate": "Tablica",
+ "package": "Paket",
+ "bbq_grill": "Grill za BBQ",
+ "amazon": "Amazon",
+ "usps": "USPS",
+ "ups": "UPS",
+ "fedex": "FedEx",
+ "dhl": "DHL",
+ "an_post": "An Post",
+ "purolator": "Purolator",
+ "postnl": "PostNL",
+ "nzpost": "NZPost",
+ "postnord": "PostNord",
+ "gls": "GLS",
+ "dpd": "DPD",
+ "canada_post": "Canada Post",
+ "royal_mail": "Royal Mail",
+ "school_bus": "Školski autobus",
+ "skunk": "Mrdka",
+ "kangaroo": "Kanguru"
+}
diff --git a/web/public/locales/bs/views/chat.json b/web/public/locales/bs/views/chat.json
new file mode 100644
index 0000000000..643825a780
--- /dev/null
+++ b/web/public/locales/bs/views/chat.json
@@ -0,0 +1,46 @@
+{
+ "documentTitle": "Razgovor - Frigate",
+ "title": "Frigate razgovor",
+ "subtitle": "Vaš AI asistent za upravljanje kamerama i insighti",
+ "placeholder": "Pitajte bilo što...",
+ "error": "Nešto je pošlo po zlu. Molimo pokušajte ponovo.",
+ "processing": "Obrađivanje...",
+ "toolsUsed": "Korišteno: {{tools}}",
+ "showTools": "Prikaži alate ({{count}})",
+ "hideTools": "Sakrij alate",
+ "call": "Poziv",
+ "result": "Rezultat",
+ "arguments": "Argumenti:",
+ "response": "Odgovor:",
+ "attachment_chip_label": "{{label}} na {{camera}}",
+ "attachment_chip_remove": "Ukloni privitak",
+ "open_in_explore": "Otvori u Explore",
+ "attach_event_aria": "Prikači događaj {{eventId}}",
+ "attachment_picker_paste_label": "Ili zalijepite ID događaja",
+ "attachment_picker_attach": "Prikači",
+ "attachment_picker_placeholder": "Prikači događaj",
+ "quick_reply_find_similar": "Pronađi slične susreti",
+ "quick_reply_tell_me_more": "Recite mi više o ovome",
+ "quick_reply_when_else": "Kada je još puta vidjeno?",
+ "quick_reply_find_similar_text": "Pronađi slične susreti za ovaj.",
+ "quick_reply_tell_me_more_text": "Recite mi više o ovom.",
+ "quick_reply_when_else_text": "Kada je to još puta vidjeno?",
+ "anchor": "Referenca",
+ "similarity_score": "Sličnost",
+ "no_similar_objects_found": "Nisu pronađeni slični objekti.",
+ "semantic_search_required": "Semantička pretraga mora biti omogućena da bi se pronašli slični objekti.",
+ "send": "Pošalji",
+ "suggested_requests": "Pokušaj pitati:",
+ "starting_requests": {
+ "show_recent_events": "Prikaži nedavne događaje",
+ "show_camera_status": "Prikaži status kamere",
+ "recap": "Što se desilo dok sam bio odsutan?",
+ "watch_camera": "Pratite kameru za aktivnost"
+ },
+ "starting_requests_prompts": {
+ "show_recent_events": "Prikaži mi nedavne događaje iz posljednjeg sata",
+ "show_camera_status": "Koji je trenutni status mojih kamera?",
+ "recap": "Što se desilo dok sam bio odsutan?",
+ "watch_camera": "Pratite ulazna vrata i obavijestite me ako netko dođe"
+ }
+}
diff --git a/web/public/locales/bs/views/classificationModel.json b/web/public/locales/bs/views/classificationModel.json
new file mode 100644
index 0000000000..aa18d5a07f
--- /dev/null
+++ b/web/public/locales/bs/views/classificationModel.json
@@ -0,0 +1,205 @@
+{
+ "documentTitle": "Modeli klasifikacije - Frigate",
+ "details": {
+ "scoreInfo": "Ocjena predstavlja prosjek pouzdanosti klasifikacije kroz sve detekcije ovog objekta.",
+ "none": "Nijedan",
+ "unknown": "Nepoznato"
+ },
+ "button": {
+ "deleteClassificationAttempts": "Obriši slike klasifikacije",
+ "renameCategory": "Preimenuj klasu",
+ "deleteCategory": "Obriši klasu",
+ "deleteImages": "Obriši slike",
+ "trainModel": "Obuci model",
+ "addClassification": "Dodaj klasifikaciju",
+ "deleteModels": "Obriši modele",
+ "editModel": "Uredi model"
+ },
+ "tooltip": {
+ "trainingInProgress": "Model trenutno obučava",
+ "noNewImages": "Nema novih slika za obuku. Prvo klasificirajte više slika u skupu podataka.",
+ "noChanges": "Nema promjena u skupu podataka od posljednje obuke.",
+ "modelNotReady": "Model nije spreman za obuku"
+ },
+ "toast": {
+ "success": {
+ "deletedModel_one": "Uspješno obrisan {{count}} model",
+ "deletedModel_few": "Uspješno obrisani {{count}} modeli",
+ "deletedModel_other": "Uspješno obrisani {{count}} modeli",
+ "categorizedImage": "Uspješno klasificirana slika",
+ "reclassifiedImage": "Uspješno ponovno klasificirana slika",
+ "trainedModel": "Uspješno obučen model.",
+ "trainingModel": "Uspješno pokrenuta obuka modela.",
+ "updatedModel": "Uspješno ažurirana konfiguracija modela",
+ "renamedCategory": "Uspješno preimenovana klasa u {{name}}",
+ "deletedCategory_one": "Obrisana {{count}} klasa",
+ "deletedCategory_few": "Obrisane {{count}} klase",
+ "deletedCategory_other": "Obrisane {{count}} klase",
+ "deletedImage_one": "Izbrisana {{count}} slika",
+ "deletedImage_few": "Izbrisane {{count}} slike",
+ "deletedImage_other": "Izbrisane {{count}} slike"
+ },
+ "error": {
+ "deleteImageFailed": "Neuspješno brisanje: {{errorMessage}}",
+ "deleteCategoryFailed": "Neuspješno brisanje klase: {{errorMessage}}",
+ "deleteModelFailed": "Neuspješno brisanje modela: {{errorMessage}}",
+ "categorizeFailed": "Neuspješno kategoriziranje slike: {{errorMessage}}",
+ "trainingFailed": "Obuka modela nije uspješna. Provjerite zapise Frigate za detalje.",
+ "trainingFailedToStart": "Neuspješno pokretanje obuke modela: {{errorMessage}}",
+ "updateModelFailed": "Neuspješno ažuriranje modela: {{errorMessage}}",
+ "renameCategoryFailed": "Neuspješno preimenovanje klase: {{errorMessage}}",
+ "reclassifyFailed": "Neuspješno ponovno klasifikovanje slike: {{errorMessage}}"
+ }
+ },
+ "deleteCategory": {
+ "title": "Izbriši klasu",
+ "desc": "Sigurni li ste da želite izbrisati klasu {{name}}? Ovo će trajno izbrisati sve povezane slike i zahtijevati ponovnu obuku modela.",
+ "minClassesTitle": "Nemoguće izbrisati klasu",
+ "minClassesDesc": "Model klasifikacije mora imati najmanje 2 klase. Dodajte još jednu klasu prije brisanja ove."
+ },
+ "deleteModel": {
+ "title": "Izbriši model klasifikacije",
+ "single": "Sigurni li ste da želite izbrisati {{name}}? Ovo će trajno izbrisati sve povezane podatke uključujući slike i podatke o obuci. Ova akcija ne može biti poništena.",
+ "desc_one": "Sigurni li ste da želite izbrisati {{count}} model? Ovo će trajno izbrisati sve povezane podatke uključujući slike i podatke o obuci. Ova akcija ne može biti poništena.",
+ "desc_few": "Sigurni li ste da želite izbrisati {{count}} modele? Ovo će trajno izbrisati sve povezane podatke uključujući slike i podatke o obuci. Ova akcija ne može biti poništena.",
+ "desc_other": "Sigurni li ste da želite izbrisati {{count}} modele? Ovo će trajno izbrisati sve povezane podatke uključujući slike i podatke o obuci. Ova akcija ne može biti poništena."
+ },
+ "edit": {
+ "title": "Uredi model klasifikacije",
+ "descriptionState": "Uredi klase za ovaj model klasifikacije stanja. Promjene će zahtijevati ponovnu obuku modela.",
+ "descriptionObject": "Uredi vrstu objekta i vrstu klasifikacije za ovaj model klasifikacije objekta.",
+ "stateClassesInfo": "Napomena: Promjene klasa stanja zahtijevaju ponovnu obuku modela sa ažuriranim klasama."
+ },
+ "deleteDatasetImages": {
+ "title": "Izbriši slike skupa podataka",
+ "desc_one": "Sigurni li ste da želite izbrisati {{count}} sliku iz {{dataset}}? Ova akcija ne može biti poništena i zahtijevati će ponovnu obuku modela.",
+ "desc_few": "Sigurni li ste da želite obrisati {{count}} slike iz {{dataset}}? Ova akcija ne može se poništiti i zahtijevat će ponovno treniranje modela.",
+ "desc_other": "Sigurni li ste da želite obrisati {{count}} slike iz {{dataset}}? Ova akcija ne može se poništiti i zahtijevat će ponovno treniranje modela."
+ },
+ "deleteTrainImages": {
+ "title": "Obriši slike za treniranje",
+ "desc_one": "Sigurni li ste da želite obrisati {{count}} sliku? Ova akcija ne može se poništiti.",
+ "desc_few": "Sigurni li ste da želite obrisati {{count}} slike? Ova akcija ne može se poništiti.",
+ "desc_other": "Sigurni li ste da želite obrisati {{count}} slike? Ova akcija ne može se poništiti."
+ },
+ "renameCategory": {
+ "title": "Preimenuj klasu",
+ "desc": "Unesite novo ime za {{name}}. Za promjenu imena će vam se zahtijevati ponovno treniranje modela."
+ },
+ "description": {
+ "invalidName": "Neprihvatljivo ime. Imena mogu sadržavati samo slova, brojeve, razmake, aposrofe, donje crte i crte."
+ },
+ "train": {
+ "title": "Nedavne klasifikacije",
+ "titleShort": "Nedavno",
+ "aria": "Odaberite nedavne klasifikacije"
+ },
+ "categories": "Klase",
+ "createCategory": {
+ "new": "Stvori novu klasu"
+ },
+ "categorizeImageAs": "Klasificiraj sliku kao:",
+ "categorizeImage": "Klasificiraj sliku",
+ "reclassifyImageAs": "Ponovno klasificiraj sliku kao:",
+ "reclassifyImage": "Ponovno klasificiraj sliku",
+ "menu": {
+ "objects": "Objekti",
+ "states": "Stanja"
+ },
+ "noModels": {
+ "object": {
+ "title": "Nema modela za klasifikaciju objekata",
+ "description": "Stvorite prilagođeni model za klasifikaciju detektiranih objekata.",
+ "buttonText": "Stvori model objekta"
+ },
+ "state": {
+ "title": "Nema modela za klasifikaciju stanja",
+ "description": "Stvorite prilagođeni model za praćenje i klasifikaciju promjena stanja u određenim područjima kamere.",
+ "buttonText": "Stvori model stanja"
+ }
+ },
+ "wizard": {
+ "title": "Stvori novu klasifikaciju",
+ "steps": {
+ "nameAndDefine": "Ime i definicija",
+ "stateArea": "Područje stanja",
+ "chooseExamples": "Odaberite primjere"
+ },
+ "step1": {
+ "description": "Modeli stanja nadziraju fiksne područja kamere za promjene (npr. vrata otvorena/zatvorena). Modeli objekata dodaju klasifikacije detektiranim objektima (npr. poznati životinje, dostavljači, itd.).",
+ "name": "Ime",
+ "namePlaceholder": "Unesite ime modela...",
+ "type": "Tip",
+ "typeState": "Stanje",
+ "typeObject": "Objekt",
+ "objectLabel": "Oznaka objekta",
+ "objectLabelPlaceholder": "Odaberite vrstu objekta...",
+ "classificationType": "Vrsta klasifikacije",
+ "classificationTypeTip": "Učite više o vrstama klasifikacije",
+ "classificationTypeDesc": "Podoznake dodaju dodatni tekst oznaci objekta (npr. 'Ljudsko bit će: UPS'). Atributi su pretraživi metapodaci pohranjeni zasebno u metapodacima objekta.",
+ "classificationSubLabel": "Podoznaka",
+ "classificationAttribute": "Atribut",
+ "classes": "Klase",
+ "states": "Stanja",
+ "classesTip": "Učite više o klasama",
+ "classesStateDesc": "Definirajte različita stanja koja može imati područje kamere. Na primjer: 'otvoreno' i 'zatvoreno' za vrata garaže.",
+ "classesObjectDesc": "Definirajte različite kategorije u koje ćete klasificirati detektirane objekte. Na primjer: 'dostavljac', 'stanovnik', 'stranac' za klasifikaciju ljudi.",
+ "classPlaceholder": "Unesite ime klase...",
+ "errors": {
+ "nameRequired": "Ime modela je obavezno",
+ "nameLength": "Ime modela mora imati 64 znaka ili manje",
+ "nameOnlyNumbers": "Ime modela ne može sadržavati samo brojeve",
+ "classRequired": "Potrebna je bar jedna klasa",
+ "classesUnique": "Imena klasa moraju biti jedinstvena",
+ "noneNotAllowed": "Klasa 'none' nije dozvoljena",
+ "stateRequiresTwoClasses": "Modeli stanja zahtijevaju bar dvije klase",
+ "objectLabelRequired": "Molimo odaberite oznaku objekta",
+ "objectTypeRequired": "Molimo odaberite vrstu klasifikacije"
+ }
+ },
+ "step2": {
+ "description": "Odaberite kamere i definirajte područje koje ćete nadzirati za svaku kameru. Model će klasificirati stanje ovih područja.",
+ "cameras": "Kamere",
+ "selectCamera": "Odaberite Kameru",
+ "noCameras": "Kliknite + za dodavanje kamera",
+ "selectCameraPrompt": "Odaberite kameru iz popisa da biste definirali njezino područje nadzora"
+ },
+ "step3": {
+ "selectImagesPrompt": "Odaberite sve slike s: {{className}}",
+ "selectImagesDescription": "Kliknite na slike da biste ih odabrali. Kliknite Nadalje kada završite s ovom klasifikacijom.",
+ "allImagesRequired_one": "Molimo klasificirajte sve slike. Preostala je {{count}} slika.",
+ "allImagesRequired_few": "Molimo klasificirajte sve slike. Preostale su {{count}} slike.",
+ "allImagesRequired_other": "Molimo klasificirajte sve slike. Preostale su {{count}} slike.",
+ "generating": {
+ "title": "Generisanje uzoraka slika",
+ "description": "Frigate učitava reprezentativne slike iz vaših snimaka. Ovo može trajati trenutak..."
+ },
+ "training": {
+ "title": "Obučavanje modela",
+ "description": "Vaš model se trenutno obučava u pozadini. Zatvorite ovaj dijalog, a vaš model će započeti raditi odmah kada se obuka završi."
+ },
+ "retryGenerate": "Ponovno generisanje",
+ "noImages": "Nema generisanih uzoraka slika",
+ "classifying": "Klasifikacija i obuka...",
+ "trainingStarted": "Obuka je uspješno pokrenuta",
+ "modelCreated": "Model je uspješno stvoren. Koristite pogled Najnovije klasifikacije da dodate slike za nedostajuće stanja, a zatim obučite model.",
+ "errors": {
+ "noCameras": "Nema konfigurisanih kamera",
+ "noObjectLabel": "Nije odabrana oznaka objekta",
+ "generateFailed": "Neuspješno generisanje primera: {{error}}",
+ "generationFailed": "Generisanje nije uspješno. Molimo pokušajte ponovo.",
+ "classifyFailed": "Neuspješna klasifikacija slika: {{error}}"
+ },
+ "generateSuccess": "Uspješno generisane uzorak slike",
+ "refreshExamples": "Generiši nove primjere",
+ "refreshConfirm": {
+ "title": "Generiši nove primjere?",
+ "description": "Ovo će generisati novi skup slika i obrisati sve odabire, uključujući prethodne klase. Trebat će vam ponovno odabrati primjere za sve klase."
+ },
+ "missingStatesWarning": {
+ "title": "Primjeri nedostajućih klasa",
+ "description": "Nisu sve klase imale primjere. Pokušajte generisanje novih primjera da biste pronašli nedostajuću klasu, ili nastavite i koristite pogled Najnovije klasifikacije da biste kasnije dodali slike."
+ }
+ }
+ }
+}
diff --git a/web/public/locales/bs/views/configEditor.json b/web/public/locales/bs/views/configEditor.json
new file mode 100644
index 0000000000..7ce01001ff
--- /dev/null
+++ b/web/public/locales/bs/views/configEditor.json
@@ -0,0 +1,18 @@
+{
+ "documentTitle": "Uređivač konfiguracije - Frigate",
+ "configEditor": "Uređivač konfiguracije",
+ "safeConfigEditor": "Uređivač konfiguracije (Sigurnosni režim)",
+ "safeModeDescription": "Frigate je u sigurnosnom režimu zbog greške u validaciji konfiguracije.",
+ "copyConfig": "Kopiraj konfiguraciju",
+ "saveAndRestart": "Sačuvaj i ponovo pokreni",
+ "saveOnly": "Sačuvaj samo",
+ "confirm": "Napusti bez čuvanja?",
+ "toast": {
+ "success": {
+ "copyToClipboard": "Konfiguracija kopirana u međuspremnik."
+ },
+ "error": {
+ "savingError": "Greška prilikom čuvanja konfiguracije"
+ }
+ }
+}
diff --git a/web/public/locales/bs/views/events.json b/web/public/locales/bs/views/events.json
new file mode 100644
index 0000000000..19869a820c
--- /dev/null
+++ b/web/public/locales/bs/views/events.json
@@ -0,0 +1,92 @@
+{
+ "alerts": "Upozorenja",
+ "detections": "Detekcije",
+ "camera": "Kamera",
+ "motion": {
+ "label": "Kretanje",
+ "only": "Samo pokret"
+ },
+ "allCameras": "Sve Kamere",
+ "empty": {
+ "alert": "Nema upozorenja za pregled",
+ "detection": "Nema detekcija za pregled",
+ "motion": "Nema podataka o pokretu",
+ "recordingsDisabled": {
+ "title": "Snimci moraju biti omogućeni",
+ "description": "Pregledni stavci mogu se stvarati samo za kameru kada su snimci omogućeni za tu kameru."
+ }
+ },
+ "timeline": {
+ "label": "vremenska linija",
+ "aria": "Odaberite vremensku liniju"
+ },
+ "zoomIn": "Uvećajte",
+ "zoomOut": "Umanjite",
+ "events": {
+ "label": "Događaji",
+ "aria": "Odaberite događaje",
+ "noFoundForTimePeriod": "Nema događaja za ovaj vremenski period."
+ },
+ "detail": {
+ "label": "Detalj",
+ "noDataFound": "Nema detaljnih podataka za pregled",
+ "aria": "Prekidač pregleda detalja",
+ "trackedObject_one": "{{count}} objekt",
+ "trackedObject_other": "{{count}} objekta",
+ "noObjectDetailData": "Nema dostupnih detaljnih podataka o objektu.",
+ "settings": "Postavke pregleda detalja",
+ "alwaysExpandActive": {
+ "title": "Uvijek proširujte aktivno",
+ "desc": "Uvijek proširite detalje objekta aktivnog stavka pregleda kada su dostupni."
+ }
+ },
+ "objectTrack": {
+ "trackedPoint": "Praćeni točka",
+ "clickToSeek": "Kliknite da biste prešli na ovo vrijeme"
+ },
+ "documentTitle": "Pregled - Frigate",
+ "recordings": {
+ "documentTitle": "Snimci - Frigate",
+ "invalidSharedLink": "Nemoguće otvoriti vezu za snimak sa vremenskom oznakom zbog greške u parsiranju.",
+ "invalidSharedCamera": "Nemoguće otvoriti vezu za snimak sa vremenskom oznakom zbog nepoznate ili neovlašćene kamere."
+ },
+ "calendarFilter": {
+ "last24Hours": "Poslednje 24 sata"
+ },
+ "markAsReviewed": "Označi kao pregledano",
+ "markTheseItemsAsReviewed": "Označi ove stavke kao pregledane",
+ "newReviewItems": {
+ "label": "Pregledaj nove stavke za pregled",
+ "button": "Nove stavke za pregled"
+ },
+ "selected_one": "{{count}} odabrano",
+ "selected_other": "{{count}} odabrano",
+ "select_all": "Sve",
+ "detected": "detektovano",
+ "normalActivity": "Normal",
+ "needsReview": "Treba pregledati",
+ "securityConcern": "Sigurnosna zabrinutost",
+ "motionSearch": {
+ "menuItem": "Pretraga kretanja",
+ "openMenu": "Opcije kamere"
+ },
+ "motionPreviews": {
+ "menuItem": "Pregledaj preglednike kretanja",
+ "title": "Preglednici kretanja: {{camera}}",
+ "mobileSettingsTitle": "Postavke preglednika kretanja",
+ "mobileSettingsDesc": "Prilagodite brzinu reprodukcije i osvetljavanje, i odaberite datum za pregled snimaka samo sa kretanjem.",
+ "dim": "Osvetljavanje",
+ "dimAria": "Prilagodite intenzitet osvetljavanja",
+ "dimDesc": "Povećajte osvetljavanje da biste povećali vidljivost područja kretanja.",
+ "speed": "Brzina",
+ "speedAria": "Odaberite brzinu reprodukcije preglednika",
+ "speedDesc": "Odaberite koliko brzo će se pregledni snimci reproducirati.",
+ "back": "Nazad",
+ "empty": "Nema pregleda dostupnih",
+ "noPreview": "Pregled nije dostupan",
+ "seekAria": "Pretражuj {{camera}} igraču do {{time}}",
+ "filter": "Filtar",
+ "filterDesc": "Odaberite područja da biste prikazali samo klipove sa kretanjem u tim područjima.",
+ "filterClear": "Očisti"
+ }
+}
diff --git a/web/public/locales/bs/views/explore.json b/web/public/locales/bs/views/explore.json
new file mode 100644
index 0000000000..8049fd879f
--- /dev/null
+++ b/web/public/locales/bs/views/explore.json
@@ -0,0 +1,267 @@
+{
+ "documentTitle": "Istraživanje - Frigate",
+ "generativeAI": "Generativna AI",
+ "exploreMore": "Istražite više {{label}} objekata",
+ "exploreIsUnavailable": {
+ "title": "Istraživanje nije dostupno",
+ "embeddingsReindexing": {
+ "context": "Istraživanje može se koristiti nakon što se reindeksiranje ugrađenih objekata završi.",
+ "startingUp": "Pokretanje…",
+ "estimatedTime": "Procijenjeno preostalo vrijeme:",
+ "finishingShortly": "Završetak uskoro",
+ "step": {
+ "thumbnailsEmbedded": "Ugrađene miniaturne slike: ",
+ "descriptionsEmbedded": "Ugrađene opise: ",
+ "trackedObjectsProcessed": "Obrađeni praćeni objekti: "
+ }
+ },
+ "downloadingModels": {
+ "context": "Frigate preuzima potrebne modele ugrađenih objekata kako bi podržao funkciju Semantičke pretrage. Ovo može trajati nekoliko minuta ovisno o brzini vaše mreže.",
+ "setup": {
+ "visionModel": "Model vida",
+ "visionModelFeatureExtractor": "Izvođač značajki modela vida",
+ "textModel": "Model teksta",
+ "textTokenizer": "Tokenizator teksta"
+ },
+ "tips": {
+ "context": "Moguće je da želite ponovno indeksirati ugrađene objekte koji se prate nakon što se modele preuzmu."
+ },
+ "error": "Dogodila se greška. Provjerite zapise Frigate."
+ }
+ },
+ "trackedObjectDetails": "Detalji praćenih objekata",
+ "type": {
+ "details": "Detalji",
+ "snapshot": "Snimak",
+ "thumbnail": "miniaturna slika",
+ "video": "Video",
+ "tracking_details": "detalji praćenja"
+ },
+ "trackingDetails": {
+ "title": "Detalji praćenja",
+ "noImageFound": "Nije pronađena slika za ovaj vremenski moment.",
+ "createObjectMask": "Kreirajte masku objekta",
+ "adjustAnnotationSettings": "Prilagodite postavke oznaka",
+ "scrollViewTips": "Kliknite da biste vidjeli važne trenutke životnog ciklusa ovog objekta.",
+ "autoTrackingTips": "Pozicije okvirnih kutija neće biti tačne za autotracking kamere.",
+ "count": "{{first}} od {{second}}",
+ "trackedPoint": "Praćena tačka",
+ "lifecycleItemDesc": {
+ "visible": "{{label}} detektovan",
+ "entered_zone": "{{label}} ušao u {{zones}}",
+ "active": "{{label}} postao aktivno",
+ "stationary": "{{label}} postao stacionarno",
+ "attribute": {
+ "faceOrLicense_plate": "{{attribute}} detektovan za {{label}}",
+ "other": "{{label}} prepoznat kao {{attribute}}"
+ },
+ "gone": "{{label}} otišao",
+ "heard": "{{label}} čujeo",
+ "external": "{{label}} detektovan",
+ "header": {
+ "zones": "Zone",
+ "ratio": "Omjer",
+ "area": "Površina",
+ "score": "Rezultat",
+ "computedScore": "Izračunata ocjena",
+ "topScore": "Najbolja ocjena",
+ "toggleAdvancedScores": "Prekidač naprednih ocjena"
+ }
+ },
+ "annotationSettings": {
+ "title": "Postavke oznaka",
+ "showAllZones": {
+ "title": "Prikaži sve zone",
+ "desc": "Uvijek prikazujte zone na okvirima gdje su objekti ušli u zonu."
+ },
+ "offset": {
+ "label": "Pomak oznaka",
+ "desc": "Ova podatka dolaze iz vaše kamere detektovane snimke, ali se preklapaju na slikama iz snimke snimke. Vjerojatno nije moguće da su dva toka savršeno sinhronizirana. Kao rezultat, okvirni kutiji i snimke neće se savršeno poklopiti. Možete koristiti ovu postavku da pomaknete oznake unaprijed ili unazad u vremenu da bi ih bolje uskladili s snimljenim snimkom.",
+ "millisecondsToOffset": "Milisekunde za pomak detektovanih oznaka. Podrazumevano: 0 ",
+ "tips": "Smanjite vrijednost ako je reprodukcija videa ispred kutija i tačaka putanje, a povećajte vrijednost ako je reprodukcija videa iza njih. Ova vrijednost može biti negativna.",
+ "toast": {
+ "success": "Pomak anotacije za {{camera}} je sačuvan u konfiguracionu datoteku."
+ }
+ }
+ },
+ "carousel": {
+ "previous": "Prethodni slajd",
+ "next": "Sljedeći slajd"
+ }
+ },
+ "details": {
+ "item": {
+ "title": "Pregled detalja stavke",
+ "desc": "Detalji stavke za pregled",
+ "button": {
+ "share": "Dijelite ovu stavku za pregled",
+ "viewInExplore": "Pregledajte u Explore"
+ },
+ "tips": {
+ "mismatch_one": "{{count}} nedostupan objekat je detektovan i uključen u ovu stavku pregleda. Ti objekti se nisu kvalifikovali kao upozorenje ili detekcija, ili su već očišćeni/obrisani.",
+ "mismatch_few": "{{count}} nedostupnih objekata je detektovano i uključeno u ovu stavku pregleda. Ti objekti se nisu kvalifikovali kao upozorenje ili detekciju, ili su već očišćeni/obrisani.",
+ "mismatch_other": "{{count}} nedostupnih objekata je detektovano i uključeno u ovu stavku pregleda. Ti objekti se nisu kvalifikovali kao upozorenje ili detekciju, ili su već očišćeni/obrisani.",
+ "hasMissingObjects": "Prilagodite svoju konfiguraciju ako želite da Frigate sačuva pratiti objekte za sljedeće oznake: {{objects}} "
+ },
+ "toast": {
+ "success": {
+ "regenerate": "Zahtjev za novi opis je poslat {{provider}}. Ovisno o brzini vašeg provajdera, novi opis može potrajati neko vrijeme da se ponovno generira.",
+ "updatedSublabel": "Uspješno ažurirana podjezika.",
+ "updatedLPR": "Uspješno ažurirana tablica.",
+ "updatedAttributes": "Uspješno ažurirana atribute.",
+ "audioTranscription": "Uspješno zahtjev za audio transkripciju. Ovisno o brzini vašeg Frigate servera, transkripcija može potrajati neko vrijeme da se završi."
+ },
+ "error": {
+ "regenerate": "Neuspješno poziv {{provider}} za novi opis: {{errorMessage}}",
+ "updatedSublabelFailed": "Neuspješno ažuriranje podjezika: {{errorMessage}}",
+ "updatedLPRFailed": "Neuspješno ažuriranje tablice: {{errorMessage}}",
+ "updatedAttributesFailed": "Neuspješno ažuriranje atribute: {{errorMessage}}",
+ "audioTranscription": "Neuspješno zahtjev za audio transkripciju: {{errorMessage}}"
+ }
+ }
+ },
+ "label": "Oznaka",
+ "editSubLabel": {
+ "title": "Uredi podjeziku",
+ "desc": "Unesite novu podjeziku za ovaj {{label}}",
+ "descNoLabel": "Unesite novu podjeziku za ovaj pratiti objekt"
+ },
+ "editLPR": {
+ "title": "Uredi tablica",
+ "desc": "Unesite novu vrijednost tablice za ovaj {{label}}",
+ "descNoLabel": "Unesite novu vrijednost tablice za ovaj praćeni objekt"
+ },
+ "editAttributes": {
+ "title": "Uredi atribute",
+ "desc": "Odaberite atribute klasifikacije za ovaj {{label}}"
+ },
+ "snapshotScore": {
+ "label": "Snimak Rezultat"
+ },
+ "topScore": {
+ "label": "Najbolji Rezultat",
+ "info": "Najbolji rezultat je najviši srednji rezultat za praćeni objekt, pa se može razlikovati od rezultata prikazanog na minijaturi rezultata pretrage."
+ },
+ "score": {
+ "label": "Rezultat"
+ },
+ "recognizedLicensePlate": "Prepoznata tablica",
+ "attributes": "Atributi klasifikacije",
+ "estimatedSpeed": "Procijenjena brzina",
+ "objects": "Objekti",
+ "camera": "Kamera",
+ "zones": "Zone",
+ "timestamp": "Vremenski pečat",
+ "button": {
+ "findSimilar": "Pronađi slične",
+ "regenerate": {
+ "title": "Regeneriraj",
+ "label": "Regeneriraj opis praćenog objekta"
+ }
+ },
+ "description": {
+ "label": "Opis",
+ "placeholder": "Opis praćenog objekta",
+ "aiTips": "Frigate neće tražiti opis od vašeg generativnog AI provajdera dok se životni vijek praćenog objekta ne završi."
+ },
+ "expandRegenerationMenu": "Proširi izbornik regeneracije",
+ "regenerateFromSnapshot": "Regeneriraj iz snimka",
+ "regenerateFromThumbnails": "Regeneriraj iz minijatura",
+ "tips": {
+ "descriptionSaved": "Uspješno sačuvan opis",
+ "saveDescriptionFailed": "Neuspješno ažuriranje opisa: {{errorMessage}}"
+ },
+ "title": {
+ "label": "Naslov"
+ },
+ "scoreInfo": "Informacije o rezultatu"
+ },
+ "itemMenu": {
+ "downloadVideo": {
+ "label": "Preuzmi video",
+ "aria": "Preuzmi video"
+ },
+ "downloadSnapshot": {
+ "label": "Preuzmi snimak",
+ "aria": "Preuzmi snimak"
+ },
+ "downloadCleanSnapshot": {
+ "label": "Preuzmi čist snimak",
+ "aria": "Preuzmi čist snimak"
+ },
+ "viewTrackingDetails": {
+ "label": "Pregledaj detalje praćenja",
+ "aria": "Prikaži detalje praćenja"
+ },
+ "findSimilar": {
+ "label": "Pronađi slične",
+ "aria": "Pronađi slične praćene objekte"
+ },
+ "addTrigger": {
+ "label": "Dodaj izazov",
+ "aria": "Dodaj izazov za ovaj praćeni objekt"
+ },
+ "audioTranscription": {
+ "label": "Transkriptiraj",
+ "aria": "Zatraži transkripciju zvuka"
+ },
+ "submitToPlus": {
+ "label": "Pošalji na Frigate+",
+ "aria": "Pošalji na Frigate Plus"
+ },
+ "viewInHistory": {
+ "label": "Pregledajte u povijesti",
+ "aria": "Pregledajte u povijesti"
+ },
+ "deleteTrackedObject": {
+ "label": "Obriši ovaj praćeni objekt"
+ },
+ "showObjectDetails": {
+ "label": "Prikaži put objekta"
+ },
+ "hideObjectDetails": {
+ "label": "Sakrij put objekta"
+ },
+ "debugReplay": {
+ "label": "Debug ponovno snimanje",
+ "aria": "Pregledaj ovaj praćeni objekt u pogledu debug ponovnog snimanja"
+ },
+ "more": {
+ "aria": "Više"
+ }
+ },
+ "dialog": {
+ "confirmDelete": {
+ "title": "Potvrdi brisanje",
+ "desc": "Brisanje ovog praćenog objekta uklanja snimak, bilo kakve sačuvane ugradnje, i sve povezane unose detalja praćenja. Snimljeni materijal ovog praćenog objekta u pogledu povijesti NEĆE biti obrisan. Sigurno li želite nastaviti?"
+ },
+ "toast": {
+ "error": "Greška prilikom brisanja ovog praćenog objekta: {{errorMessage}}"
+ }
+ },
+ "noTrackedObjects": "Nijedan praćeni objekt nije pronađen",
+ "fetchingTrackedObjectsFailed": "Greška prilikom dohvaćanja praćenih objekata: {{errorMessage}}",
+ "trackedObjectsCount_one": "{{count}} praćeni objekt ",
+ "trackedObjectsCount_few": "{{count}} praćena objekta ",
+ "trackedObjectsCount_other": "{{count}} praćena objekta ",
+ "searchResult": {
+ "tooltip": "Pronađeno {{type}} na {{confidence}}%",
+ "previousTrackedObject": "Prethodni praćeni objekt",
+ "nextTrackedObject": "Sljedeći praćeni objekt",
+ "deleteTrackedObject": {
+ "toast": {
+ "success": "Praćeni objekt je uspješno obrisan.",
+ "error": "Neuspješno brisanje praćenog objekta: {{errorMessage}}"
+ }
+ }
+ },
+ "aiAnalysis": {
+ "title": "Analiza AI"
+ },
+ "concerns": {
+ "label": "Pitanja"
+ },
+ "objectLifecycle": {
+ "noImageFound": "Nije pronađena slika za ovaj praćeni objekt."
+ }
+}
diff --git a/web/public/locales/bs/views/exports.json b/web/public/locales/bs/views/exports.json
new file mode 100644
index 0000000000..b04ece10b0
--- /dev/null
+++ b/web/public/locales/bs/views/exports.json
@@ -0,0 +1,128 @@
+{
+ "search": "Pretraga",
+ "documentTitle": "Izvoz - Frigate",
+ "selected_one": "{{count}} odabrano",
+ "selected_other": "{{count}} odabrano",
+ "noExports": "Nijedan izvoz nije pronađen",
+ "headings": {
+ "cases": "Slučajevi",
+ "uncategorizedExports": "Nekategorizirani izvozi"
+ },
+ "deleteExport": {
+ "label": "Obriši izvoz",
+ "desc": "Da li ste sigurni da želite da obrišete {{exportName}}?"
+ },
+ "editExport": {
+ "title": "Preimenuj izvoz",
+ "desc": "Unesite novi naziv za ovaj izvoz.",
+ "saveExport": "Sačuvaj izvoz"
+ },
+ "tooltip": {
+ "shareExport": "Dijeli izvoz",
+ "downloadVideo": "Preuzmi video",
+ "editName": "Uredi naziv",
+ "deleteExport": "Obriši izvoz",
+ "assignToCase": "Dodaj u slučaj",
+ "removeFromCase": "Ukloni iz slučaja"
+ },
+ "toolbar": {
+ "newCase": "Novi slučaj",
+ "addExport": "Dodaj izvoz",
+ "editCase": "Uredi slučaj",
+ "deleteCase": "Obriši slučaj"
+ },
+ "toast": {
+ "error": {
+ "renameExportFailed": "Neuspješno preimenovanje izvoza: {{errorMessage}}",
+ "assignCaseFailed": "Neuspješno ažuriranje dodjele slučaja: {{errorMessage}}",
+ "caseSaveFailed": "Neuspješno čuvanje slučaja: {{errorMessage}}",
+ "caseDeleteFailed": "Neuspješno brisanje slučaja: {{errorMessage}}"
+ }
+ },
+ "deleteCase": {
+ "label": "Obriši slučaj",
+ "desc": "Da li ste sigurni da želite da obrišete {{caseName}}?",
+ "descKeepExports": "Izvozi će ostati dostupni kao nekategorizirani izvozi.",
+ "descDeleteExports": "Svi izvozi u ovom slučaju trajno će biti obrisani.",
+ "deleteExports": "Takođe izbriši izvoze"
+ },
+ "caseDialog": {
+ "title": "Dodaj u slučaj",
+ "description": "Odaberite postojeći slučaj ili napravite novi.",
+ "selectLabel": "Slučaj",
+ "newCaseOption": "Napravite novi slučaj",
+ "nameLabel": "Ime slučaja",
+ "descriptionLabel": "Opis"
+ },
+ "caseCard": {
+ "emptyCase": "Nema još izvoza"
+ },
+ "jobCard": {
+ "defaultName": "{{camera}} izvoz",
+ "queued": "U redu",
+ "running": "Pokretanje",
+ "preparing": "Priprema",
+ "copying": "Kopiranje",
+ "encoding": "Kodiranje",
+ "encodingRetry": "Kodiranje (ponovi)",
+ "finalizing": "Završavanje"
+ },
+ "caseView": {
+ "noDescription": "Nema opisa",
+ "createdAt": "Kreirano {{value}}",
+ "exportCount_one": "1 izvoz",
+ "exportCount_other": "{{count}} izvozi",
+ "cameraCount_one": "1 kamera",
+ "cameraCount_other": "{{count}} kamere",
+ "showMore": "Prikaži više",
+ "showLess": "Prikaži manje",
+ "emptyTitle": "Ovaj slučaj je prazan",
+ "emptyDescription": "Dodaj postojet će nekategorizirane izvoze kako bi slučaj ostao organizovan.",
+ "emptyDescriptionNoExports": "Nema dostupnih nekategoriziranih izvoza koje je moguće dodati još."
+ },
+ "caseEditor": {
+ "createTitle": "Kreiraj slučaj",
+ "editTitle": "Uredi slučaj",
+ "namePlaceholder": "Ime slučaja",
+ "descriptionPlaceholder": "Dodaj napomene ili kontekst za ovaj slučaj"
+ },
+ "addExportDialog": {
+ "title": "Dodaj izvoz u {{caseName}}",
+ "searchPlaceholder": "Pretraga nekategoriziranih izvoza",
+ "empty": "Nema nekategoriziranih izvoza koji odgovaraju ovoj pretrazi.",
+ "addButton_one": "Dodaj 1 izvoz",
+ "addButton_other": "Dodaj {{count}} izvoza",
+ "adding": "Dodavanje..."
+ },
+ "bulkActions": {
+ "addToCase": "Dodaj u slučaj",
+ "moveToCase": "Premjesti u slučaj",
+ "removeFromCase": "Ukloni iz slučaja",
+ "delete": "Obriši",
+ "deleteNow": "Obriši sada"
+ },
+ "bulkDelete": {
+ "title": "Obriši izvoze",
+ "desc_one": "Sigurni li ste da želite obrisati {{count}} izvoz?",
+ "desc_other": "Sigurni li ste da želite obrisati {{count}} izvoze?"
+ },
+ "bulkRemoveFromCase": {
+ "title": "Ukloni iz slučaja",
+ "desc_one": "Ukloni {{count}} izvoz iz ovog slučaja?",
+ "desc_other": "Ukloni {{count}} izvoze iz ovog slučaja?",
+ "descKeepExports": "Izvozi će biti premješteni u nekategorizirane.",
+ "descDeleteExports": "Izvozi će biti trajno obrisani.",
+ "deleteExports": "Umjesto toga, obriši izvoze"
+ },
+ "bulkToast": {
+ "success": {
+ "delete": "Uspješno obrisani izvozi",
+ "reassign": "Uspješno ažurirana dodjela slučaja",
+ "remove": "Uspješno uklonjeni izvozi iz slučaja"
+ },
+ "error": {
+ "deleteFailed": "Neuspješno brisanje izvoza: {{errorMessage}}",
+ "reassignFailed": "Neuspješno ažuriranje dodjele slučaja: {{errorMessage}}"
+ }
+ }
+}
diff --git a/web/public/locales/bs/views/faceLibrary.json b/web/public/locales/bs/views/faceLibrary.json
new file mode 100644
index 0000000000..db74fe1f56
--- /dev/null
+++ b/web/public/locales/bs/views/faceLibrary.json
@@ -0,0 +1,98 @@
+{
+ "description": {
+ "addFace": "Dodajte novu kolekciju u Biblioteku lica prema učitavanju svoje prve slike.",
+ "placeholder": "Unesite ime za ovu kolekciju",
+ "invalidName": "Neprihvatljivo ime. Imena mogu sadržavati samo slova, brojeve, razmake, aposrofe, donje crte i crte.",
+ "nameCannotContainHash": "Ime ne može sadržavati #."
+ },
+ "details": {
+ "unknown": "Nepoznato",
+ "timestamp": "Vremenski pečat",
+ "scoreInfo": "Ocjena je težinski prosjek svih ocjena lica, težinski određen prema veličini lica u svakoj slici."
+ },
+ "train": {
+ "titleShort": "Nedavno",
+ "title": "Najnovije prepoznavanja",
+ "aria": "Odaberite nedavna prepoznavanja",
+ "empty": "Nema nedavnih pokušaja prepoznavanja lica"
+ },
+ "documentTitle": "Biblioteka lica - Frigate",
+ "uploadFaceImage": {
+ "title": "Učitajte sliku lica",
+ "desc": "Učitajte sliku za skeniranje lica i uključite za {{pageToggle}}"
+ },
+ "collections": "Kolekcije",
+ "createFaceLibrary": {
+ "new": "Stvori novo lice",
+ "nextSteps": "Da biste izgradili čvrstu osnovu:Koristite karticu Najnovije prepoznavanja da biste odabrali i trenirali se na slikama za svaku detektiranu osobu. Fokusirajte se na slike iz pravog ugla za najbolje rezultate; izbjegavajte slike za treniranje koje prikazuju lica pod uglom. "
+ },
+ "steps": {
+ "faceName": "Unesite ime lica",
+ "uploadFace": "Učitajte sliku lica",
+ "nextSteps": "Sljedeći koraci",
+ "description": {
+ "uploadFace": "Učitajte sliku od {{name}} koja prikazuje njihovo lice iz pravog ugla. Slika ne mora biti izrezana samo na njihovo lice."
+ }
+ },
+ "deleteFaceLibrary": {
+ "title": "Izbrišite ime",
+ "desc": "Da li ste sigurni da želite izbrisati kolekciju {{name}}? Ovo će trajno izbrisati sva povezana lica."
+ },
+ "deleteFaceAttempts": {
+ "title": "Izbrišite lica",
+ "desc_one": "Da li ste sigurni da želite izbrisati {{count}} lice? Ova akcija ne može se poništiti.",
+ "desc_few": "Da li ste sigurni da želite izbrisati {{count}} lica? Ova akcija ne može se poništiti.",
+ "desc_other": "Da li ste sigurni da želite izbrisati {{count}} lica? Ova akcija ne može se poništiti."
+ },
+ "renameFace": {
+ "title": "Preimenujte lice",
+ "desc": "Unesite novo ime za {{name}}"
+ },
+ "button": {
+ "deleteFaceAttempts": "Izbrišite lica",
+ "addFace": "Dodaj lice",
+ "renameFace": "Preimenuj lice",
+ "deleteFace": "Obriši lice",
+ "uploadImage": "Prenesi sliku",
+ "reprocessFace": "Ponovno obradi lice"
+ },
+ "imageEntry": {
+ "validation": {
+ "selectImage": "Molimo izaberite datoteku slike."
+ },
+ "dropActive": "Pustite sliku ovdje…",
+ "dropInstructions": "Povucite i ispišite, zalijepite sliku ovdje ili kliknite za odabir",
+ "maxSize": "Maksimalna veličina: {{size}}MB"
+ },
+ "nofaces": "Nema dostupnih lica",
+ "trainFaceAs": "Obuči lice kao:",
+ "trainFace": "Obuči lice",
+ "reclassifyFaceAs": "Ponovno klasificiraj lice kao:",
+ "reclassifyFace": "Ponovno klasificiraj lice",
+ "toast": {
+ "success": {
+ "uploadedImage": "Uspješno prenesena slika.",
+ "addFaceLibrary": "{{name}} je uspješno dodan u biblioteku lica!",
+ "deletedFace_one": "Uspješno obrisano {{count}} lice.",
+ "deletedFace_few": "Uspješno obrisana {{count}} lica.",
+ "deletedFace_other": "Uspješno obrisana {{count}} lica.",
+ "deletedName_one": "{{count}} lice je uspješno obrisano.",
+ "deletedName_few": "{{count}} lica su uspješno obrisana.",
+ "deletedName_other": "{{count}} lica su uspješno obrisana.",
+ "renamedFace": "Uspješno preimenovan lice na {{name}}",
+ "trainedFace": "Uspješno obučeno lice.",
+ "reclassifiedFace": "Uspješno ponovno klasificirano lice.",
+ "updatedFaceScore": "Uspješno ažurirana ocjena lica na {{name}} ({{score}})."
+ },
+ "error": {
+ "uploadingImageFailed": "Nije uspješno prenijeti sliku: {{errorMessage}}",
+ "addFaceLibraryFailed": "Nije uspješno postaviti ime lica: {{errorMessage}}",
+ "deleteFaceFailed": "Neuspješno brisanje: {{errorMessage}}",
+ "deleteNameFailed": "Nije uspješno obrisati ime: {{errorMessage}}",
+ "renameFaceFailed": "Nije uspješno preimenovati lice: {{errorMessage}}",
+ "trainFailed": "Nije uspješno trenirati: {{errorMessage}}",
+ "reclassifyFailed": "Nije uspješno ponovno klasifikovati lice: {{errorMessage}}",
+ "updateFaceScoreFailed": "Nije uspješno ažurirati bodove lica: {{errorMessage}}"
+ }
+ }
+}
diff --git a/web/public/locales/bs/views/live.json b/web/public/locales/bs/views/live.json
new file mode 100644
index 0000000000..e4fa6735df
--- /dev/null
+++ b/web/public/locales/bs/views/live.json
@@ -0,0 +1,199 @@
+{
+ "documentTitle": {
+ "default": "Uživo - Frigate",
+ "withCamera": "{{camera}} - Uživo - Frigate"
+ },
+ "lowBandwidthMode": "Nizopojasni režim",
+ "twoWayTalk": {
+ "enable": "Omogući dvostrani razgovor",
+ "disable": "Onemogući dvostrani razgovor"
+ },
+ "cameraAudio": {
+ "enable": "Omogući zvuk kamere",
+ "disable": "Onemogući zvuk kamere"
+ },
+ "ptz": {
+ "move": {
+ "clickMove": {
+ "label": "Kliknite unutar okvira da biste centrirali kameru",
+ "enable": "Omogući klik za pomak",
+ "enableWithZoom": "Omogući klik za pomak / povucite za uvećanje",
+ "disable": "Onemogući klik za pomak"
+ },
+ "left": {
+ "label": "Pomaknite PTZ kameru ulevo"
+ },
+ "up": {
+ "label": "Pomaknite PTZ kameru gore"
+ },
+ "down": {
+ "label": "Pomaknite PTZ kameru dolje"
+ },
+ "right": {
+ "label": "Pomaknite PTZ kameru udesno"
+ }
+ },
+ "zoom": {
+ "in": {
+ "label": "Uvećajte PTZ kameru"
+ },
+ "out": {
+ "label": "Umanjite PTZ kameru"
+ }
+ },
+ "focus": {
+ "in": {
+ "label": "Fokusirajte PTZ kameru unapred"
+ },
+ "out": {
+ "label": "Fokusirajte PTZ kameru unazad"
+ }
+ },
+ "frame": {
+ "center": {
+ "label": "Kliknite unutar okvira da biste centrirali PTZ kameru"
+ }
+ },
+ "presets": "Preseti PTZ kamere"
+ },
+ "camera": {
+ "enable": "Omogući kameru",
+ "disable": "Onemogući kameru"
+ },
+ "muteCameras": {
+ "enable": "Utišajte sve kamere",
+ "disable": "Ponovo uključite zvuk za sve kamere"
+ },
+ "detect": {
+ "enable": "Omogući detekciju",
+ "disable": "Onemogući detekciju"
+ },
+ "recording": {
+ "enable": "Omogući snimanje",
+ "disable": "Onemogući snimanje"
+ },
+ "snapshots": {
+ "enable": "Omogući snimke",
+ "disable": "Onemogući snimke"
+ },
+ "snapshot": {
+ "takeSnapshot": "Preuzmi trenutni snimak",
+ "noVideoSource": "Nema dostupnog video izvora za snimak.",
+ "captureFailed": "Neuspješno snimanje trenutnog snimka.",
+ "downloadStarted": "Preuzimanje trenutnog snimka započeto."
+ },
+ "audioDetect": {
+ "enable": "Omogući detekciju zvuka",
+ "disable": "Onemogući detekciju zvuka"
+ },
+ "transcription": {
+ "enable": "Omogući prepoznavanje zvuka uživo",
+ "disable": "Onemogući prepoznavanje zvuka uživo"
+ },
+ "autotracking": {
+ "enable": "Omogući automatsko praćenje",
+ "disable": "Onemogući automatsko praćenje"
+ },
+ "streamStats": {
+ "enable": "Prikaži statistiku prijenosa",
+ "disable": "Sakrij statistiku prijenosa"
+ },
+ "manualRecording": {
+ "title": "Na zahtjev",
+ "tips": "Preuzmi trenutni snimak ili pokreni ručni događaj na temelju postavki trajanja snimanja ove kamere.",
+ "playInBackground": {
+ "label": "Ponovno postavi stream",
+ "desc": "Omogući ovu opciju da nastavi streamanje kada je pokazivač sakriven."
+ },
+ "showStats": {
+ "label": "Prikaži statistiku",
+ "desc": "Omogući ovu opciju da prikaže statistiku prijenosa kao preklapanje na toku kamere."
+ },
+ "debugView": "Pregled za otklanjanje grešaka",
+ "start": "Počni snimanje na zahtjev",
+ "started": "Pokrenuto ručno snimanje na zahtjev.",
+ "failedToStart": "Neuspješno pokretanje ručnog snimanja na zahtjev.",
+ "recordDisabledTips": "Kako je snimanje onemogućeno ili ograničeno u konfiguraciji za ovu kameru, spremat će se samo snimak.",
+ "end": "Završi snimanje na zahtjev",
+ "ended": "Završeno ručno snimanje na zahtjev.",
+ "failedToEnd": "Neuspješno završavanje ručnog snimanja na zahtjev."
+ },
+ "streamingSettings": "Postavke streamanja",
+ "notifications": "Obavještenja",
+ "audio": "Audio",
+ "suspend": {
+ "forTime": "Pauziraj za: "
+ },
+ "stream": {
+ "title": "Tok",
+ "audio": {
+ "tips": {
+ "title": "Audio mora biti izlaz iz vaše kamere i konfiguriran u go2rtc za ovaj stream."
+ },
+ "available": "Audio je dostupan za ovaj stream",
+ "unavailable": "Audio nije dostupan za ovaj stream"
+ },
+ "debug": {
+ "picker": "Izbor streama nije dostupan u režimu debuga. Pregled debuga uvijek koristi stream dodeljen ulozi detekcije."
+ },
+ "twoWayTalk": {
+ "tips": "Vaš uređaj mora podržavati funkciju, a WebRTC mora biti konfiguriran za dvosmernu komunikaciju.",
+ "available": "Dvosmerna komunikacija je dostupna za ovaj stream",
+ "unavailable": "Dvosmerna komunikacija nije dostupna za ovaj stream"
+ },
+ "lowBandwidth": {
+ "tips": "Živo prikazivanje je u režimu niske propusnosti zbog buferiranja ili grešaka u streamu.",
+ "resetStream": "Ponovno postavi stream"
+ },
+ "playInBackground": {
+ "label": "Ponovno postavi stream",
+ "tips": "Omogući ovu opciju da nastavi streamanje kada je pokazivač sakriven."
+ }
+ },
+ "cameraSettings": {
+ "title": "{{camera}} Postavke",
+ "cameraEnabled": "Kamera omogućena",
+ "objectDetection": "Detekcija objekata",
+ "recording": "Snimanje",
+ "snapshots": "Snimci",
+ "audioDetection": "Detekcija zvuka",
+ "transcription": "Transkripcija zvuka",
+ "autotracking": "Autotračenje"
+ },
+ "history": {
+ "label": "Prikaži povijesne snimke"
+ },
+ "effectiveRetainMode": {
+ "modes": {
+ "all": "Sve",
+ "motion": "Kretanje",
+ "active_objects": "Aktivni objekti"
+ }
+ },
+ "editLayout": {
+ "label": "Uredi raspored",
+ "group": {
+ "label": "Uredi grupu kamera"
+ },
+ "exitEdit": "Izađi iz uređivanja"
+ },
+ "noCameras": {
+ "title": "Nema konfiguriranih kamera",
+ "description": "Počnite tako što ćete povezati kameru s Frigate.",
+ "buttonText": "Dodaj kameru",
+ "restricted": {
+ "title": "Nema dostupnih kamera",
+ "description": "Nemate dozvolu za pregled bilo koje kamere u ovoj grupi."
+ },
+ "default": {
+ "title": "Nema konfiguriranih kamera",
+ "description": "Počnite tako što ćete povezati kameru s Frigate.",
+ "buttonText": "Dodaj kameru"
+ },
+ "group": {
+ "title": "Nema kamera u grupi",
+ "description": "Ova grupa kamera nema dodeljene ili omogućene kamere.",
+ "buttonText": "Upravljajte grupama"
+ }
+ }
+}
diff --git a/web/public/locales/bs/views/motionSearch.json b/web/public/locales/bs/views/motionSearch.json
new file mode 100644
index 0000000000..f9e417fd19
--- /dev/null
+++ b/web/public/locales/bs/views/motionSearch.json
@@ -0,0 +1,77 @@
+{
+ "documentTitle": "Pretraga pokreta - Frigate",
+ "title": "Pretraga pokreta",
+ "description": "Nacrtaj poligon da biste definirali regiju interesa, a zatim navedite vremenski raspon za pretragu promjena pokreta unutar te regije.",
+ "selectCamera": "Pretraga pokreta učitava se",
+ "startSearch": "Počni pretragu",
+ "searchStarted": "Pretraga započeta",
+ "searchCancelled": "Pretraga otkazana",
+ "cancelSearch": "Otkaži",
+ "searching": "Pretraga u toku.",
+ "searchComplete": "Pretraga završena",
+ "noResultsYet": "Pokrenite pretragu da biste pronašli promjene pokreta u odabranoj regiji",
+ "noChangesFound": "Nisu otkrivene promjene piksela u odabranoj regiji",
+ "changesFound_one": "Pronađeno {{count}} promjena pokreta",
+ "changesFound_few": "Pronađeno {{count}} nekoliko pokreta",
+ "changesFound_other": "Pronađeno {{count}} promjene pokreta",
+ "framesProcessed": "{{count}} okvir procesiran",
+ "jumpToTime": "Preskoči na ovo vrijeme",
+ "results": "Rezultati",
+ "showSegmentHeatmap": "Top mapa",
+ "newSearch": "Nova pretraga",
+ "clearResults": "Očisti rezultate",
+ "clearROI": "Očisti poligon",
+ "polygonControls": {
+ "points_one": "{{count}} tačka",
+ "points_few": "{{count}} tačke",
+ "points_other": "{{count}} tačke",
+ "undo": "Poništi posljednju tačku",
+ "reset": "Ponovi poligon"
+ },
+ "motionHeatmapLabel": "Top mapa pokreta",
+ "dialog": {
+ "title": "Pretraga pokreta",
+ "cameraLabel": "Kamera",
+ "previewAlt": "Pregled kamere za {{camera}}"
+ },
+ "timeRange": {
+ "title": "Opseg pretrage",
+ "start": "Početno vrijeme",
+ "end": "Krajnje vrijeme"
+ },
+ "settings": {
+ "title": "Postavke pretrage",
+ "parallelMode": "Paralelni način",
+ "parallelModeDesc": "Skeniranje više segmenata snimaka istovremeno (brže, ali značajno intenzivnije za CPU)",
+ "threshold": "Praga osjetljivosti",
+ "thresholdDesc": "Niže vrijednosti detektiraju manje promjene (1-255)",
+ "minArea": "Minimalna površina promjene",
+ "minAreaDesc": "Minimalni postotak područja interesa koji mora promijeniti da bi se smatrao značajnim",
+ "frameSkip": "Preskoči okvir",
+ "frameSkipDesc": "Obrađujte svaki N-ti okvir. Postavite ovo na brzinu okvira vaše kamere da biste obradili jedan okvir po sekundi (npr. 5 za 5 FPS kameru, 30 za 30 FPS kameru). Više vrijednosti će biti brže, ali mogu propustiti kratke događaje pokreta.",
+ "maxResults": "Maksimalni rezultati",
+ "maxResultsDesc": "Zaustavi nakon ovog broja odgovarajućih vremenskih oznaka"
+ },
+ "errors": {
+ "noCamera": "Molimo odaberite kameru",
+ "noROI": "Molimo nacrtajte područje interesa",
+ "noTimeRange": "Molimo odaberite vremenski opseg",
+ "invalidTimeRange": "Krajnje vrijeme mora biti nakon početnog vremena",
+ "searchFailed": "Pretraga neuspješna: {{message}}",
+ "polygonTooSmall": "Poligon mora imati najmanje 3 točke",
+ "unknown": "Nepoznata greška"
+ },
+ "changePercentage": "{{percentage}}% promijenjeno",
+ "metrics": {
+ "title": "Metrike pretrage",
+ "segmentsScanned": "Skenirani segmenti",
+ "segmentsProcessed": "Obrađeno",
+ "segmentsSkippedInactive": "Preskočeno (bez aktivnosti)",
+ "segmentsSkippedHeatmap": "Preskočeno (bez preklapanja ROI)",
+ "fallbackFullRange": "Povratni put skeniranje cijelog opsega",
+ "framesDecoded": "Dekodirani okviri",
+ "wallTime": "Vrijeme pretrage",
+ "segmentErrors": "Greške segmenta",
+ "seconds": "{{seconds}}s"
+ }
+}
diff --git a/web/public/locales/bs/views/recording.json b/web/public/locales/bs/views/recording.json
new file mode 100644
index 0000000000..1810fadad2
--- /dev/null
+++ b/web/public/locales/bs/views/recording.json
@@ -0,0 +1,12 @@
+{
+ "filter": "Filtar",
+ "export": "Izvoz",
+ "calendar": "Kalendar",
+ "filters": "Filtari",
+ "toast": {
+ "error": {
+ "noValidTimeSelected": "Nije odabran valjan vremenski opseg",
+ "endTimeMustAfterStartTime": "Krajnje vrijeme mora biti nakon početnog vremena"
+ }
+ }
+}
diff --git a/web/public/locales/bs/views/replay.json b/web/public/locales/bs/views/replay.json
new file mode 100644
index 0000000000..0b102b53f2
--- /dev/null
+++ b/web/public/locales/bs/views/replay.json
@@ -0,0 +1,59 @@
+{
+ "title": "Debug ponavljanje",
+ "description": "Ponovno prikazivanje snimaka kamere za ispitivanje. Lista objekata prikazuje zakasnjelje sažetak detektiranih objekata, a kartica Zapisi prikazuje tok unutrašnjih poruka Frigate iz snimaka ponavljanja.",
+ "websocket_messages": "Poruke",
+ "dialog": {
+ "title": "Počni debug ponavljanje",
+ "description": "Kreiraj privremenu kameru za ponavljanje koja ponavlja povijesne snimke za ispitivanje problema detekcije i praćenja objekata. Kamera za ponavljanje će imati istu konfiguraciju detekcije kao i izvorna kamera. Odaberite vremenski raspon za početak.",
+ "camera": "Izvorna kamera",
+ "timeRange": "Vremenski opseg",
+ "preset": {
+ "1m": "Posljednja 1 minuta",
+ "5m": "Posljednje 5 minuta",
+ "timeline": "Iz vremenske linije",
+ "custom": "Prilagođeno"
+ },
+ "startButton": "Počni ponavljanje",
+ "selectFromTimeline": "Odaberite",
+ "starting": "Pokretanje ponavljanja...",
+ "startLabel": "Početak",
+ "endLabel": "Kraj",
+ "toast": {
+ "error": "Neuspješno pokretanje debug ponavljanja: {{error}}",
+ "alreadyActive": "Već postoji aktivna sesija ponavljanja",
+ "stopError": "Neuspješno zaustavljanje debug ponavljanja: {{error}}",
+ "goToReplay": "Idi na ponavljanje"
+ }
+ },
+ "page": {
+ "noSession": "Nema aktivne sesije ponavljanja",
+ "noSessionDesc": "Pokrenite debug ponavljanje iz pogleda Povijest klikom na dugme Debug Replay u alatnoj traci.",
+ "goToRecordings": "Idi na povijest",
+ "sourceCamera": "Izvorna kamera",
+ "replayCamera": "Kamera za ponavljanje",
+ "initializingReplay": "Inicijalizacija ponavljanja...",
+ "stoppingReplay": "Zaustavljanje ponavljanja...",
+ "stopReplay": "Zaustavi ponavljanje",
+ "confirmStop": {
+ "title": "Zaustavi režim ponavljanja za debagovanje?",
+ "description": "Ovo će zaustaviti sesiju ponavljanja i očistiti sve privremene podatke. Sigurni li?",
+ "confirm": "Zaustavi ponavljanje",
+ "cancel": "Otkaži"
+ },
+ "activity": "Aktivnost",
+ "objects": "Popis objekata",
+ "audioDetections": "Audio detekcije",
+ "noActivity": "Nema detektovane aktivnosti",
+ "activeTracking": "Aktivno praćenje",
+ "noActiveTracking": "Nema aktivnog praćenja",
+ "configuration": "Konfiguracija",
+ "configurationDesc": "Podesiti precizno detekciju pokreta i praćenje objekata za kameru za debagovanje ponavljanja. Promjene se ne čuvaju u datoteci konfiguracije Frigate.",
+ "preparingClip": "Pripremam klip…",
+ "preparingClipDesc": "Frigate spaja snimke za odabrani vremenski raspon. Ovo može potrajati minut za duže raspone.",
+ "startingCamera": "Pokretanje ponovnog pokretanja otklanjanja grešaka…",
+ "startError": {
+ "title": "Neuspjelo pokretanje ponovnog prikaza otklanjanja grešaka",
+ "back": "Povratak na historiju"
+ }
+ }
+}
diff --git a/web/public/locales/bs/views/search.json b/web/public/locales/bs/views/search.json
new file mode 100644
index 0000000000..f33ff1025b
--- /dev/null
+++ b/web/public/locales/bs/views/search.json
@@ -0,0 +1,73 @@
+{
+ "search": "Pretraga",
+ "button": {
+ "save": "Sačuvaj pretragu",
+ "clear": "Očisti pretragu",
+ "delete": "Obriši sačuvanu pretragu",
+ "filterInformation": "Filtrirajte informacije",
+ "filterActive": "Filtari aktivni"
+ },
+ "savedSearches": "Sačuvane pretrage",
+ "searchFor": "Pretraga za {{inputValue}}",
+ "trackedObjectId": "ID praćenog objekta",
+ "filter": {
+ "label": {
+ "cameras": "Kamere",
+ "labels": "Oznake",
+ "zones": "Zone",
+ "sub_labels": "Podoznake",
+ "attributes": "Atributi",
+ "search_type": "Tip pretrage",
+ "time_range": "Vremenski opseg",
+ "before": "Prije",
+ "after": "Nakon",
+ "min_score": "Min. bodovi",
+ "max_score": "Max. bodovi",
+ "min_speed": "Min. brzina",
+ "max_speed": "Max. brzina",
+ "recognized_license_plate": "Prepoznata tablica",
+ "has_clip": "Ima klip",
+ "has_snapshot": "Ima snimak"
+ },
+ "searchType": {
+ "thumbnail": "Minijatura",
+ "description": "Opis"
+ },
+ "toast": {
+ "error": {
+ "beforeDateBeLaterAfter": "Datum 'before' mora biti kasniji od datuma 'after'.",
+ "afterDatebeEarlierBefore": "Datum 'after' mora biti raniji od datuma 'before'.",
+ "minScoreMustBeLessOrEqualMaxScore": "Vrijednost 'min_score' mora biti manja ili jednaka vrijednosti 'max_score'.",
+ "maxScoreMustBeGreaterOrEqualMinScore": "Vrijednost 'max_score' mora biti veća ili jednaka vrijednosti 'min_score'.",
+ "minSpeedMustBeLessOrEqualMaxSpeed": "Vrijednost 'min_speed' mora biti manja ili jednaka vrijednosti 'max_speed'.",
+ "maxSpeedMustBeGreaterOrEqualMinSpeed": "Vrijednost 'max_speed' mora biti veća ili jednaka vrijednosti 'min_speed'."
+ }
+ },
+ "tips": {
+ "title": "Kako koristiti tekstualne filtere",
+ "desc": {
+ "text": "Filteri vam pomažu da sužite rezultate pretrage. Evo kako ih koristiti u polju za unos:",
+ "step1": "Unesite ime ključa filtera, zatim dvojtočku (npr. \"kamere:\").",
+ "step2": "Izaberite vrijednost iz predloga ili unesite vlastitu.",
+ "step3": "Koristite više filtera dodavanjem jednog za drugim s razmakom između.",
+ "step4": "Filteri datuma (pre: i nakon:) koriste {{DateFormat}} format.",
+ "step5": "Filter raspona vremena koristi format {{exampleTime}}.",
+ "step6": "Uklonite filtre klikom na 'x' pored njih.",
+ "exampleLabel": "Primjer:"
+ }
+ },
+ "header": {
+ "currentFilterType": "Vrijednosti filtera",
+ "noFilters": "Filtari",
+ "activeFilters": "Aktivni filteri"
+ }
+ },
+ "similaritySearch": {
+ "title": "Pretraga sličnosti",
+ "active": "Pretraga sličnosti aktivna",
+ "clear": "Očisti pretragu sličnosti"
+ },
+ "placeholder": {
+ "search": "Pretraži…"
+ }
+}
diff --git a/web/public/locales/bs/views/settings.json b/web/public/locales/bs/views/settings.json
new file mode 100644
index 0000000000..10126febb0
--- /dev/null
+++ b/web/public/locales/bs/views/settings.json
@@ -0,0 +1,1698 @@
+{
+ "documentTitle": {
+ "default": "Postavke - Frigate",
+ "authentication": "Postavke autentifikacije - Frigate",
+ "cameraManagement": "Upravljanje kamerama - Frigate",
+ "cameraReview": "Postavke pregleda kamera - Frigate",
+ "enrichments": "Postavke bogatstva - Frigate",
+ "masksAndZones": "Uređivač maski i zona - Frigate",
+ "motionTuner": "Podešavanje pokreta - Frigate",
+ "object": "Debug - Frigate",
+ "general": "Postavke korisničkog sučelja - Frigate",
+ "globalConfig": "Globalna konfiguracija - Frigate",
+ "cameraConfig": "Konfiguracija kamere - Frigate",
+ "frigatePlus": "Postavke Frigate+ - Frigate",
+ "notifications": "Postavke obavijesti - Frigate",
+ "maintenance": "Održavanje - Frigate",
+ "profiles": "Profili - Frigate"
+ },
+ "menu": {
+ "system": "Sistem",
+ "profiles": "Profili",
+ "general": "Općenito",
+ "globalConfig": "Globalna konfiguracija",
+ "integrations": "Integracije",
+ "cameras": "Konfiguracija kamere",
+ "ui": "UI",
+ "uiSettings": "Postavke korisničkog sučelja",
+ "globalDetect": "Detekcija objekata",
+ "globalRecording": "Snimanje",
+ "globalSnapshots": "Snimci",
+ "globalFfmpeg": "FFmpeg",
+ "globalMotion": "Detekcija pokreta",
+ "globalObjects": "Objekti",
+ "globalReview": "Pregled",
+ "globalAudioEvents": "Audio događaji",
+ "globalLivePlayback": "Uživo prikaz",
+ "globalTimestampStyle": "Stil vremenske oznake",
+ "systemDatabase": "Baza podataka",
+ "systemTls": "TLS",
+ "systemAuthentication": "Autentifikacija",
+ "systemNetworking": "Mrežno",
+ "systemProxy": "Proxy",
+ "systemUi": "UI",
+ "systemLogging": "Zapisi",
+ "systemEnvironmentVariables": "Okolinski varijable",
+ "systemTelemetry": "Telemetrija",
+ "systemBirdseye": "Birdseye",
+ "systemFfmpeg": "FFmpeg",
+ "systemDetectorHardware": "Hardver detektora",
+ "systemDetectionModel": "Model detekcije",
+ "systemMqtt": "MQTT",
+ "systemGo2rtcStreams": "go2rtc streams",
+ "integrationSemanticSearch": "Semantička pretraga",
+ "integrationGenerativeAi": "Generativna AI",
+ "integrationFaceRecognition": "Prepoznavanje lica",
+ "integrationLpr": "Prepoznavanje tablice za registraciju",
+ "integrationObjectClassification": "Klasifikacija objekata",
+ "integrationAudioTranscription": "Transkripcija zvuka",
+ "cameraDetect": "Detekcija objekata",
+ "cameraFfmpeg": "FFmpeg",
+ "cameraRecording": "Snimanje",
+ "cameraSnapshots": "Snimci",
+ "cameraMotion": "Detekcija pokreta",
+ "cameraObjects": "Objekti",
+ "cameraConfigReview": "Pregled",
+ "cameraAudioEvents": "Audio događaji",
+ "cameraAudioTranscription": "Transkripcija zvuka",
+ "cameraNotifications": "Obavještenja",
+ "cameraLivePlayback": "Uživo prikaz",
+ "cameraBirdseye": "Birdseye",
+ "cameraFaceRecognition": "Prepoznavanje lica",
+ "cameraLpr": "Prepoznavanje tablice za registraciju",
+ "cameraMqttConfig": "MQTT",
+ "cameraOnvif": "ONVIF",
+ "cameraUi": "Kamera UI",
+ "cameraTimestampStyle": "Stil vremenske oznake",
+ "cameraMqtt": "Kamera MQTT",
+ "cameraManagement": "Upravljanje",
+ "cameraReview": "Pregled",
+ "masksAndZones": "Maska / Zone",
+ "motionTuner": "Regulator kretanja",
+ "enrichments": "Poboljšanja",
+ "users": "Korisnici",
+ "roles": "Uloge",
+ "notifications": "Obavještenja",
+ "triggers": "Pokretači",
+ "debug": "Debug",
+ "frigateplus": "Frigate+",
+ "maintenance": "Održavanje",
+ "mediaSync": "Sinkronizacija medija",
+ "regionGrid": "Mreža regija"
+ },
+ "button": {
+ "overriddenGlobal": "Prekriveno (Globalno)",
+ "overriddenGlobalTooltip": "Ova kamera prekriva globalne postavke konfiguracije u ovom odjeljku",
+ "overriddenBaseConfig": "Prekriveno (Bazna konfiguracija)",
+ "overriddenBaseConfigTooltip": "Profil {{profile}} prekriva postavke konfiguracije u ovom odjeljku",
+ "overriddenInCameras": {
+ "label_one": "Nadjačano u {{count}} kameri",
+ "label_few": "Nadjačano u {{count}} kamere",
+ "label_other": "Nadjačano u {{count}} kamera",
+ "tooltip_one": "{{count}} kamera nadjačava vrijednosti u ovom odjeljku. Kliknite za detalje.",
+ "tooltip_few": "{{count}} kamere nadjačavaju vrijednosti u ovom odjeljku. Kliknite za detalje.",
+ "tooltip_other": "{{count}} kamera nadjačava vrijednosti u ovom odjeljku. Kliknite za detalje.",
+ "heading_one": "Ovaj globalni odjeljak ima polja koja su nadjačana u {{count}} kameri.",
+ "heading_few": "Ovaj globalni odjeljak ima polja koja su nadjačana u {{count}} kamere.",
+ "heading_other": "Ovaj globalni odjeljak ima polja koja su nadjačana u {{count}} kamera.",
+ "othersField_one": "{{count}} drugo",
+ "othersField_few": "{{count}} druga",
+ "othersField_other": "{{count}} drugih",
+ "profilePrefix": "{{profile}} profil: {{fields}}"
+ }
+ },
+ "dialog": {
+ "unsavedChanges": {
+ "title": "Imate nečuvane promjene.",
+ "desc": "Želite li da sačuvate promjene prije nego što nastavite?"
+ }
+ },
+ "saveAllPreview": {
+ "title": "Promjene koje treba sačuvati",
+ "triggerLabel": "Pregledajte čekajuće promjene",
+ "empty": "Nema čekajućih promjena.",
+ "scope": {
+ "label": "Opseg",
+ "global": "Globalni",
+ "camera": "Kamera: {{cameraName}}"
+ },
+ "profile": {
+ "label": "Profil"
+ },
+ "field": {
+ "label": "Polje"
+ },
+ "value": {
+ "label": "Nova vrijednost",
+ "reset": "Resetuj"
+ }
+ },
+ "cameraSetting": {
+ "camera": "Kamera",
+ "noCamera": "Nema kamere"
+ },
+ "general": {
+ "title": "Postavke korisničkog sučelja",
+ "liveDashboard": {
+ "title": "Uživo upravljačko sučelje",
+ "automaticLiveView": {
+ "label": "Automatski pregled uživo",
+ "desc": "Automatski pređite na pregled uživo kamere kada se detektira aktivnost. Onemogućavanje ove opcije uzrokuje da statične slike kamere na upravljačkom sučelju uživo ažuriraju se jednom na minut."
+ },
+ "playAlertVideos": {
+ "label": "Pregledajte videozapise upozorenja",
+ "desc": "Zaduženo, nedavna upozorenja na upravljačkom sučelju uživo se prikazuju kao mala petlja videa. Onemogućavanje ove opcije omogućava prikaz samo statične slike nedavnih upozorenja na ovom uređaju/pregledniku."
+ },
+ "displayCameraNames": {
+ "label": "Uvijek prikaži imena kamere",
+ "desc": "Uvijek prikaži imena kamere u čipu u pregledu uživo više kamera."
+ },
+ "liveFallbackTimeout": {
+ "label": "Vrijeme čekanja za povratno prelazak igrača uživo",
+ "desc": "Kada je visokokvalitetni tok uživo za kameru nedostupan, pređite na način s niskom širinom pojasa nakon ovog broja sekundi. Zadano: 3."
+ }
+ },
+ "storedLayouts": {
+ "title": "Pohranjene izgleda",
+ "desc": "Izgled kamere u grupi kamera može se povući/ponoviti. Položaji se pohranjuju u lokalno skladište vašeg preglednika.",
+ "clearAll": "Izbriši sve izgleda"
+ },
+ "cameraGroupStreaming": {
+ "title": "Postavke streaminga za grupu kamera",
+ "desc": "Postavke streaminga za svaku grupu kamera pohranjene su u lokalno skladište vašeg preglednika.",
+ "clearAll": "Izbriši sve postavke streaminga"
+ },
+ "recordingsViewer": {
+ "title": "Pregledač snimaka",
+ "defaultPlaybackRate": {
+ "label": "Zadani brzina reprodukcije",
+ "desc": "Zadani brzina reprodukcije za prikazivanje snimaka."
+ }
+ },
+ "calendar": {
+ "title": "Kalendar",
+ "firstWeekday": {
+ "label": "Prvi radni dan",
+ "desc": "Dan na kojem počinju tjedni pregled kalendar.",
+ "sunday": "Nedjelja",
+ "monday": "Ponedjeljak"
+ }
+ },
+ "toast": {
+ "success": {
+ "clearStoredLayout": "Izbrisana pohranjena izgled za {{cameraName}}",
+ "clearStreamingSettings": "Izbrisane postavke streaminga za sve grupe kamera."
+ },
+ "error": {
+ "clearStoredLayoutFailed": "Nije uspješno obrisano pohranjeno raspoređenje: {{errorMessage}}",
+ "clearStreamingSettingsFailed": "Nije uspješno obrisano pohranjene postavke streaminga: {{errorMessage}}"
+ }
+ }
+ },
+ "enrichments": {
+ "title": "Postavke obogaćivanja",
+ "unsavedChanges": "Nespremljene promjene postavki bogatstva",
+ "birdClassification": {
+ "title": "Klasifikacija ptica",
+ "desc": "Klasifikacija ptica identifikuje poznate ptice pomoću kvantiziranog Tensorflow modela. Kada se prepozna poznata ptica, njezino uobičajeno ime bit će dodato kao sub_label. Ova informacija uključena je u UI, filtere, kao i u obavijesti."
+ },
+ "semanticSearch": {
+ "title": "Semantička pretraga",
+ "desc": "Semantička pretraga u Frigate omogućava vam da pronađete praćene objekte unutar vaših stavki za pregled pomoću same slike, korisnički definiranog tekstualnog opisa ili automatski generiranog.",
+ "reindexNow": {
+ "label": "Ponovno indeksiraj sada",
+ "desc": "Ponovno indeksiranje će ponovno generirati ugrađivanja za sve praćene objekte. Ovaj proces se izvršava u pozadini i može iskoristiti punu snagu vašeg CPU-a i može trajati značajno vrijeme ovisno o broju praćenih objekata koje imate.",
+ "confirmTitle": "Potvrdi ponovno indeksiranje",
+ "confirmDesc": "Sigurni li ste da želite ponovno indeksirati ugrađivanja svih praćenih objekata? Ovaj proces će se izvršavati u pozadini, ali može iskoristiti punu snagu vašeg CPU-a i može trajati značajno vrijeme. Možete pratiti napredak na stranici Istraživanje.",
+ "confirmButton": "Ponovno indeksiraj",
+ "success": "Ponovno indeksiranje je uspješno pokrenuto.",
+ "alreadyInProgress": "Ponovno indeksiranje već je u toku.",
+ "error": "Nije uspješno pokrenuto ponovno indeksiranje: {{errorMessage}}"
+ },
+ "modelSize": {
+ "label": "Veličina modela",
+ "desc": "Veličina modela korištenog za semantičke pretrage ugrađivanja.",
+ "small": {
+ "title": "mali",
+ "desc": "Korištenje mali koristi kvantiziranu verziju modela koja koristi manje RAM-a i brže se izvršava na CPU-u s vrlo zanemarivim razlikama u kvaliteti ugrađivanja."
+ },
+ "large": {
+ "title": "veliki",
+ "desc": "Korištenje veliki koristi puni Jina model i automatski će se izvršavati na GPU-u ako je primjenjivo."
+ }
+ }
+ },
+ "faceRecognition": {
+ "title": "Prepoznavanje lica",
+ "desc": "Prepoznavanje lica omogućava ljudima da se dodele imena, a kada se prepozna lice, Frigate će dodijeliti ime osobe kao sub_label. Ova informacija uključena je u UI, filtere, kao i u obavijesti.",
+ "modelSize": {
+ "label": "Veličina modela",
+ "desc": "Veličina modela korištenog za prepoznavanje lica.",
+ "small": {
+ "title": "mali",
+ "desc": "Korišćenje mali koristi model za ugradnju lica FaceNet koji učinkovito radi na većini CPU-ova."
+ },
+ "large": {
+ "title": "veliki",
+ "desc": "Korišćenje veliki koristi model za ugradnju lica ArcFace i automatski će se pokrenuti na GPU-u ako je primjenjivo."
+ }
+ }
+ },
+ "licensePlateRecognition": {
+ "title": "Prepoznavanje tablice vozila",
+ "desc": "Frigate može prepoznati registracijske tablice na vozilima i automatski dodati detektirane znakove u polje recognized_license_plate ili poznato ime kao sub_label objektima koji su tipa automobil. Često korišćeni slučaj može biti čitanje registracijskih tablica automobila koji se voze u dvorište ili automobila koji prolaze ulicom."
+ },
+ "restart_required": "Potrebno je ponovno pokretanje (promijenjene postavke bogatstva)",
+ "toast": {
+ "success": "Postavke bogatstva su sačuvane. Ponovno pokrenite Frigate da biste primijenili svoje promjene.",
+ "error": "Nije uspješno sačuvana promjena konfiguracije: {{errorMessage}}"
+ }
+ },
+ "cameraWizard": {
+ "title": "Dodaj kameru",
+ "description": "Slijedite korake ispod da biste dodali novu kameru u svoju instalaciju Frigate.",
+ "steps": {
+ "nameAndConnection": "Ime i Povezivanje",
+ "probeOrSnapshot": "Testiranje ili Snimak",
+ "streamConfiguration": "Konfiguracija strujanja",
+ "validationAndTesting": "Validacija i Testiranje"
+ },
+ "save": {
+ "success": "Uspješno sačuvana nova kamera {{cameraName}}.",
+ "failure": "Greška prilikom sačuvavanja {{cameraName}}."
+ },
+ "testResultLabels": {
+ "resolution": "Rezolucija",
+ "video": "Video",
+ "audio": "Audio",
+ "fps": "FPS"
+ },
+ "commonErrors": {
+ "noUrl": "Molimo unesite važeću URL adresu za strujanje",
+ "testFailed": "Test strujanja nije uspio: {{error}}"
+ },
+ "step1": {
+ "description": "Unesite detalje o svojoj kameri i odaberite testiranje kamere ili ručno odaberite proizvođača.",
+ "cameraName": "Ime kamere",
+ "cameraNamePlaceholder": "npr. front_door ili Pregled zadnjeg dvorišta",
+ "host": "Host/IP adresa",
+ "port": "Port",
+ "username": "Korisničko ime",
+ "usernamePlaceholder": "Opcionalno",
+ "password": "Lozinka",
+ "passwordPlaceholder": "Opcionalno",
+ "selectTransport": "Odaberite protokol prenosa",
+ "cameraBrand": "Marka kamere",
+ "selectBrand": "Odaberite marku kamere za URL predložak",
+ "customUrl": "Prilagođeni URL tokova",
+ "brandInformation": "Informacije o brendu",
+ "brandUrlFormat": "Za kamere sa formatom RTSP URL-a kao: {{exampleUrl}}",
+ "customUrlPlaceholder": "rtsp://username:password@host:port/path",
+ "connectionSettings": "Postavke veze",
+ "detectionMethod": "Metoda detekcije tokova",
+ "onvifPort": "Port ONVIF",
+ "probeMode": "Pregledaj kameru",
+ "manualMode": "Ručna selekcija",
+ "detectionMethodDescription": "Pregledaj kameru pomoću ONVIF (ako je podržano) kako bi pronašao URL-ove tokova kamere, ili ručno odaberi brend kamere za korištenje preddefiniranih URL-ova. Za unos prilagođenog RTSP URL-a, odaberi ručni metod i odaberi \"Ostalo\".",
+ "onvifPortDescription": "Za kamere koje podržavaju ONVIF, ovo je obično 80 ili 8080.",
+ "useDigestAuth": "Koristi digest autentifikaciju",
+ "useDigestAuthDescription": "Koristi HTTP digest autentifikaciju za ONVIF. Neke kamere mogu zahtijevati dedikovane korisničko ime/lozinku ONVIF umjesto standardnog korisnika admin.",
+ "errors": {
+ "brandOrCustomUrlRequired": "Odaberite brend kamere s host/IP ili odaberite 'Ostalo' s prilagođenim URL-om",
+ "nameRequired": "Ime kamere je obavezno",
+ "nameLength": "Ime kamere mora imati 64 znaka ili manje",
+ "invalidCharacters": "Ime kamere sadrži nevažeće znakove",
+ "nameExists": "Ime kamere već postoji",
+ "customUrlRtspRequired": "Prilagođeni URL-ovi moraju početi s \"rtsp://\". Za tokove kamere koji nisu RTSP potrebna je ručna konfiguracija."
+ }
+ },
+ "step2": {
+ "description": "Pregledajte kameru za dostupne tokove ili konfigurirajte ručne postavke na temelju odabrane metode detekcije.",
+ "testSuccess": "Test veze uspješan!",
+ "testFailed": "Test veze nije uspio. Molimo provjerite svoj unos i pokušajte ponovno.",
+ "testFailedTitle": "Test nije uspio",
+ "streamDetails": "Detalji tokova",
+ "probing": "Pregledavanje kamere...",
+ "retry": "Pokušaj ponovno",
+ "testing": {
+ "probingMetadata": "Pregledavanje metapodataka kamere...",
+ "fetchingSnapshot": "Učitavanje snimka kamere..."
+ },
+ "probeFailed": "Neuspješno ispitivanje kamere: {{error}}",
+ "probingDevice": "Ispitivanje uređaja...",
+ "probeSuccessful": "Uspješno ispitivanje",
+ "probeError": "Greška ispitivanja",
+ "probeNoSuccess": "Neuspješno ispitivanje",
+ "deviceInfo": "Informacije o uređaju",
+ "manufacturer": "Proizvođač",
+ "model": "Model",
+ "firmware": "Firmver",
+ "profiles": "Profili",
+ "ptzSupport": "Podrška PTZ",
+ "autotrackingSupport": "Podrška za autotračenje",
+ "presets": "Podešavanja",
+ "rtspCandidates": "RTSP kandidati",
+ "rtspCandidatesDescription": "Sljedeće RTSP URL-ovi su pronađeni iz ispitivanja kamere. Testirajte vezu za pregled metapodataka o streamu.",
+ "noRtspCandidates": "Nisu pronađeni RTSP URL-ovi iz kamere. Vaše vjerodajnice mogu biti netočne, ili kamera možda ne podržava ONVIF ili metod za dobivanje RTSP URL-ova. Vrati se i ručno unesi RTSP URL.",
+ "candidateStreamTitle": "Kandidat {{number}}",
+ "useCandidate": "Koristi",
+ "uriCopy": "Kopiraj",
+ "uriCopied": "URI kopiran na međuspremnik",
+ "testConnection": "Testiraj vezu",
+ "toggleUriView": "Kliknite za prebacivanje u puni prikaz URI-ja",
+ "connected": "Povezani",
+ "notConnected": "Nije povezan",
+ "errors": {
+ "hostRequired": "Potrebna je adresa hosta/IP"
+ }
+ },
+ "step3": {
+ "description": "Konfigurirajte uloge streamova i dodajte dodatne streamove za vašu kameru.",
+ "streamsTitle": "Streamovi kamere",
+ "addStream": "Dodaj snimak",
+ "addAnotherStream": "Dodaj još jedan snimak",
+ "streamTitle": "Snimak {{number}}",
+ "streamUrl": "URL snimka",
+ "streamUrlPlaceholder": "rtsp://username:password@host:port/path",
+ "selectStream": "Odaberite snimak",
+ "searchCandidates": "Pretražite kandidate...",
+ "noStreamFound": "Nijedan snimak nije pronađen",
+ "url": "URL",
+ "resolution": "Rezolucija",
+ "selectResolution": "Odaberite rezoluciju",
+ "quality": "Kvalitet",
+ "selectQuality": "Odaberite kvalitet",
+ "roles": "Uloge",
+ "roleLabels": {
+ "detect": "Detekcija objekata",
+ "record": "Snimanje",
+ "audio": "Audio"
+ },
+ "testStream": "Testirajte vezu",
+ "testSuccess": "Test snimka uspješan!",
+ "testFailed": "Test snimka neuspješan",
+ "testFailedTitle": "Test neuspješan",
+ "connected": "Povezani",
+ "notConnected": "Nije povezan",
+ "featuresTitle": "Funkcije",
+ "go2rtc": "Smanjite povezivanje s kamerom",
+ "detectRoleWarning": "Bar jedan snimak mora imati ulogu \"detektirati\" da biste nastavili.",
+ "rolesPopover": {
+ "title": "Uloge snimka",
+ "detect": "Glavni tok za detekciju objekata.",
+ "record": "Sprema segmente video toka na temelju postavki konfiguracije.",
+ "audio": "Tok za detekciju na temelju zvuka."
+ },
+ "featuresPopover": {
+ "title": "Značajke snimka",
+ "description": "Koristite go2rtc restreaming za smanjenje povezivanja s vašom kamerom."
+ }
+ },
+ "step4": {
+ "description": "Konačna validacija i analiza prije spašavanja vaše nove kamere. Povežite svaki tok prije spašavanja.",
+ "validationTitle": "Validacija toka",
+ "connectAllStreams": "Poveži sve toke",
+ "reconnectionSuccess": "Ponovno povezivanje uspješno.",
+ "reconnectionPartial": "Neki tokovi su neuspješno ponovno povezani.",
+ "streamUnavailable": "Pregled toka nije dostupan",
+ "reload": "Ponovno učitaj",
+ "connecting": "Povezivanje...",
+ "streamTitle": "Tok {{number}}",
+ "valid": "Važeći",
+ "failed": "Neuspješno",
+ "notTested": "Nije testirano",
+ "connectStream": "Poveži",
+ "connectingStream": "Povezivanje",
+ "disconnectStream": "Odspoji",
+ "estimatedBandwidth": "Procijenjena širina pojasa",
+ "roles": "Uloge",
+ "ffmpegModule": "Koristi režim kompatibilnosti toka",
+ "ffmpegModuleDescription": "Ako tok ne učita nakon nekoliko pokušaja, pokušajte omogućavanje ovoga. Kada je omogućeno, Frigate će koristiti modul ffmpeg sa go2rtc. Ovo može pružiti bolju kompatibilnost sa nekim tokovima kamera.",
+ "none": "Nijedan",
+ "error": "Greška",
+ "streamValidated": "Tok {{number}} uspješno validiran",
+ "streamValidationFailed": "Tok {{number}} validacija neuspješna",
+ "saveAndApply": "Sačuvaj novu kameru",
+ "saveError": "Neispravna konfiguracija. Molimo provjerite svoje postavke.",
+ "issues": {
+ "title": "Validacija toka",
+ "videoCodecGood": "Video kodak je {{codec}}.",
+ "audioCodecGood": "Audio kodak je {{codec}}.",
+ "resolutionHigh": "Rezolucija od {{resolution}} može uzrokovati povećanu upotrebu resursa.",
+ "resolutionLow": "Rezolucija od {{resolution}} može biti preniska za pouzdanu detekciju malih objekata.",
+ "resolutionUnknown": "Rezolucija ovog tokova nije moguća za ispitivanje. Trebalo bi ručno postaviti detekciju rezolucije u Postavkama ili vašoj konfiguraciji.",
+ "noAudioWarning": "Nije detektovan zvuk za ovaj tok, snimci neće imati zvuk.",
+ "audioCodecRecordError": "Potreban je AAC audio kodac za podršku zvuku u snimcima.",
+ "audioCodecRequired": "Potreban je audio tok za podržavanje detekcije zvuka.",
+ "restreamingWarning": "Smanjenje povezivanja sa kamerom za snimljeni tok može lagano povećati upotrebu CPU.",
+ "brands": {
+ "reolink-rtsp": "Reolink RTSP nije preporučen. Omogući HTTP u postavkama firmvera kamere i ponovo pokreni čarobnjaka.",
+ "reolink-http": "Reolink HTTP tokovi trebaju koristiti FFmpeg za bolju kompatibilnost. Omogući 'Korištenje režima kompatibilnosti tokova' za ovaj tok."
+ },
+ "dahua": {
+ "substreamWarning": "Podstrujak 1 je zaključan na nisku rezoluciju. Mnoge kamere Dahua / Amcrest / EmpireTech podržavaju dodatne podstrujke koje treba omogućiti u postavkama kamere. Preporučuje se da ih provjerite i iskoristite ako su dostupne."
+ },
+ "hikvision": {
+ "substreamWarning": "Podstrujak 1 je zaključan na nisku rezoluciju. Mnoge kamere Hikvision podržavaju dodatne podstrujke koje treba omogućiti u postavkama kamere. Preporučuje se da ih provjerite i iskoristite ako su dostupne."
+ }
+ }
+ }
+ },
+ "cameraManagement": {
+ "title": "Upravljanje kamerama",
+ "addCamera": "Dodaj novu kameru",
+ "deleteCamera": "Obriši kameru",
+ "deleteCameraDialog": {
+ "title": "Obriši kameru",
+ "description": "Brisanje kamere trajno uklanja sve snimke, praćene objekte i konfiguraciju za tu kameru. Bilo bi potrebno ručno ukloniti sve go2rtc tokove povezane s ovom kamerom.",
+ "selectPlaceholder": "Izaberite kameru...",
+ "confirmTitle": "Sigurni ste?",
+ "confirmWarning": "Brisanje {{cameraName}} ne može se povući.",
+ "deleteExports": "Takođe obriši izvoze za ovu kameru",
+ "confirmButton": "Obriši trajno",
+ "success": "Kamera {{cameraName}} uspješno obrisana",
+ "error": "Neuspješno brisanje kamere {{cameraName}}"
+ },
+ "editCamera": "Uredi kameru:",
+ "selectCamera": "Izaberite kameru",
+ "backToSettings": "Povratak na postavke kamere",
+ "streams": {
+ "title": "Omogući / Onemogući kamere",
+ "enableLabel": "Omogućene kamere",
+ "enableDesc": "Privremeno onemogući omogućenu kameru dok Frigate ne ponovo započne. Onemogućavanje kamere potpuno zaustavlja obradu tokova ove kamere od strane Frigate. Detekcija, snimanje i praćenje nedostat će. Napomena: Ovo ne onemogućava restreamove go2rtc. ",
+ "disableLabel": "Onemogućene kamere",
+ "disableDesc": "Omogući kameru koja trenutno nije vidljiva u UI-ju i onemogućena u konfiguraciji. Potrebno je ponovno pokrenuti Frigate nakon omogućavanja.",
+ "enableSuccess": "Omogućena {{cameraName}} u konfiguraciji. Ponovno pokrenite Frigate da biste primijenili promjene.",
+ "friendlyName": {
+ "edit": "Uredi prikazano ime kamere",
+ "title": "Uredi prikazano ime",
+ "description": "Postavite prijateljsko ime koje će se prikazivati za ovu kameru kroz cijeli Frigate UI. Ostavite prazno da biste koristili ID kamere.",
+ "rename": "Preimenuj"
+ }
+ },
+ "cameraConfig": {
+ "add": "Dodaj kameru",
+ "edit": "Uredi kameru",
+ "description": "Konfiguriraj postavke kamere uključujući ulazne tokove i uloge.",
+ "name": "Ime kamere",
+ "nameRequired": "Ime kamere je obavezno",
+ "nameLength": "Ime kamere mora imati manje od 64 znaka.",
+ "namePlaceholder": "npr. front_door ili Pregled zadnjeg dvorišta",
+ "enabled": "Omogućeno",
+ "ffmpeg": {
+ "inputs": "Ulazni tokovi",
+ "path": "Putanja toka",
+ "pathRequired": "Putanja toka je obavezna",
+ "pathPlaceholder": "rtsp://...",
+ "roles": "Uloge",
+ "rolesRequired": "Potrebna je bar jedna uloga",
+ "rolesUnique": "Svaka uloga (audio, detekcija, snimanje) može se dodijeliti samo jednom toku",
+ "addInput": "Dodaj ulazni tok",
+ "removeInput": "Ukloni ulazni tok",
+ "inputsRequired": "Potrebno je bar jedan ulazni tok"
+ },
+ "go2rtcStreams": "go2rtc Tokovi",
+ "streamUrls": "URL-ovi tokova",
+ "addUrl": "Dodaj URL",
+ "addGo2rtcStream": "Dodaj go2rtc tok",
+ "toast": {
+ "success": "Kamera {{cameraName}} je uspješno sačuvana"
+ }
+ },
+ "profiles": {
+ "title": "Prekrižavanja kamere profila",
+ "selectLabel": "Odaberi profil",
+ "description": "Konfigurirajte koje kamere su omogućene ili onemogućene kada je profil aktiviran. Kamere postavljene na \"Naslijeđivanje\" očuvaju svoje osnovno stanje omogućeno.",
+ "inherit": "Naslijeđivanje",
+ "enabled": "Omogućeno",
+ "disabled": "Onemogućeno"
+ },
+ "cameraType": {
+ "title": "Tip kamere",
+ "label": "Tip kamere",
+ "description": "Postavite tip za svaku kameru. Posvećene LPR kamere su jednonamjenske kamere sa snažnim optičkim zumom za hvatanje registarskih tablica na udaljenim vozilima. Većina kamera treba koristiti normalan tip kamere, osim ako je kamera namjenski za LPR i ima usko fokusiran pogled na registarske tablice.",
+ "normal": "Normalna",
+ "dedicatedLpr": "Posvećena LPR",
+ "saveSuccess": "Tip kamere ažuriran za {{cameraName}}. Ponovo pokrenite Frigate da bi se promjene primijenile."
+ }
+ },
+ "cameraReview": {
+ "title": "Postavke pregleda kamere",
+ "object_descriptions": {
+ "title": "Opisi objekata generativne AI",
+ "desc": "Privremeno omogući/onemogući opise objekata generativne AI za ovu kameru dok Frigate ne ponovo pokrene. Kada je onemogućeno, opisi generirani AI-om neće se tražiti za praćene objekte na ovoj kameri."
+ },
+ "review_descriptions": {
+ "title": "Opisi pregleda generativne AI",
+ "desc": "Privremeno omogući/onemogući opise pregleda generativne AI za ovu kameru dok Frigate ne ponovo pokrene. Kada je onemogućeno, opisi generirani AI-om neće se tražiti za stavke pregleda na ovoj kameri."
+ },
+ "review": {
+ "title": "Pregled",
+ "desc": "Privremeno omogući/onemogući upozorenja i detekcije za ovu kameru dok Frigate ne ponovo pokrene. Kada je onemogućeno, neće se generirati nove stavke pregleda. ",
+ "alerts": "Upozorenja ",
+ "detections": "Detekcije "
+ },
+ "reviewClassification": {
+ "title": "Klasifikacija pregleda",
+ "desc": "Frigate klasificira stavke pregleda kao Upozorenja i Detekcije. Po defaultu, svi osobe i automobili objekti se smatraju Upozorenjima. Možete usavršiti klasifikaciju svojih stavki pregleda konfiguriranjem potrebnih zona za njih.",
+ "noDefinedZones": "Nema definiranih zona za ovu kameru.",
+ "objectAlertsTips": "Svi {{alertsLabels}} objekti na {{cameraName}} bit će prikazani kao Upozorenja.",
+ "zoneObjectAlertsTips": "Svi {{alertsLabels}} objekti detektirani u {{zone}} na {{cameraName}} bit će prikazani kao Upozorenja.",
+ "objectDetectionsTips": "Svi {{detectionsLabels}} objekti koji nisu kategorizirani na {{cameraName}} bit će prikazani kao Detekcije bez obzira na kojoj se zoni nalaze.",
+ "zoneObjectDetectionsTips": {
+ "text": "Svi {{detectionsLabels}} objekti koji nisu kategorizirani u {{zone}} na {{cameraName}} bit će prikazani kao Detekcije.",
+ "notSelectDetections": "Svi {{detectionsLabels}} objekti detektirani u {{zone}} na {{cameraName}} koji nisu kategorizirani kao Upozorenja bit će prikazani kao Detekcije bez obzira na kojoj se zoni nalaze.",
+ "regardlessOfZoneObjectDetectionsTips": "Svi {{detectionsLabels}} objekti koji nisu kategorizirani na {{cameraName}} bit će prikazani kao Detekcije bez obzira na koju zonu se nalaze."
+ },
+ "unsavedChanges": "Nespremljene postavke klasifikacije pregleda za {{camera}}",
+ "selectAlertsZones": "Odaberite zone za Upozorenja",
+ "selectDetectionsZones": "Odaberite zone za Detekcije",
+ "limitDetections": "Ograničite detekcije na specifične zone",
+ "toast": {
+ "success": "Konfiguracija klasifikacije pregleda je sačuvana. Ponovo pokrenite Frigate da biste primijenili promjene."
+ }
+ }
+ },
+ "masksAndZones": {
+ "filter": {
+ "all": "Svi Maski i Zone"
+ },
+ "restart_required": "Potrebno je ponovo pokrenuti (maska/zone promijenjene)",
+ "disabledInConfig": "Stavka je onemogućena u datoteci konfiguracije",
+ "addDisabledProfile": "Prvo dodajte u osnovnu konfiguraciju, zatim prekrijte u profilu",
+ "profileBase": "(osnovna)",
+ "profileOverride": "(preklop)",
+ "toast": {
+ "success": {
+ "copyCoordinates": "Koordinate za {{polyName}} su kopirane u međuspremnik."
+ },
+ "error": {
+ "copyCoordinatesFailed": "Nemoguće kopirati koordinate u međuspremnik."
+ }
+ },
+ "motionMaskLabel": "Maska za pokret {{number}}",
+ "objectMaskLabel": "Maska za objekt {{number}}",
+ "form": {
+ "id": {
+ "error": {
+ "mustNotBeEmpty": "ID ne smije biti prazan.",
+ "alreadyExists": "Postoji maska s ovim ID-om za ovu kameru."
+ }
+ },
+ "name": {
+ "error": {
+ "mustNotBeEmpty": "Ime ne smije biti prazno."
+ }
+ },
+ "zoneName": {
+ "error": {
+ "mustBeAtLeastTwoCharacters": "Ime zone mora imati najmanje 2 karaktera.",
+ "mustNotBeSameWithCamera": "Ime zone ne smije biti isto kao ime kamere.",
+ "alreadyExists": "Postoji zona s ovim imenom za ovu kameru.",
+ "mustNotContainPeriod": "Ime zone ne smije sadržavati tačke.",
+ "hasIllegalCharacter": "Ime zone sadrži nedozvoljene znakove.",
+ "mustHaveAtLeastOneLetter": "Ime zone mora imati bar jedan slovo."
+ }
+ },
+ "distance": {
+ "error": {
+ "text": "Rastojanje mora biti veće ili jednako 0.1.",
+ "mustBeFilled": "Sva polja za rastojanje moraju biti popunjena da biste koristili procjenu brzine."
+ }
+ },
+ "inertia": {
+ "error": {
+ "mustBeAboveZero": "Inercija mora biti iznad 0."
+ }
+ },
+ "loiteringTime": {
+ "error": {
+ "mustBeGreaterOrEqualZero": "Vrijeme loiteringa mora biti veće ili jednako 0."
+ }
+ },
+ "speed": {
+ "error": {
+ "mustBeGreaterOrEqualTo": "Prag brzine mora biti veći ili jednak 0.1."
+ }
+ },
+ "polygonDrawing": {
+ "type": {
+ "zone": "Zona",
+ "motion_mask": "maska pokreta",
+ "object_mask": "maska objekta"
+ },
+ "removeLastPoint": "Ukloni posljednju tačku",
+ "reset": {
+ "label": "Obriši sve tačke"
+ },
+ "snapPoints": {
+ "true": "Prilagodi tačke",
+ "false": "Ne prilagođavaj tačke"
+ },
+ "delete": {
+ "title": "Potvrdi brisanje",
+ "desc": "Da li ste sigurni da želite izbrisati {{type}} {{name}} ?",
+ "success": "{{name}} je izbrisan."
+ },
+ "revertOverride": {
+ "title": "Povrati se na osnovnu konfiguraciju",
+ "desc": "Ovo će ukloniti preklop profila za {{type}} {{name}} i povrati se na osnovnu konfiguraciju."
+ },
+ "error": {
+ "mustBeFinished": "Crtanje poligona mora se završiti prije spašavanja."
+ }
+ }
+ },
+ "zones": {
+ "label": "Zone",
+ "documentTitle": "Uredi zonu - Frigate",
+ "desc": {
+ "title": "Zona omogućava da definirate specifičnu područje okvira da biste odredili je li objekt unutar određenog područja.",
+ "documentation": "Dokumentacija"
+ },
+ "add": "Dodaj zonu",
+ "edit": "Uredi zonu",
+ "point_one": "{{count}} tačka",
+ "point_few": "{{count}} tačke",
+ "point_other": "{{count}} tačke",
+ "clickDrawPolygon": "Kliknite da nacrtate poligon na slici.",
+ "name": {
+ "title": "Ime",
+ "inputPlaceHolder": "Unesite ime…",
+ "tips": "Ime mora imati najmanje 2 karaktera, mora imati bar jedan slovo, i ne smije biti ime kamere ili druge zone na ovoj kameri."
+ },
+ "enabled": {
+ "title": "Omogućeno",
+ "description": "Da li je ova zona aktivna i omogućena u konfiguracionoj datoteci. Ako je onemogućena, ne može se omogućiti putem MQTT. Onemogućene zone se zanemaruju u vremenu izvršavanja."
+ },
+ "inertia": {
+ "title": "Inercija",
+ "desc": "Određuje koliko okvira mora objekt biti u zoni prije nego što se uzima u obzir u zoni. Podrazumevano: 3 "
+ },
+ "loiteringTime": {
+ "title": "Vrijeme loiteringa",
+ "desc": "Postavlja minimalno vrijeme u sekundama koje objekt mora biti u zoni da bi aktivirao. Zadano: 0 "
+ },
+ "objects": {
+ "title": "Objekti",
+ "desc": "Popis objekata koji se odnose na ovu zonu."
+ },
+ "allObjects": "Svi objekti",
+ "speedEstimation": {
+ "title": "Procjena brzine",
+ "desc": "Omogući procjenu brzine za objekte u ovoj zoni. Zona mora imati točno 4 točke.",
+ "lineADistance": "Udaljenost linije A ({{unit}})",
+ "lineBDistance": "Udaljenost linije B ({{unit}})",
+ "lineCDistance": "Udaljenost linije C ({{unit}})",
+ "lineDDistance": "Udaljenost linije D ({{unit}})"
+ },
+ "speedThreshold": {
+ "title": "Prag brzine ({{unit}})",
+ "desc": "Određuje minimalnu brzinu za objekte da bi se smatrali u ovoj zoni.",
+ "toast": {
+ "error": {
+ "pointLengthError": "Procjena brzine je onemogućena za ovu zonu. Zone s procjenom brzine moraju imati točno 4 točke.",
+ "loiteringTimeError": "Zone s vremenima trajanja većim od 0 ne bi trebale se koristiti s procjenom brzine."
+ }
+ }
+ },
+ "toast": {
+ "success": "Zona ({{zoneName}}) je sačuvana."
+ }
+ },
+ "motionMasks": {
+ "label": "Maska pokreta",
+ "documentTitle": "Uredi masku pokreta - Frigate",
+ "desc": {
+ "title": "Maska pokreta koristi se za spriječavanje neželjenih vrsta pokreta da aktiviraju detekciju. Prekrižavanje će napraviti teško praćenje objekata.",
+ "documentation": "Dokumentacija"
+ },
+ "add": "Nova maska pokreta",
+ "edit": "Uredi masku pokreta",
+ "defaultName": "Maska pokreta {{number}}",
+ "context": {
+ "title": "Maska pokreta koristi se za spriječavanje neželjenih vrsta pokreta da aktiviraju detekciju (primjer: grančice stabala, vremenske oznake kamere). Maska pokreta treba se koristiti vrlo retko , prekrižavanje će napraviti teško praćenje objekata."
+ },
+ "point_one": "{{count}} tačka",
+ "point_few": "{{count}} tačke",
+ "point_other": "{{count}} tačke",
+ "clickDrawPolygon": "Kliknite da nacrtate poligon na slici.",
+ "name": {
+ "title": "Ime",
+ "description": "Nepovlačni prijateljski naziv za ovu masku pokreta.",
+ "placeholder": "Unesite ime..."
+ },
+ "polygonAreaTooLarge": {
+ "title": "Maska pokreta pokriva {{polygonArea}}% okvirnog slike kamere. Velike maske pokreta nisu preporučene.",
+ "tips": "Maska za pokret ne sprječava detekciju objekata. Umjesto toga, trebalo bi koristiti obaveznu zonu."
+ },
+ "toast": {
+ "success": {
+ "title": "{{polygonName}} je sačuvan.",
+ "noName": "Maska za pokret je sačuvana."
+ }
+ }
+ },
+ "objectMasks": {
+ "label": "Maska za objekte",
+ "documentTitle": "Uredi masku za objekte - Frigate",
+ "desc": {
+ "title": "Maska za filtriranje objekata koristi se za uklanjanje lažnih pozitivnih rezultata za određeni tip objekta na temelju lokacije.",
+ "documentation": "Dokumentacija"
+ },
+ "add": "Dodaj masku za objekte",
+ "edit": "Uredi masku za objekte",
+ "context": "Maska za filtriranje objekata koristi se za uklanjanje lažnih pozitivnih rezultata za određeni tip objekta na temelju lokacije.",
+ "point_one": "{{count}} tačka",
+ "point_few": "{{count}} tačke",
+ "point_other": "{{count}} tačke",
+ "clickDrawPolygon": "Kliknite da nacrtate poligon na slici.",
+ "name": {
+ "title": "Ime",
+ "description": "Nepovlaženo prijateljivo ime za ovu masku za objekte.",
+ "placeholder": "Unesite ime..."
+ },
+ "objects": {
+ "title": "Objekti",
+ "desc": "Tip objekta koji se odnosi na ovu masku za objekte.",
+ "allObjectTypes": "Svi tipovi objekata"
+ },
+ "toast": {
+ "success": {
+ "title": "{{polygonName}} je sačuvan.",
+ "noName": "Maska za objekte je sačuvana."
+ }
+ }
+ },
+ "masks": {
+ "enabled": {
+ "title": "Omogućeno",
+ "description": "Da li je ova maska omogućena u konfiguracijskoj datoteci. Ako je onemogućena, ne može se omogućiti putem MQTT. Onemogućene maske se zanemaruju tijekom izvršavanja."
+ }
+ }
+ },
+ "motionDetectionTuner": {
+ "title": "Podešavač detekcije pokreta",
+ "unsavedChanges": "Nespremljene promjene podešavača detekcije pokreta ({{camera}})",
+ "desc": {
+ "title": "Frigate koristi detekciju pokreta kao prvi korak provjere da li se nešto događa u okviru vrijedno provjere pomoću detekcije objekata.",
+ "documentation": "Pročitajte vodič za podešavanje detekcije pokreta"
+ },
+ "Threshold": {
+ "title": "Prag",
+ "desc": "Vrijednost pragodiktira koliko promjene u svjetlosnosti piksela je potrebno za razmatranje kao pokret. Default: 30 "
+ },
+ "contourArea": {
+ "title": "Površina kontura",
+ "desc": "Vrijednost površine kontura koristi se za odluku koja skupine promijenjenih piksela kvalificiraju kao pokret. Default: 10 "
+ },
+ "improveContrast": {
+ "title": "Poboljšaj kontrast",
+ "desc": "Poboljšaj kontrast za tamnije scene. Zadano: UKLJUČENO "
+ },
+ "toast": {
+ "success": "Postavke pokreta su sačuvane."
+ }
+ },
+ "debug": {
+ "title": "Uklanjanje grešaka",
+ "detectorDesc": "Frigate koristi vaše detektore ({{detectors}}) za detekciju objekata u vašem video toku kamere.",
+ "desc": "Pregled u režimu uklanjanja grešaka prikazuje stvarni pregled praćenih objekata i njihovih statistika. Lista objekata prikazuje zakasnjeni pregled detektovanih objekata.",
+ "openCameraWebUI": "Otvori Web UI {{camera}}",
+ "debugging": "Uklanjanje grešaka",
+ "objectList": "Popis objekata",
+ "noObjects": "Nema objekata",
+ "audio": {
+ "title": "Audio",
+ "noAudioDetections": "Nema detekcija zvuka",
+ "score": "poena",
+ "currentRMS": "Trenutni RMS",
+ "currentdbFS": "Trenutni dbFS"
+ },
+ "boundingBoxes": {
+ "title": "Okvirne kutije",
+ "desc": "Prikaži okvirne kutije oko praćenih objekata",
+ "colors": {
+ "label": "Boje okvirnih kutija objekata",
+ "info": "Na početku, različite boje će biti dodijeljene svakom oznaci objekta Tanjira crna linija označava da objekt nije detektovan u ovom trenutku Tanjira siva linija označava da objekt detektovan kao stacionaran Deblja linija označava da je objekt subjekt automatskog praćenja (kada je omogućeno) "
+ }
+ },
+ "timestamp": {
+ "title": "Vremenski pečat",
+ "desc": "Prikazati vremenski pečat na slici"
+ },
+ "zones": {
+ "title": "Zone",
+ "desc": "Prikaži konturu definisanih zona"
+ },
+ "mask": {
+ "title": "Maska za pokret",
+ "desc": "Prikaži poligone maski za pokret"
+ },
+ "motion": {
+ "title": "Kutije za pokret",
+ "desc": "Prikaži kutije oko područja gdje je detektovan pokret",
+ "tips": "Kutije za pokret
Crvene kutije će biti prikazane na područjima okvira gdje se trenutno detektuje pokret
"
+ },
+ "regions": {
+ "title": "Regije",
+ "desc": "Prikaži kutiju područja interesa poslatog objektu detektora",
+ "tips": "Kutije regija
Sjajno zelene kutije bit će preklopljene na područjima zanimanja u okviru koji se šalju detektoru objekata.
"
+ },
+ "paths": {
+ "title": "Putanje",
+ "desc": "Prikaži značajne točke putanje praćenog objekta",
+ "tips": "Putanje
Linije i krugovi će pokazati značajne točke koje je praćeni objekt prešao tokom svojeg života.
"
+ },
+ "objectShapeFilterDrawing": {
+ "title": "Crtanje filtera oblika objekta",
+ "desc": "Nacrtaj pravokutnik na slici da bi pogledao detalje površine i omjera",
+ "tips": "Omogući ovu opciju da nacrtate pravokutnik na slici kamere da biste prikazali njegovu površinu i omjer. Ove vrijednosti zatim mogu se koristiti za postavljanje parametara filtera oblika objekta u vašoj konfiguraciji.",
+ "score": "Rezultat",
+ "ratio": "Omjer",
+ "area": "Površina"
+ }
+ },
+ "timestampPosition": {
+ "tl": "Gornji lijevo",
+ "tr": "Gornji desno",
+ "bl": "Donji lijevo",
+ "br": "Donji desno"
+ },
+ "users": {
+ "title": "Korisnici",
+ "management": {
+ "title": "Upravljanje korisnicima",
+ "desc": "Upravljajte računima korisnika ove instance Frigate."
+ },
+ "addUser": "Dodaj korisnika",
+ "updatePassword": "Ponovno postavi lozinku",
+ "toast": {
+ "success": {
+ "createUser": "Korisnik {{user}} uspješno stvoren",
+ "deleteUser": "Korisnik {{user}} uspješno obrisan",
+ "updatePassword": "Lozinka uspješno ažurirana.",
+ "roleUpdated": "Uloga ažurirana za {{user}}"
+ },
+ "error": {
+ "setPasswordFailed": "Neuspješno spremanje lozinke: {{errorMessage}}",
+ "createUserFailed": "Neuspješno stvaranje korisnika: {{errorMessage}}",
+ "deleteUserFailed": "Neuspješno brisanje korisnika: {{errorMessage}}",
+ "roleUpdateFailed": "Neuspješno ažuriranje uloge: {{errorMessage}}"
+ }
+ },
+ "table": {
+ "username": "Korisničko ime",
+ "actions": "Akcije",
+ "role": "Uloga",
+ "noUsers": "Nema pronađenih korisnika.",
+ "changeRole": "Promijeni ulogu korisnika",
+ "password": "Ponovno postavi lozinku",
+ "deleteUser": "Obriši korisnika"
+ },
+ "dialog": {
+ "form": {
+ "user": {
+ "title": "Korisničko ime",
+ "desc": "Dozvoljeno su samo slova, brojevi, tačke i donje crte.",
+ "placeholder": "Unesite korisničko ime"
+ },
+ "password": {
+ "title": "Lozinka",
+ "placeholder": "Unesite lozinku",
+ "show": "Prikaži lozinku",
+ "hide": "Sakrij lozinku",
+ "confirm": {
+ "title": "Potvrdite lozinku",
+ "placeholder": "Potvrdite lozinku"
+ },
+ "strength": {
+ "title": "Jakoća lozinke: ",
+ "weak": "Slaba",
+ "medium": "Srednja",
+ "strong": "Jaka",
+ "veryStrong": "Veoma jaka"
+ },
+ "requirements": {
+ "title": "Zahtjevi za lozinku:",
+ "length": "Bar 12 karaktera"
+ },
+ "match": "Lozinke se poklapaju",
+ "notMatch": "Lozinke se ne poklapaju"
+ },
+ "newPassword": {
+ "title": "Nova lozinka",
+ "placeholder": "Unesite novu lozinku",
+ "confirm": {
+ "placeholder": "Ponovite novu lozinku"
+ }
+ },
+ "currentPassword": {
+ "title": "Trenutna lozinka",
+ "placeholder": "Unesite svoju trenutnu lozinku"
+ },
+ "usernameIsRequired": "Korisničko ime je obavezno",
+ "passwordIsRequired": "Lozinka je obavezna"
+ },
+ "createUser": {
+ "title": "Kreirajte novog korisnika",
+ "desc": "Dodajte novi korisnički račun i odredite ulogu za pristup područjima sučelja Frigate.",
+ "usernameOnlyInclude": "Korisničko ime može sadržavati samo slova, brojeve, . ili _",
+ "confirmPassword": "Molimo potvrdite svoju lozinku"
+ },
+ "deleteUser": {
+ "title": "Obriši korisnika",
+ "desc": "Ova akcija ne može se poništiti. Ovo će trajno izbrisati korisnički račun i ukloniti sve povezane podatke.",
+ "warn": "Sigurni ste da želite izbrisati {{username}} ?"
+ },
+ "passwordSetting": {
+ "cannotBeEmpty": "Lozinka ne može biti prazna",
+ "doNotMatch": "Lozinke se ne podudaraju",
+ "currentPasswordRequired": "Trenutna lozinka je obavezna",
+ "incorrectCurrentPassword": "Trenutna lozinka je netočna",
+ "passwordVerificationFailed": "Neuspješno provjeravanje lozinke",
+ "updatePassword": "Ažurirajte lozinku za {{username}}",
+ "setPassword": "Postavi lozinku",
+ "desc": "Napravite jaku lozinku za sigurnost ovog računa.",
+ "multiDeviceWarning": "Bilo koje druge uređaje na kojima ste prijavljeni bit će potrebno ponovno se prijaviti unutar {{refresh_time}}.",
+ "multiDeviceAdmin": "Takođe možete obavezati sve korisnike da se odmah ponovno autentificiraju rotiranjem vaše tajne JWT."
+ },
+ "changeRole": {
+ "title": "Promijenite ulogu korisnika",
+ "select": "Odaberite ulogu",
+ "desc": "Ažurirajte dozvole za {{username}} ",
+ "roleInfo": {
+ "intro": "Odaberite odgovarajuću ulogu za ovog korisnika:",
+ "admin": "Administrator",
+ "adminDesc": "Pun pristup svim funkcijama.",
+ "viewer": "Pregledač",
+ "viewerDesc": "Ograničeno na Uživo tablo, pregled, istraživanje i izvoze.",
+ "customDesc": "Prilagođena uloga s određenim pristupom kamerama."
+ }
+ }
+ }
+ },
+ "roles": {
+ "management": {
+ "title": "Upravljanje ulogama gledatelja",
+ "desc": "Upravljajte prilagođenim ulogama gledatelja i njihovim dozvolama za pristup kamerama za ovu instancu Frigate."
+ },
+ "addRole": "Dodaj ulogu",
+ "table": {
+ "role": "Uloga",
+ "cameras": "Kamere",
+ "actions": "Akcije",
+ "noRoles": "Nisu pronađene prilagođene uloge.",
+ "editCameras": "Uredi Kamere",
+ "deleteRole": "Obriši ulogu"
+ },
+ "toast": {
+ "success": {
+ "createRole": "Uloga {{role}} uspješno stvorena",
+ "updateCameras": "Kamere ažurirane za ulogu {{role}}",
+ "deleteRole": "Uloga {{role}} uspješno obrisana",
+ "userRolesUpdated_one": "{{count}} korisnik dodeljen ovoj ulogi je ažuriran na 'viewer', koji ima pristup svim kamerama.",
+ "userRolesUpdated_few": "{{count}} korisnici dodeljeni ovoj ulogi su ažurirani na 'viewer', koji ima pristup svim kamerama.",
+ "userRolesUpdated_other": "{{count}} korisnici dodeljeni ovoj ulogi su ažurirani na 'viewer', koji ima pristup svim kamerama."
+ },
+ "error": {
+ "createRoleFailed": "Neuspješno stvaranje uloge: {{errorMessage}}",
+ "updateCamerasFailed": "Neuspješno ažuriranje kamera: {{errorMessage}}",
+ "deleteRoleFailed": "Neuspješno brisanje uloge: {{errorMessage}}",
+ "userUpdateFailed": "Neuspješno ažuriranje uloga korisnika: {{errorMessage}}"
+ }
+ },
+ "dialog": {
+ "createRole": {
+ "title": "Stvori novu ulogu",
+ "desc": "Dodaj novu ulogu i specifično odredi dozvole za pristup kamerama."
+ },
+ "editCameras": {
+ "title": "Uredi kamere uloge",
+ "desc": "Ažuriraj pristup kamerama za ulogu {{role}} ."
+ },
+ "deleteRole": {
+ "title": "Obriši ulogu",
+ "desc": "Ova akcija ne može biti poništena. Ovo će trajno izbrisati ulogu i dodeliti sve korisnike s ovom ulogom ulogi 'viewer', što će im dati pristup svim kamerama.",
+ "warn": "Da li ste sigurni da želite izbrisati {{role}} ?",
+ "deleting": "Brisanje..."
+ },
+ "form": {
+ "role": {
+ "title": "Ime uloge",
+ "placeholder": "Unesite ime uloge",
+ "desc": "Dozvoljeno su samo slova, brojevi, tačke i donje crte.",
+ "roleIsRequired": "Ime uloge je obavezno",
+ "roleOnlyInclude": "Ime uloge može sadržavati samo slova, brojeve, . ili _",
+ "roleExists": "Uloga s ovim imenom već postoji."
+ },
+ "cameras": {
+ "title": "Kamere",
+ "desc": "Odaberite kamere kojima ova uloga ima pristup. Potreban je bar jedan pristup.",
+ "required": "Mora biti odabrana bar jedna kamera."
+ }
+ }
+ }
+ },
+ "notification": {
+ "title": "Obavještenja",
+ "notificationSettings": {
+ "title": "Postavke obavijesti",
+ "desc": "Frigate može nativno slati obavijesti na vaš uređaj kada radi u pregledaču ili je instalirana kao PWA."
+ },
+ "notificationUnavailable": {
+ "title": "Obavijesti nedostupne",
+ "desc": "Web obavijesti zahtijevaju sigurni kontekst (https://…). Ovo je ograničenje pregledača. Pristupite Frigate sigurno da biste koristili obavijesti."
+ },
+ "globalSettings": {
+ "title": "Globalne postavke",
+ "desc": "Privremeno zaustavi obavijesti za određene kamere na svim registrovanim uređajima."
+ },
+ "email": {
+ "title": "E-mail",
+ "placeholder": "npr. example@email.com",
+ "desc": "Potrebna je važeća e-mail adresa i koristit će se za obavijestavanje ako dođe do problema sa uslugom slanja obavijesti."
+ },
+ "cameras": {
+ "title": "Kamere",
+ "noCameras": "Nema dostupnih kamera",
+ "desc": "Odaberite koje kamere omogućiti za obavijesti."
+ },
+ "deviceSpecific": "Postavke specifične za uređaj",
+ "registerDevice": "Registrujte ovaj uređaj",
+ "unregisterDevice": "Deregistrujte ovaj uređaj",
+ "sendTestNotification": "Pošaljite test obavijest",
+ "unsavedRegistrations": "Nečuvane registracije obavijesti",
+ "unsavedChanges": "Nečuvane promjene obavijesti",
+ "active": "Obavijesti aktivne",
+ "suspended": "Obavijesti zaustavljene {{time}}",
+ "suspendTime": {
+ "suspend": "Zaustavi",
+ "5minutes": "Zaustavi za 5 minuta",
+ "10minutes": "Zaustavi za 10 minuta",
+ "30minutes": "Zaustavi za 30 minuta",
+ "1hour": "Zaustavi za 1 sat",
+ "12hours": "Zaustavi za 12 sati",
+ "24hours": "Odložiti za 24 sata",
+ "untilRestart": "Odložiti do ponovnog pokretanja"
+ },
+ "cancelSuspension": "Otkaži odloženje",
+ "toast": {
+ "success": {
+ "registered": "Uspješno registrovan za obaveštenja. Potrebno je ponovno pokrenuti Frigate prije nego što se mogu slati obaveštenja (uključujući test obaveštenje).",
+ "settingSaved": "Postavke obaveštenja su sačuvane."
+ },
+ "error": {
+ "registerFailed": "Neuspješno sačuvana registracija obaveštenja."
+ }
+ }
+ },
+ "frigatePlus": {
+ "title": "Postavke Frigate+",
+ "description": "Frigate+ je usluga pretplate koja pruža pristup dodatnim funkcijama i mogućnostima za vašu instancu Frigate, uključujući mogućnost korištenja prilagođenih modela detekcije objekata treniranih na vašim podacima. Ovdje možete upravljati postavkama modela Frigate+.",
+ "cardTitles": {
+ "api": "API",
+ "currentModel": "Trenutni model",
+ "otherModels": "Drugi modeli",
+ "configuration": "Konfiguracija"
+ },
+ "apiKey": {
+ "title": "Frigate+ API ključ",
+ "validated": "Frigate+ API ključ je detektovan i validiran",
+ "notValidated": "Frigate+ API ključ nije detektovan ili nije validiran",
+ "desc": "Frigate+ API ključ omogućava integraciju sa uslugom Frigate+.",
+ "plusLink": "Pročitajte više o Frigate+"
+ },
+ "snapshotConfig": {
+ "title": "Konfiguracija snimaka",
+ "desc": "Slanje na Frigate+ zahtijeva da su snimci omogućeni u vašoj konfiguraciji.",
+ "cleanCopyWarning": "Neki uređaji imaju isključene snimke",
+ "table": {
+ "camera": "Kamera",
+ "snapshots": "Snimci"
+ }
+ },
+ "modelInfo": {
+ "title": "Informacije o modelu",
+ "modelType": "Tip modela",
+ "trainDate": "Datum treniranja",
+ "baseModel": "Osnovni model",
+ "plusModelType": {
+ "baseModel": "Osnovni model",
+ "userModel": "Podeseno"
+ },
+ "supportedDetectors": "Podržani detektori",
+ "cameras": "Kamere",
+ "loading": "Učitavanje informacija o modelu…",
+ "error": "Neuspješno učitavanje informacija o modelu",
+ "availableModels": "Dostupni modeli",
+ "loadingAvailableModels": "Učitavanje dostupnih modela…",
+ "modelSelect": "Vaši dostupni modeli na Frigate+ mogu se odabrati ovdje. Napomena: samo modeli kompatibilni s vašom trenutnom konfiguracijom detektora mogu se odabrati."
+ },
+ "unsavedChanges": "Nespremljene promjene postavki Frigate+",
+ "restart_required": "Potrebno je ponovno pokretanje (model Frigate+ promijenjen)",
+ "toast": {
+ "success": "Postavke Frigate+ su spremljene. Ponovno pokrenite Frigate da biste primijenili promjene.",
+ "error": "Nije uspješno sačuvana promjena konfiguracije: {{errorMessage}}"
+ }
+ },
+ "detectionModel": {
+ "plusActive": {
+ "title": "Upravljanje modelima Frigate+",
+ "label": "Trenutni izvor modela",
+ "description": "Ova instanca pokreće model Frigate+. Odaberite ili promijenite svoj model u postavkama Frigate+.",
+ "goToFrigatePlus": "Idi na postavke Frigate+",
+ "showModelForm": "Ručno konfigurirajte model"
+ }
+ },
+ "triggers": {
+ "documentTitle": "Pokretači",
+ "semanticSearch": {
+ "title": "Semantička pretraga je onemogućena",
+ "desc": "Semantička pretraga mora biti omogućena da biste koristili izazivače."
+ },
+ "management": {
+ "title": "Pokretači",
+ "desc": "Upravljanje izazivačima za {{camera}}. Korištenjem tipa prikaznog slika, izazivači se mogu aktivirati za slične prikazne slike odabranom praćenom objektu, a tipom opisa za slične opise teksta koji navodite."
+ },
+ "addTrigger": "Dodaj izazivač",
+ "table": {
+ "name": "Ime",
+ "type": "Tip",
+ "content": "Sadržaj",
+ "threshold": "Prag",
+ "actions": "Akcije",
+ "noTriggers": "Nema konfiguriranih izazivača za ovu kameru.",
+ "edit": "Uredi",
+ "deleteTrigger": "Obriši izazivač",
+ "lastTriggered": "Zadnji put izazvan"
+ },
+ "type": {
+ "thumbnail": "Minijatura",
+ "description": "Opis"
+ },
+ "actions": {
+ "notification": "Pošalji obavijest",
+ "sub_label": "Dodaj podnaziv",
+ "attribute": "Dodaj atribut"
+ },
+ "dialog": {
+ "createTrigger": {
+ "title": "Kreiraj izazov",
+ "desc": "Kreiraj izazov za kameru {{camera}}"
+ },
+ "editTrigger": {
+ "title": "Uredi izazov",
+ "desc": "Uredi postavke za izazov na kameri {{camera}}"
+ },
+ "deleteTrigger": {
+ "title": "Obriši izazov",
+ "desc": "Da li ste sigurni da želite obrisati izazov {{triggerName}} ? Ova akcija ne može biti poništena."
+ },
+ "form": {
+ "name": {
+ "title": "Ime",
+ "placeholder": "Daj ime ovom izazovu",
+ "description": "Unesite jedinstveno ime ili opis da biste identifikovali ovaj izazov",
+ "error": {
+ "minLength": "Polje mora imati najmanje 2 karaktera.",
+ "invalidCharacters": "Polje može sadržavati samo slova, brojeve, donje crte i crte.",
+ "alreadyExists": "Izazov sa ovim imenom već postoji za ovu kameru."
+ }
+ },
+ "enabled": {
+ "description": "Omogući ili onemogući ovaj izazov"
+ },
+ "type": {
+ "title": "Tip",
+ "placeholder": "Odaberite vrstu izazova",
+ "description": "Izazov kada se detektuje opis sličnog praćenog objekta",
+ "thumbnail": "Izazov kada se detektuje minijaturna slika sličnog praćenog objekta"
+ },
+ "content": {
+ "title": "Sadržaj",
+ "imagePlaceholder": "Odaberite minijaturnu sliku",
+ "textPlaceholder": "Unesite tekstualni sadržaj",
+ "imageDesc": "Prikazivaju se samo najnovije 100 minijaturnih slika. Ako ne možete pronaći željenu minijaturnu sliku, pregledajte ranije objekte u Pretraživanju i postavite izazov iz menija tamo.",
+ "textDesc": "Unesite tekst za izazivanje ove akcije kada se detektuje opis sličnog praćenog objekta.",
+ "error": {
+ "required": "Sadržaj je obavezan."
+ }
+ },
+ "threshold": {
+ "title": "Prag",
+ "desc": "Postavite prag sličnosti za ovaj izazov. Viši prag znači da je potrebno bliže podudaranje da bi se izazov aktivirao.",
+ "error": {
+ "min": "Prag mora biti bar 0",
+ "max": "Prag mora biti najviše 1"
+ }
+ },
+ "actions": {
+ "title": "Akcije",
+ "desc": "Po defaultu, Frigate šalje poruku MQTT za sve izazovnike. Podnošnici dodaju ime izazovnog događaja u oznaku objekta. Atributi su pretraživi metapodaci pohranjeni zasebno u metapodacima praćenih objekata.",
+ "error": {
+ "min": "Mora se odabrati bar jedna akcija."
+ }
+ }
+ }
+ },
+ "wizard": {
+ "title": "Kreiraj izazov",
+ "step1": {
+ "description": "Konfiguriraj osnovne postavke za tvoj izazov."
+ },
+ "step2": {
+ "description": "Postavi sadržaj koji će izazvati ovu akciju."
+ },
+ "step3": {
+ "description": "Konfiguriraj prag i akcije za ovaj izazov."
+ },
+ "steps": {
+ "nameAndType": "Ime i Tip",
+ "configureData": "Konfiguriraj podatke",
+ "thresholdAndActions": "Prag i Akcije"
+ }
+ },
+ "toast": {
+ "success": {
+ "createTrigger": "Izazov {{name}} uspješno kreiran.",
+ "updateTrigger": "Izazov {{name}} uspješno ažuriran.",
+ "deleteTrigger": "Izazov {{name}} uspješno obrisan."
+ },
+ "error": {
+ "createTriggerFailed": "Neuspješno kreiranje izazova: {{errorMessage}}",
+ "updateTriggerFailed": "Neuspješno ažuriranje izazova: {{errorMessage}}",
+ "deleteTriggerFailed": "Neuspješno brisanje izazova: {{errorMessage}}"
+ }
+ }
+ },
+ "maintenance": {
+ "title": "Održavanje",
+ "sync": {
+ "title": "Sinkronizacija medija",
+ "desc": "Frigate će periodično čistiti medije prema regularnom rasporedu u skladu s vašom konfiguracijom retencije. Normalno je da se vidi nekoliko orfaniranih datoteka dok Frigate radi. Koristite ovu funkciju za uklanjanje orfaniranih datoteka medija s diska koje više nisu referencirane u bazi podataka.",
+ "started": "Sinkronizacija započeta.",
+ "alreadyRunning": "Postoji već pokrenuta poslovna jedinica",
+ "error": "Neuspješno pokretanje sinkronizacije",
+ "currentStatus": "Status",
+ "jobId": "ID posla",
+ "startTime": "Vrijeme početka",
+ "endTime": "Vrijeme kraja",
+ "statusLabel": "Status",
+ "results": "Rezultati",
+ "errorLabel": "Greška",
+ "mediaTypes": "Tipovi medija",
+ "allMedia": "Svi mediji",
+ "dryRun": "Sušenje",
+ "dryRunEnabled": "Nijedna datoteka neće biti obrisana",
+ "dryRunDisabled": "Datoteke će biti obrisane",
+ "force": "Silovito",
+ "forceDesc": "Preskočiti prag sigurnosti i završiti sinkronizaciju čak i ako bi više od 50% datoteka bilo obrisano.",
+ "verbose": "Detaljan",
+ "verboseDesc": "Napisati pun popis siročića na disk za pregled.",
+ "running": "Sinkronizacija u toku...",
+ "start": "Pokreni sinkronizaciju",
+ "inProgress": "Sinkronizacija je u toku. Ova stranica je onemogućena.",
+ "status": {
+ "queued": "U redu",
+ "running": "Pokretanje",
+ "completed": "Završeno",
+ "failed": "Neuspešno",
+ "notRunning": "Nije u toku"
+ },
+ "resultsFields": {
+ "filesChecked": "Provjerene datoteke",
+ "orphansFound": "Nađeni siročići",
+ "orphansDeleted": "Obrisani siročići",
+ "aborted": "Prekinuto. Brisanje bi premašilo prag sigurnosti.",
+ "error": "Greška",
+ "totals": "Ukupno"
+ },
+ "event_snapshots": "Snimci praćenih objekata",
+ "event_thumbnails": "Minijature praćenih objekata",
+ "review_thumbnails": "Pregled minijatura",
+ "previews": "Pregledi",
+ "exports": "Izvozi",
+ "recordings": "Snimci"
+ },
+ "regionGrid": {
+ "title": "Mreža regija",
+ "desc": "Mreža regija je optimizacija koja uči gdje se objekti različitih veličina obično pojavljuju u svakoj kamere polju pogleda. Frigate koristi ove podatke da učinkovito postavi regije detekcije. Mreža se automatski gradi tokom vremena iz podataka o praćenim objektima.",
+ "clear": "Očisti rešetku područja",
+ "clearConfirmTitle": "Očisti Rešetku Područja",
+ "clearConfirmDesc": "Očišćavanje rešetke područja nije preporučeno osim ako ste nedavno promijenili veličinu modela detektora ili promijenili fizičku poziciju kamere i imate probleme s praćenjem objekata. Rešetka će se automatski ponovno izgraditi tokom vremena kada se objekti praćuju. Potreban je ponovni pokretanje Frigate-a za primjenu promjena.",
+ "clearSuccess": "Rešetka područja uspješno očišćena",
+ "clearError": "Neuspješno očišćavanje rešetke područja",
+ "restartRequired": "Potreban je ponovni pokretanje za primjenu promjena rešetke područja"
+ }
+ },
+ "configForm": {
+ "global": {
+ "title": "Globalne postavke",
+ "description": "Ove postavke se primjenjuju na sve kamere osim ako nisu prekrivene u postavkama specifičnim za kameru."
+ },
+ "camera": {
+ "title": "Postavke kamere",
+ "description": "Ove postavke se primjenjuju samo na ovu kameru i prekrivaju globalne postavke.",
+ "noCameras": "Nema dostupnih kamera"
+ },
+ "advancedSettingsCount": "Napredne postavke ({{count}})",
+ "advancedCount": "Napredno ({{count}})",
+ "showAdvanced": "Prikaži napredne postavke",
+ "tabs": {
+ "sharedDefaults": "Dijeljene zadane vrijednosti",
+ "system": "Sistem",
+ "integrations": "Integracije"
+ },
+ "additionalProperties": {
+ "keyLabel": "Ključ",
+ "valueLabel": "Vrijednost",
+ "keyPlaceholder": "Novi ključ",
+ "remove": "Ukloni"
+ },
+ "knownPlates": {
+ "namePlaceholder": "npr. Automobil supružnice",
+ "platePlaceholder": "Broj ploče ili regex"
+ },
+ "timezone": {
+ "defaultOption": "Koristi vremensku zonu pregledača"
+ },
+ "roleMap": {
+ "empty": "Nema mapiranja uloga",
+ "roleLabel": "Uloga",
+ "groupsLabel": "Grupe",
+ "addMapping": "Dodaj mapiranje uloga",
+ "remove": "Ukloni"
+ },
+ "ffmpegArgs": {
+ "preset": "Predefinisana postavka",
+ "manual": "Ručni argumenti",
+ "inherit": "Naslijeđuj iz postavke kamere",
+ "none": "Nijedan",
+ "useGlobalSetting": "Naslijeđuj iz globalne postavke",
+ "selectPreset": "Odaberite predpostavljeno",
+ "manualPlaceholder": "Unesite argumente FFmpeg",
+ "presetLabels": {
+ "preset-rpi-64-h264": "Raspberry Pi (H.264)",
+ "preset-rpi-64-h265": "Raspberry Pi (H.265)",
+ "preset-vaapi": "VAAPI (Intel/AMD GPU)",
+ "preset-intel-qsv-h264": "Intel QuickSync (H.264)",
+ "preset-intel-qsv-h265": "Intel QuickSync (H.265)",
+ "preset-nvidia": "NVIDIA GPU",
+ "preset-jetson-h264": "NVIDIA Jetson (H.264)",
+ "preset-jetson-h265": "NVIDIA Jetson (H.265)",
+ "preset-rkmpp": "Rockchip RKMPP",
+ "preset-http-jpeg-generic": "HTTP JPEG (Općenito)",
+ "preset-http-mjpeg-generic": "HTTP MJPEG (Općenito)",
+ "preset-http-reolink": "HTTP - Kamere Reolink",
+ "preset-rtmp-generic": "RTMP (Općenito)",
+ "preset-rtsp-generic": "RTSP (Općenito)",
+ "preset-rtsp-restream": "RTSP - Ponovno preusmjeravanje iz go2rtc",
+ "preset-rtsp-restream-low-latency": "RTSP - Ponovno preusmjeravanje iz go2rtc (Niska kašnjenja)",
+ "preset-rtsp-udp": "RTSP - UDP",
+ "preset-rtsp-blue-iris": "RTSP - Blue Iris",
+ "preset-record-generic": "Snimanje (Općenito, bez zvuka)",
+ "preset-record-generic-audio-copy": "Snimanje (Općenito + Kopiraj zvuk)",
+ "preset-record-generic-audio-aac": "Snimanje (Općenito + Zvuk u AAC)",
+ "preset-record-mjpeg": "Snimanje - Kamere MJPEG",
+ "preset-record-jpeg": "Snimanje - JPEG Kamere",
+ "preset-record-ubiquiti": "Snimanje - Ubiquiti Kamere"
+ }
+ },
+ "cameraInputs": {
+ "itemTitle": "Prijenos {{index}}"
+ },
+ "restartRequiredField": "Potrebno je ponovno pokretanje",
+ "restartRequiredFooter": "Konfiguracija promijenjena - Potrebno je ponovno pokretanje",
+ "sections": {
+ "detect": "Detekcija",
+ "record": "Snimanje",
+ "snapshots": "Snimci",
+ "motion": "Kretanje",
+ "objects": "Objekti",
+ "review": "Pregled",
+ "audio": "Audio",
+ "notifications": "Obavještenja",
+ "live": "Pregled uživo",
+ "timestamp_style": "Vremenske oznake",
+ "mqtt": "MQTT",
+ "database": "Baza podataka",
+ "telemetry": "Telemetrija",
+ "auth": "Autentifikacija",
+ "tls": "TLS",
+ "proxy": "Proxy",
+ "go2rtc": "go2rtc",
+ "ffmpeg": "FFmpeg",
+ "detectors": "Detektori",
+ "model": "Model",
+ "semantic_search": "Semantička pretraga",
+ "genai": "GenAI",
+ "face_recognition": "Prepoznavanje lica",
+ "lpr": "Prepoznavanje tablice vozila",
+ "birdseye": "Birdseye",
+ "masksAndZones": "Maskice / Zone"
+ },
+ "detect": {
+ "title": "Postavke detekcije"
+ },
+ "detectors": {
+ "title": "Postavke detektora",
+ "singleType": "Dozvoljen je samo jedan {{type}} detektor.",
+ "keyRequired": "Ime detektora je obavezno.",
+ "keyDuplicate": "Ime detektora već postoji.",
+ "noSchema": "Nema dostupnih šema detektora.",
+ "none": "Nema konfiguriranih instanci detektora.",
+ "add": "Dodaj detektor",
+ "addCustomKey": "Dodaj prilagođeni ključ"
+ },
+ "record": {
+ "title": "Postavke snimanja"
+ },
+ "snapshots": {
+ "title": "Postavke snimka"
+ },
+ "motion": {
+ "title": "Postavke pokreta"
+ },
+ "objects": {
+ "title": "Postavke objekta"
+ },
+ "audioLabels": {
+ "summary": "Odabrano {{count}} audio oznake",
+ "empty": "Nema dostupnih audio oznaka"
+ },
+ "objectLabels": {
+ "summary": "Odabrano {{count}} tipova objekata",
+ "empty": "Nema dostupnih oznaka objekata"
+ },
+ "reviewLabels": {
+ "summary": "Odabrano {{count}} oznaka",
+ "empty": "Nema dostupnih oznaka"
+ },
+ "filters": {
+ "objectFieldLabel": "{{field}} za {{label}}"
+ },
+ "zoneNames": {
+ "summary": "{{count}} odabrano",
+ "empty": "Nema dostupnih zona"
+ },
+ "inputRoles": {
+ "summary": "Odabrano {{count}} uloga",
+ "empty": "Nema dostupnih uloga",
+ "options": {
+ "detect": "Detektiraj",
+ "record": "Snimi",
+ "audio": "Audio"
+ }
+ },
+ "genaiRoles": {
+ "options": {
+ "embeddings": "Ugrađivanje",
+ "vision": "Vizija",
+ "tools": "Alati"
+ }
+ },
+ "semanticSearchModel": {
+ "placeholder": "Odaberi model…",
+ "builtIn": "Ugrađeni modeli",
+ "genaiProviders": "Dostavljatelji GenAI"
+ },
+ "review": {
+ "title": "Pregled postavki"
+ },
+ "audio": {
+ "title": "Postavke audija"
+ },
+ "notifications": {
+ "title": "Postavke obavijesti"
+ },
+ "live": {
+ "title": "Postavke pregleda uživo"
+ },
+ "timestamp_style": {
+ "title": "Postavke vremenske oznake"
+ },
+ "searchPlaceholder": "Pretraži...",
+ "addCustomLabel": "Dodaj prilagođenu oznaku...",
+ "genaiModel": {
+ "placeholder": "Odaberi model…",
+ "search": "Pretraži modele…",
+ "noModels": "Nema dostupnih modela"
+ }
+ },
+ "globalConfig": {
+ "title": "Globalna konfiguracija",
+ "description": "Konfigurirajte globalne postavke koje se primjenjuju na sve kamere osim ako nisu prekriveni.",
+ "toast": {
+ "success": "Globalne postavke uspješno sačuvane",
+ "error": "Neuspješno spremanje globalnih postavki",
+ "validationError": "Validacija neuspješna"
+ }
+ },
+ "cameraConfig": {
+ "title": "Konfiguracija kamere",
+ "description": "Konfigurirajte postavke za pojedinačne kamere. Postavke prekrivaju globalne podrazumijevane vrijednosti.",
+ "overriddenBadge": "Preklopljeno",
+ "resetToGlobal": "Vrati na globalno",
+ "toast": {
+ "success": "Postavke kamere uspješno sačuvane",
+ "error": "Neuspješno spremanje postavki kamere"
+ }
+ },
+ "toast": {
+ "success": "Postavke uspješno sačuvane",
+ "applied": "Postavke uspješno primijenjene",
+ "successRestartRequired": "Postavke uspješno sačuvane. Ponovo pokrenite Frigate da biste primijenili svoje promjene.",
+ "error": "Neuspješno spremanje postavki",
+ "validationError": "Validacija neuspješna: {{message}}",
+ "resetSuccess": "Poništi i vratiti se na globalne podrazumijevane vrijednosti",
+ "resetError": "Neuspješno poništavanje postavki",
+ "saveAllSuccess_one": "Uspješno sačuvan odjeljak {{count}}.",
+ "saveAllSuccess_few": "Svi odjeljci {{count}} uspješno sačuvani.",
+ "saveAllSuccess_other": "Svi odjeljci {{count}} uspješno sačuvani.",
+ "saveAllPartial_one": "{{successCount}} od {{totalCount}} odjeljka sačuvan. {{failCount}} neuspješno.",
+ "saveAllPartial_few": "{{successCount}} od {{totalCount}} odjeljaka sačuvanih. {{failCount}} neuspješno.",
+ "saveAllPartial_other": "{{successCount}} od {{totalCount}} odjeljaka sačuvanih. {{failCount}} neuspješno.",
+ "saveAllFailure": "Neuspješno spremanje svih odjeljaka."
+ },
+ "profiles": {
+ "title": "Profili",
+ "activeProfile": "Aktivni profil",
+ "noActiveProfile": "Nema aktivnog profila",
+ "active": "Aktivno",
+ "activated": "Profil '{{profile}}' aktiviran",
+ "activateFailed": "Neuspješno postavljanje profila",
+ "deactivated": "Profil deaktiviran",
+ "noProfiles": "Nema definisanih profila.",
+ "noOverrides": "Nema prekriženja",
+ "cameraCount_one": "kamera {{count}}",
+ "cameraCount_few": "{{count}} kamere",
+ "cameraCount_other": "{{count}} kamere",
+ "columnCamera": "Kamera",
+ "columnOverrides": "Prekriženja profila",
+ "baseConfig": "Bazna konfiguracija",
+ "addProfile": "Dodaj profil",
+ "newProfile": "Novi profil",
+ "profileNamePlaceholder": "npr. Opremljen, Odsutan, Noćni režim",
+ "friendlyNameLabel": "Ime profila",
+ "profileIdLabel": "ID profila",
+ "profileIdDescription": "Unutarnji identifikator korišten u konfiguraciji i automatizacijama",
+ "nameInvalid": "Dozvoljena su samo mala slova, brojevi i donje crte",
+ "nameDuplicate": "Profil s ovim imenom već postoji",
+ "error": {
+ "mustBeAtLeastTwoCharacters": "Mora imati najmanje 2 karaktera",
+ "mustNotContainPeriod": "Ne smije sadržavati tačke",
+ "alreadyExists": "Profil s ovim ID-om već postoji"
+ },
+ "renameProfile": "Preimenuj profil",
+ "renameSuccess": "Profil preimenovan u '{{profile}}'",
+ "deleteProfile": "Obriši profil",
+ "deleteProfileConfirm": "Obriši profil \"{{profile}}\" sa svih kamera? Ovo ne može biti poništeno.",
+ "deleteSuccess": "Profil '{{profile}}' obrisan",
+ "createSuccess": "Profil '{{profile}}' kreiran",
+ "removeOverride": "Ukloni prekrivanje profila",
+ "deleteSection": "Izbriši prekrivanja sekcije",
+ "deleteSectionConfirm": "Ukloni prekrivanja {{section}} za profil {{profile}} na {{camera}}?",
+ "deleteSectionSuccess": "Uklonjena prekrivanja {{section}} za {{profile}}",
+ "enableSwitch": "Omogući profile",
+ "enabledDescription": "Profilei su omogućeni. Napravite novi profil ispod, pređite na sekciju konfiguracije kamere da biste napravili promjene i sačuvajte da bi promjene bile primijenjene.",
+ "disabledDescription": "Profilei vam omogućavaju da definirate imenovane skupove prekrivanja konfiguracije kamere (npr., opremljen, odsutan, noć) koji se mogu aktivirati na zahtjev."
+ },
+ "unsavedChanges": "Imate nepohranjene promjene",
+ "confirmReset": "Potvrdi ponovno postavljanje",
+ "resetToDefaultDescription": "Ovo će ponovno postaviti sve postavke u ovoj sekciji na svoje zadane vrijednosti. Ova akcija ne može se povući.",
+ "resetToGlobalDescription": "Ovo će ponovno postaviti postavke u ovoj sekciji na globalne zadane vrijednosti. Ova akcija ne može se povući.",
+ "go2rtcStreams": {
+ "title": "go2rtc streamovi",
+ "description": "Upravljajte konfiguracijama go2rtc streamova za ponovno praćenje kamere. Svaki stream ima ime i jednu ili više izvornih URL-ova.",
+ "addStream": "Dodaj stream",
+ "addStreamDesc": "Unesite ime za novi stream. Ovo ime će se koristiti za referenciranje streama u vašoj konfiguraciji kamere.",
+ "addUrl": "Dodaj URL",
+ "streamName": "Ime streama",
+ "streamNamePlaceholder": "npr., front_door",
+ "streamUrlPlaceholder": "npr., rtsp://user:pass@192.168.1.100/stream",
+ "deleteStream": "Izbriši stream",
+ "deleteStreamConfirm": "Sigurni ste da želite izbrisati stream \"{{streamName}}\"? Kamere koje se referiraju na ovaj stream mogu prestati da rade.",
+ "noStreams": "Nema konfiguriranih go2rtc streamova. Dodajte stream da biste započeli.",
+ "validation": {
+ "nameRequired": "Ime streama je obavezno",
+ "nameDuplicate": "Stream s ovim imenom već postoji",
+ "nameInvalid": "Ime streama može sadržavati samo slova, brojeve, donje crte i crte za odvajanje",
+ "urlRequired": "Potrebna je bar jedna URL adresa"
+ },
+ "renameStream": "Preimenuj tok",
+ "renameStreamDesc": "Unesite novi naziv za ovaj tok. Preimenovanje toka može oštetiti kamere ili druge toke koji se reference na njega po nazivu.",
+ "newStreamName": "Novi naziv toka",
+ "ffmpeg": {
+ "useFfmpegModule": "Koristi režim kompatibilnosti (ffmpeg)",
+ "video": "Video",
+ "audio": "Audio",
+ "hardware": "Hardverska ubrzanja",
+ "videoCopy": "Kopiraj",
+ "videoH264": "Prevedi na H.264",
+ "videoH265": "Prevedi na H.265",
+ "videoExclude": "Izuzmi",
+ "audioCopy": "Kopiraj",
+ "audioAac": "Prevedi na AAC",
+ "audioOpus": "Prevedi na Opus",
+ "audioPcmu": "Prevedi na PCM μ-law",
+ "audioPcma": "Prevedi na PCM A-law",
+ "audioPcm": "Prevedi na PCM",
+ "audioMp3": "Prevedi na MP3",
+ "audioExclude": "Izuzmi",
+ "hardwareNone": "Bez hardverske ubrzanja",
+ "hardwareAuto": "Automatska hardverska ubrzanja"
+ }
+ },
+ "onvif": {
+ "profileAuto": "Automatski",
+ "profileLoading": "Učitavanje profila..."
+ },
+ "configMessages": {
+ "review": {
+ "recordDisabled": "Snimanje je onemogućeno, stavke za pregled neće biti generisane.",
+ "detectDisabled": "Detekcija objekata je onemogućena. Stavke za pregled zahtijevaju detektovane objekte za kategorizaciju upozorenja i detekcija.",
+ "allNonAlertDetections": "Sve aktivnosti koje nisu upozorenja bit će uključene kao detekcije."
+ },
+ "audio": {
+ "noAudioRole": "Nijedan tok nema definisan ulogu zvuka. Morate omogućiti ulogu zvuka da bi detekcija zvuka mogla da funkcioniše."
+ },
+ "audioTranscription": {
+ "audioDetectionDisabled": "Detekcija zvuka nije omogućena za ovu kameru. Transkripcija zvuka zahtijeva da detekcija zvuka bude aktivna."
+ },
+ "detect": {
+ "fpsGreaterThanFive": "Postavljanje vrijednosti detect FPS veće od 5 nije preporučljivo. Veće vrijednosti mogu uzrokovati probleme s performansama i neće pružiti nikakvu korist."
+ },
+ "faceRecognition": {
+ "globalDisabled": "Prepoznavanje lica nije omogućeno na globalnom nivou. Omogućite ga u Obogaćivanjima da bi prepoznavanje lica na nivou kamere funkcioniralo.",
+ "personNotTracked": "Prepoznavanje lica zahtijeva da se objekat 'osoba' praći. Osigurajte da je 'osoba' u listi praćenja objekata."
+ },
+ "lpr": {
+ "globalDisabled": "Prepoznavanje registarskih tablica nije omogućeno na globalnom nivou. Omogućite ga u Obogaćivanjima da bi LPR na nivou kamere funkcionirao.",
+ "vehicleNotTracked": "Prepoznavanje tablice zahtijeva da se praći 'automobil' ili 'motocikl'."
+ },
+ "record": {
+ "noRecordRole": "Nema streamova koji imaju definisanu ulogu snimanja. Snimanje neće funkcionišati."
+ },
+ "birdseye": {
+ "objectsModeDetectDisabled": "Birdseye je postavljen na režim 'objekti', ali je detekcija objekata onemogućena za ovu kameru. Kamera neće biti prikazana u Birdseye."
+ },
+ "snapshots": {
+ "detectDisabled": "Detekcija objekata je onemogućena. Snimci se generišu iz praćenih objekata i neće biti kreirani."
+ },
+ "detectors": {
+ "mixedTypes": "Svi detektori moraju koristiti isti tip. Uklonite postojet će detektore da biste koristili drugi tip.",
+ "mixedTypesSuggestion": "Svi detektori moraju koristiti isti tip. Uklonite postojet će detektore ili izaberite {{type}}."
+ }
+ }
+}
diff --git a/web/public/locales/bs/views/system.json b/web/public/locales/bs/views/system.json
new file mode 100644
index 0000000000..b36221ec36
--- /dev/null
+++ b/web/public/locales/bs/views/system.json
@@ -0,0 +1,256 @@
+{
+ "documentTitle": {
+ "cameras": "Statistika kamere - Frigate",
+ "storage": "Statistika skladišta - Frigate",
+ "general": "Opća statistika - Frigate",
+ "enrichments": "Statistika bogatstva - Frigate",
+ "logs": {
+ "frigate": "Zapisi Frigate - Frigate",
+ "go2rtc": "Zapisi Go2RTC - Frigate",
+ "nginx": "Zapisi Nginx - Frigate",
+ "websocket": "Zapisi poruka - Frigate"
+ }
+ },
+ "title": "Sistem",
+ "metrics": "Sistem metrike",
+ "logs": {
+ "websocket": {
+ "label": "Zapisi",
+ "pause": "Pauziraj",
+ "resume": "Nastavi",
+ "clear": "Očisti",
+ "filter": {
+ "all": "Svi temi",
+ "topics": "Teme",
+ "events": "Događaji",
+ "reviews": "Pregledi",
+ "classification": "Klasifikacija",
+ "face_recognition": "Prepoznavanje lica",
+ "lpr": "LPR",
+ "camera_activity": "Aktivnost kamere",
+ "system": "Sistem",
+ "camera": "Kamera",
+ "all_cameras": "Sve kamere",
+ "cameras_count_one": "{{count}} Kamera",
+ "cameras_count_other": "{{count}} Kamere"
+ },
+ "empty": "Nema još prihvaćenih poruka",
+ "count_one": "{{count}} poruka",
+ "count_other": "{{count}} poruke",
+ "expanded": {
+ "payload": "Opterećenje"
+ }
+ },
+ "download": {
+ "label": "Preuzimanje zapisa"
+ },
+ "copy": {
+ "label": "Kopiraj u clipboard",
+ "success": "Zapisi su kopirani u clipboard",
+ "error": "Nije moguće kopirati zapise u clipboard"
+ },
+ "type": {
+ "label": "Tip",
+ "timestamp": "Vremenski pečat",
+ "tag": "Oznaka",
+ "message": "Poruka"
+ },
+ "tips": "Zapisi se prenose sa servera",
+ "toast": {
+ "error": {
+ "fetchingLogsFailed": "Greška prilikom preuzimanja zapisa: {{errorMessage}}",
+ "whileStreamingLogs": "Greška prilikom prijenosa protokola: {{errorMessage}}"
+ }
+ }
+ },
+ "general": {
+ "title": "Općenito",
+ "detector": {
+ "title": "Detektori",
+ "inferenceSpeed": "Brzina zaključivanja detektora",
+ "temperature": "Temperatura detektora",
+ "cpuUsage": "Korištenje CPU detektora",
+ "cpuUsageInformation": "CPU korištena za pripremu ulaznih i izlaznih podataka za/iz modela detekcije. Ova vrijednost ne mjeri korištenje zaključivanja, čak i ako se koristi GPU ili ubrzivač.",
+ "memoryUsage": "Korištenje memorije detektora"
+ },
+ "hardwareInfo": {
+ "title": "Hardverske informacije",
+ "gpuUsage": "Korištenje GPU",
+ "gpuMemory": "Memorija GPU",
+ "gpuEncoder": "Kodiralo GPU",
+ "gpuCompute": "GPU Izračunavanje / Kodiranje",
+ "gpuDecoder": "Dekodiranje GPU",
+ "gpuTemperature": "Temperatura GPU",
+ "gpuInfo": {
+ "vainfoOutput": {
+ "title": "Vainfo Izlaz",
+ "returnCode": "Kod povratka: {{code}}",
+ "processOutput": "Izlaz procesa:",
+ "processError": "Greška procesa:"
+ },
+ "nvidiaSMIOutput": {
+ "title": "Nvidia SMI Izlaz",
+ "name": "Ime: {{name}}",
+ "driver": "Vozač: {{driver}}",
+ "cudaComputerCapability": "CUDA sposobnost izračunavanja: {{cuda_compute}}",
+ "vbios": "VBios informacije: {{vbios}}"
+ },
+ "closeInfo": {
+ "label": "Zatvori informacije GPU"
+ },
+ "copyInfo": {
+ "label": "Kopiraj informacije GPU"
+ },
+ "toast": {
+ "success": "Kopirano informacije GPU u međuspremnik"
+ }
+ },
+ "npuUsage": "Korišćenje NPU",
+ "npuMemory": "Memorija NPU",
+ "npuTemperature": "Temperatura NPU",
+ "intelGpuWarning": {
+ "title": "Upozorenje o statistikama Intel GPU",
+ "message": "Statistike GPU nedostupne",
+ "description": "Ovo je poznati bug u alatima za prikaz statistika Intel GPU (intel_gpu_top) gdje će se prekiniti i ponovo vratiti GPU korišćenje od 0% čak i u slučajevima kada se hardverska akceleracija i detekcija objekata ispravno izvršavaju na (i)GPU. Ovo nije bug Frigate. Možete ponovo pokrenuti host kako biste privremeno popravili problem i potvrdili da GPU radi ispravno. Ovo ne utiče na performanse."
+ }
+ },
+ "otherProcesses": {
+ "title": "Drugi procesi",
+ "processCpuUsage": "Korišćenje CPU procesa",
+ "processMemoryUsage": "Korišćenje memorije procesa",
+ "series": {
+ "go2rtc": "go2rtc",
+ "recording": "Snimanje",
+ "review_segment": "pregled segmenta",
+ "embeddings": "Ugrađivanja",
+ "audio_detector": "audio detektor"
+ }
+ }
+ },
+ "storage": {
+ "title": "Skladište",
+ "overview": "Pregled",
+ "recordings": {
+ "title": "Snimci",
+ "tips": "Ova vrijednost predstavlja ukupno skladište koje se koristi za snimke u bazi podataka Frigate. Frigate ne praćenje korišćenje skladišta za sve datoteke na vašem disku.",
+ "earliestRecording": "Najstariji dostupni snimak:"
+ },
+ "shm": {
+ "title": "Alokacija SHM (deljenja memorije)",
+ "warning": "Trenutna veličina SHM od {{total}}MB je prevelika. Povećajte je na najmanje {{min_shm}}MB.",
+ "frameLifetime": {
+ "title": "Vijek trajanja okvira",
+ "description": "Svaka kamera ima {{frames}} slotova za okvire u deljenoj memoriji. Na najbržoj brzini okvira kamere, svaki okvir je dostupan za približno {{lifetime}}s prije nego što se prepiše."
+ }
+ },
+ "cameraStorage": {
+ "title": "Skladište kamere",
+ "camera": "Kamera",
+ "unusedStorageInformation": "Informacije o neiskorišćenom skladištu",
+ "storageUsed": "Skladište",
+ "percentageOfTotalUsed": "Postotak ukupno",
+ "bandwidth": "Širina pojasa",
+ "unused": {
+ "title": "Neiskorišćeno",
+ "tips": "Ova vrijednost može nepravilno predstavljati slobodno prostor dostupan Frigate ako imate druge datoteke pohranjene na vašem disku izvan snimaka Frigate. Frigate ne praćenje korišćenje skladišta izvan svojih snimaka."
+ }
+ }
+ },
+ "cameras": {
+ "title": "Kamere",
+ "overview": "Pregled",
+ "info": {
+ "aspectRatio": "odnos stranica",
+ "cameraProbeInfo": "{{camera}} Informacije o ispitivanju kamere",
+ "streamDataFromFFPROBE": "Podaci o prijenosu se dobijaju pomoću ffprobe.",
+ "fetching": "Prenošenje podataka o kameri",
+ "stream": "Prijenos {{idx}}",
+ "video": "Video:",
+ "codec": "Kodek:",
+ "resolution": "Rješenje:",
+ "fps": "FPS:",
+ "unknown": "Nepoznato",
+ "audio": "Zvuk:",
+ "error": "Greška: {{error}}",
+ "tips": {
+ "title": "Informacije o ispitivanju kamere"
+ }
+ },
+ "framesAndDetections": "Okviri / Detekcije",
+ "label": {
+ "camera": "Kamera",
+ "detect": "detektirati",
+ "skipped": "preskočeno",
+ "ffmpeg": "FFmpeg",
+ "capture": "snimiti",
+ "overallFramesPerSecond": "ukupni okviri po sekundi",
+ "overallDetectionsPerSecond": "ukupne detekcije po sekundi",
+ "overallSkippedDetectionsPerSecond": "ukupno preskočene detekcije po sekundi",
+ "cameraFfmpeg": "{{camName}} FFmpeg",
+ "cameraCapture": "{{camName}} snimiti",
+ "cameraDetect": "{{camName}} detektirati",
+ "cameraGpu": "{{camName}} GPU",
+ "cameraFramesPerSecond": "{{camName}} okviri po sekundi",
+ "cameraDetectionsPerSecond": "{{camName}} detekcije po sekundi",
+ "cameraSkippedDetectionsPerSecond": "{{camName}} preskočenih detekcija u sekundi"
+ },
+ "connectionQuality": {
+ "title": "Kvaliteta veze",
+ "excellent": "Izuzetno dobra",
+ "fair": "Uredna",
+ "poor": "Loša",
+ "unusable": "Nepogodna",
+ "fps": "FPS",
+ "expectedFps": "Očekivani FPS",
+ "reconnectsLastHour": "Ponovne povezivanja (posljednje satu)",
+ "stallsLastHour": "Pauze (posljednje satu)"
+ },
+ "toast": {
+ "success": {
+ "copyToClipboard": "Podaci o testiranju kopirani u clipboard."
+ },
+ "error": {
+ "unableToProbeCamera": "Nemoguće testiranje kamere: {{errorMessage}}"
+ }
+ }
+ },
+ "lastRefreshed": "Posljednje ažuriranje: ",
+ "stats": {
+ "ffmpegHighCpuUsage": "{{camera}} ima visoku upotrebu CPU za FFmpeg ({{ffmpegAvg}}%)",
+ "detectHighCpuUsage": "{{camera}} ima visoku upotrebu CPU za detekciju ({{detectAvg}}%)",
+ "healthy": "Sistem je zdrav",
+ "reindexingEmbeddings": "Ponovno indeksiranje ugrađenih vjerodajnica ({{processed}}% završeno)",
+ "cameraIsOffline": "{{camera}} je offline",
+ "detectIsSlow": "{{detect}} je spor ({{speed}} ms)",
+ "detectIsVerySlow": "{{detect}} je vrlo spor ({{speed}} ms)",
+ "shmTooLow": "/dev/shm alokacija ({{total}} MB) treba povećati na najmanje {{min}} MB.",
+ "debugReplayActive": "Debug ponavljanje sesije je aktivno"
+ },
+ "enrichments": {
+ "title": "Obogaćivanja",
+ "infPerSecond": "Inferencije po sekundi",
+ "averageInf": "Prosjek vremena inferencije",
+ "embeddings": {
+ "image_embedding": "Slika ugrađenih vjerodajnica",
+ "text_embedding": "Tekst ugrađenih vjerodajnica",
+ "face_recognition": "Prepoznavanje lica",
+ "plate_recognition": "Prepoznavanje ploča",
+ "image_embedding_speed": "Brzina ugradnje slika",
+ "face_embedding_speed": "Brzina ugradnje lica",
+ "face_recognition_speed": "Brzina prepoznavanja lica",
+ "plate_recognition_speed": "Brzina prepoznavanja ploča",
+ "text_embedding_speed": "Brzina ugradnje teksta",
+ "yolov9_plate_detection_speed": "Brzina detekcije ploča YOLOv9",
+ "yolov9_plate_detection": "Detekcija ploča YOLOv9",
+ "review_description": "Pregled opisa",
+ "review_description_speed": "Brzina pregleda opisa",
+ "review_description_events_per_second": "Pregled opisa",
+ "object_description": "Opis objekta",
+ "object_description_speed": "Brzina opisa objekta",
+ "object_description_events_per_second": "Opis objekta",
+ "classification": "{{name}} Klasifikacija",
+ "classification_speed": "{{name}} Brzina klasifikacije",
+ "classification_events_per_second": "{{name}} Događaji klasifikacije po sekundi"
+ }
+ }
+}
diff --git a/web/public/locales/ca/audio.json b/web/public/locales/ca/audio.json
index 98ed63bb40..0cd1959b6c 100644
--- a/web/public/locales/ca/audio.json
+++ b/web/public/locales/ca/audio.json
@@ -138,7 +138,7 @@
"plucked_string_instrument": "Instrument de corda pinçada",
"guitar": "Guitarra",
"electric_guitar": "Guitarra elèctrica",
- "bass_guitar": "Baix",
+ "bass_guitar": "Guitarra baixa",
"acoustic_guitar": "Guitarra acústica",
"steel_guitar": "Guitarra steel",
"tapping": "Tapping",
diff --git a/web/public/locales/ca/common.json b/web/public/locales/ca/common.json
index d1593e9486..219c044370 100644
--- a/web/public/locales/ca/common.json
+++ b/web/public/locales/ca/common.json
@@ -49,7 +49,9 @@
"gl": "Galego (Gallec)",
"id": "Bahasa Indonesia (Indonesi)",
"ur": "اردو (Urdú)",
- "hr": "Hrvatski (croat)"
+ "hr": "Hrvatski (croat)",
+ "bs": "Bosanski (Bosni)",
+ "zhHant": "繁體中文 (Xinès Tradicional)"
},
"system": "Sistema",
"systemMetrics": "Mètriques del sistema",
@@ -109,7 +111,8 @@
"classification": "Classificació",
"chat": "Xat",
"actions": "Accions",
- "profiles": "Perfils"
+ "profiles": "Perfils",
+ "features": "Característiques"
},
"pagination": {
"previous": {
@@ -241,7 +244,7 @@
"done": "Fet",
"disabled": "Deshabilitat",
"disable": "Deshabilitar",
- "save": "Guardar",
+ "save": "Desa",
"copy": "Copiar",
"back": "Enrere",
"pictureInPicture": "Imatge en Imatge",
@@ -321,5 +324,8 @@
"internalID": "L'ID intern que Frigate s'utilitza a la configuració i a la base de dades"
},
"no_items": "Sense elements",
- "validation_errors": "Errors de validació"
+ "validation_errors": "Errors de validació",
+ "credentialField": {
+ "savedPlaceholder": "Desat — deixa en blanc per mantenir l'actual"
+ }
}
diff --git a/web/public/locales/ca/components/camera.json b/web/public/locales/ca/components/camera.json
index e2309db0a5..b95cd133c4 100644
--- a/web/public/locales/ca/components/camera.json
+++ b/web/public/locales/ca/components/camera.json
@@ -68,7 +68,10 @@
},
"success": "El grup de càmeres ({{name}}) ha estat guardat.",
"icon": "Icona",
- "label": "Grups de Càmeres"
+ "label": "Grups de Càmeres",
+ "showAll": "Mostra tots els grups de càmeres",
+ "showLess": "Mostra menys",
+ "editGroups": "Edita els grups de la càmera"
},
"debug": {
"options": {
diff --git a/web/public/locales/ca/components/dialog.json b/web/public/locales/ca/components/dialog.json
index 9e2900d8aa..f63d47f74f 100644
--- a/web/public/locales/ca/components/dialog.json
+++ b/web/public/locales/ca/components/dialog.json
@@ -24,6 +24,9 @@
},
"state": {
"submitted": "Enviat"
+ },
+ "toast": {
+ "error": "No s'ha pogut enviar a Frigate+. Si us plau, comproveu la vostra connexió de xarxa i torneu-ho a provar."
}
}
},
@@ -60,15 +63,83 @@
"noVaildTimeSelected": "No s'ha seleccionat un rang de temps vàlid",
"failed": "No s'ha pogut inciar l'exportació: {{error}}"
},
- "view": "Vista"
+ "view": "Vista",
+ "queued": "Exporta a la cua. Mostra el progrés a la pàgina d'exportacions.",
+ "batchSuccess_one": "S'ha iniciat l'exportació 1. Obrint el cas ara.",
+ "batchSuccess_many": "S'han iniciat {{count}} exportacions. Obrint el cas ara.",
+ "batchSuccess_other": "S'han iniciat {{count}} exportacions. Obrint el cas ara.",
+ "batchPartial": "S'han iniciat {{successful}} de {{total}} exportacions. Càmeres fallides: {{failedCameras}}",
+ "batchFailed": "No s'han pogut iniciar {{total}} exportacions. Càmeres fallides: {{failedCameras}}",
+ "batchQueuedSuccess_one": "Exporta a la cua 1. Obrint el cas ara.",
+ "batchQueuedSuccess_many": "{{count}} exportacions a la cua. Obrint el cas ara.",
+ "batchQueuedSuccess_other": "{{count}} exportacions a la cua. Obrint el cas ara.",
+ "batchQueuedPartial": "{{successful}} de {{total}} exportacions a la cua. Càmeres fallides: {{failedCameras}}",
+ "batchQueueFailed": "No s'han pogut posar a la cua {{total}} exportacions. Càmeres fallides: {{failedCameras}}"
},
"fromTimeline": {
"saveExport": "Guardar exportació",
- "previewExport": "Previsualitzar exportació"
+ "previewExport": "Previsualitzar exportació",
+ "queueingExport": "S'està fent la cua de l'exportació...",
+ "useThisRange": "Utilitza aquest interval"
},
"case": {
"label": "Cas",
- "placeholder": "Selecciona un cas"
+ "placeholder": "Selecciona un cas",
+ "newCaseOption": "Crea un cas no",
+ "newCaseNamePlaceholder": "Nom de cas nou",
+ "newCaseDescriptionPlaceholder": "Descripció del cas",
+ "nonAdminHelp": "Es crearà un nou cas per a aquestes exportacions."
+ },
+ "queueing": "S'està fent la cua de l'exportació...",
+ "tabs": {
+ "export": "Càmera única",
+ "multiCamera": "Multicàmera"
+ },
+ "multiCamera": {
+ "timeRange": "Interval de temps",
+ "selectFromTimeline": "Selecciona des de la línia de temps",
+ "cameraSelection": "Càmeres",
+ "cameraSelectionHelp": "Les càmeres amb objectes rastrejats en aquest interval de temps estan preseleccionades",
+ "checkingActivity": "Comprovant l'activitat de la càmera...",
+ "noCameras": "No hi ha càmeres disponibles",
+ "detectionCount_one": "1 objecte rastrejat",
+ "detectionCount_many": "{{count}} objectes rastrejats",
+ "detectionCount_other": "{{count}} objectes rastrejats",
+ "nameLabel": "Nom de l'exportació",
+ "namePlaceholder": "Nom base opcional per a aquestes exportacions",
+ "queueingButton": "S'estan posant a la cua les exportacions...",
+ "exportButton_one": "Exporta 1 càmera",
+ "exportButton_many": "Exporta {{count}} càmeres",
+ "exportButton_other": "Exporta {{count}} càmeres",
+ "searchOrSelectGroup": "Cerca, o selecciona un grup de càmeres...",
+ "selectAll": "Selecciona totes les càmeres",
+ "clearSelection": "Neteja la selecció",
+ "selectWithActivity": "Càmeres amb objectes rastrejats",
+ "selectGroup": "Selecciona un grup",
+ "noMatchingCameras": "No hi ha càmeres que coincideixin amb la cerca",
+ "selectedCount": "{{selected}} / {{total}} seleccionats"
+ },
+ "multi": {
+ "title_one": "Exporta {{count}} ressenyes",
+ "title_many": "Exporta {{count}} ressenyes",
+ "title_other": "Exporta {{count}} ressenyes",
+ "description": "Exporta cada revisió seleccionada. Totes les exportacions s'agruparan en un sol cas.",
+ "descriptionNoCase": "Exporta cada revisió seleccionada.",
+ "caseNamePlaceholder": "Exporta la revisió - {{date}}",
+ "exportButton_one": "Exporta {{count}} ressenyes",
+ "exportButton_many": "Exporta {{count}} ressenyes",
+ "exportButton_other": "Exporta {{count}} ressenyes",
+ "exportingButton": "S'està exportant...",
+ "toast": {
+ "started_one": "S'ha iniciat l'exportació 1. Obrint el cas ara.",
+ "started_many": "S'han iniciat {{count}} exportacions. Obrint el cas ara.",
+ "started_other": "S'han iniciat {{count}} exportacions. Obrint el cas ara.",
+ "startedNoCase_one": "S'ha iniciat l'exportació 1.",
+ "startedNoCase_many": "S'han iniciat {{count}} exportacions.",
+ "startedNoCase_other": "S'han iniciat {{count}} exportacions.",
+ "partial": "S'han iniciat {{successful}} de {{total}} exportacions. Ha fallat: {{failedItems}}",
+ "failed": "No s'han pogut iniciar {{total}} exportacions. Ha fallat: {{failedItems}}"
+ }
}
},
"streaming": {
@@ -116,6 +187,14 @@
"success": "Els enregistraments de vídeo associats als elements de revisió seleccionats s’han suprimit correctament.",
"error": "No s'ha pogut suprimir: {{error}}"
}
+ },
+ "shareTimestamp": {
+ "label": "Comparteix la marca horària",
+ "title": "Comparteix la marca horària",
+ "description": "Comparteix un URL amb marca horària de la posició actual del jugador o tria una marca horària personalitzada. Tingueu en compte que aquest no és un URL de compartició pública i només és accessible per als usuaris amb accés a Frigate i aquesta càmera.",
+ "custom": "Marca horària personalitzada",
+ "button": "Comparteix l'URL de la marca horària",
+ "shareTitle": "Marca de temps de revisió de Frigate: {{camera}}"
}
},
"imagePicker": {
diff --git a/web/public/locales/ca/components/player.json b/web/public/locales/ca/components/player.json
index 1fed78eff8..161727946e 100644
--- a/web/public/locales/ca/components/player.json
+++ b/web/public/locales/ca/components/player.json
@@ -32,7 +32,8 @@
"noPreviewFoundFor": "No s'ha trobat cap previsualització per a {{cameraName}}",
"submitFrigatePlus": {
"title": "Enviar aquesta imatge a Frigate+?",
- "submit": "Enviar"
+ "submit": "Enviar",
+ "previewError": "No s'ha pogut carregar la vista prèvia de la instantània. És possible que l'enregistrament no estigui disponible en aquest moment."
},
"livePlayerRequiredIOSVersion": "Es requereix iOS 17.1 o superior per a aquest tipus de reproducció en directe.",
"streamOffline": {
@@ -47,5 +48,6 @@
"error": {
"submitFrigatePlusFailed": "Error al enviar fotograma a Frigate+"
}
- }
+ },
+ "cameraOff": "La càmera està apagada"
}
diff --git a/web/public/locales/ca/config/cameras.json b/web/public/locales/ca/config/cameras.json
index 090de49fb9..1ba68b11cd 100644
--- a/web/public/locales/ca/config/cameras.json
+++ b/web/public/locales/ca/config/cameras.json
@@ -13,7 +13,7 @@
"description": "Habilitat"
},
"audio": {
- "label": "Esdeveniments d'àudio",
+ "label": "Detecció d'àudio",
"description": "Configuració per a la detecció d'esdeveniments basats en àudio per a aquesta càmera.",
"enabled": {
"label": "Habilita la detecció d'àudio",
@@ -29,11 +29,15 @@
},
"listen": {
"label": "Tipus d'escoltes",
- "description": "Llista de tipus d'esdeveniment d'àudio a detectar (per exemple: escorça, focarmalarma, crit, parla, crida)."
+ "description": "Llista de tipus d'esdeveniment d'àudio a detectar (per exemple: escorça, focarmalarma, parla, crida)."
},
"filters": {
"label": "Filtres d'àudio",
- "description": "Paràmetres de filtre per-àudio-tipus, com ara llindars de confiança utilitzats per reduir falsos positius."
+ "description": "Paràmetres de filtre per-àudio-tipus, com ara llindars de confiança utilitzats per reduir falsos positius.",
+ "threshold": {
+ "label": "Confiança mínima de l'àudio",
+ "description": "Llindar mínim de confiança per a l'esdeveniment d'àudio a comptar."
+ }
},
"enabled_in_config": {
"label": "Estat d'àudio original",
@@ -148,11 +152,11 @@
}
},
"ffmpeg": {
- "label": "FFmpeg",
- "description": "Paràmetres del FFmpeg que inclouen la ruta dels binaris, args, opcions de hwaccel i args de sortida per rol.",
+ "label": "Fluxos (FFmpeg)",
+ "description": "Les entrades de flux de càmera i les opcions del FFmpeg, incloent-hi el camí binari, els arguments, l'hwaccel i els arguments de sortida per rol.",
"path": {
"label": "Ruta FFmpeg",
- "description": "Ruta al binari FFmpeg a usar o un àlies de versió («5.0» o «7.0»)."
+ "description": "Ruta al binari FFmpeg a usar o un àlies de versió («7.0» o «8.0»)."
},
"global_args": {
"label": "Arguments globals del FFmpeg",
@@ -485,6 +489,10 @@
"hwaccel_args": {
"label": "Exporta els arguments de l'hwaccel",
"description": "Args d'acceleració de maquinari a utilitzar per a operacions d'exportació/transcodificació."
+ },
+ "max_concurrent": {
+ "label": "Màxim d'exportacions concurrents",
+ "description": "Nombre màxim de treballs d'exportació a processar al mateix temps."
}
},
"preview": {
@@ -678,7 +686,7 @@
},
"timestamp_style": {
"label": "Estil de la marca horària",
- "description": "Opcions d'estilització per a marques de temps d'alimentació aplicades a enregistraments i instantànies.",
+ "description": "Opcions d'estilització per a marques de temps aplicades instantànies i la vista de depuració.",
"position": {
"label": "Posició de la marca horària",
"description": "Posició de la marca horària a la imatge (tl/tr/bl/br)."
@@ -858,6 +866,10 @@
"dashboard": {
"label": "Mostra a l'interfície d'usuari",
"description": "Estableix si aquesta càmera és visible a tot arreu a la interfície d'usuari de la Frigate. Desactivar això requerirà editar manualment la configuració per tornar a veure aquesta càmera a la interfície d'usuari."
+ },
+ "review": {
+ "label": "Mostra en la revisió",
+ "description": "Alterna si aquesta càmera és visible a la revisió (la pàgina de revisió i el seu filtre de càmera, la revisió de moviment i la vista de l'historial)."
}
},
"webui_url": {
diff --git a/web/public/locales/ca/config/global.json b/web/public/locales/ca/config/global.json
index d81735a614..038deb4d41 100644
--- a/web/public/locales/ca/config/global.json
+++ b/web/public/locales/ca/config/global.json
@@ -7,7 +7,7 @@
"description": "Paràmetres del FFmpeg que inclouen la ruta dels binaris, args, opcions de hwaccel i args de sortida per rol.",
"path": {
"label": "Ruta FFmpeg",
- "description": "Ruta al binari FFmpeg a usar o un àlies de versió («5.0» o «7.0»)."
+ "description": "Ruta al binari FFmpeg a usar o un àlies de versió («7.0» o «8.0»)."
},
"global_args": {
"label": "Arguments globals del FFmpeg",
@@ -258,6 +258,41 @@
},
"raw_mask": {
"label": "Màscara en brut"
+ },
+ "filters_attribute": {
+ "label": "Filtres d'atribut",
+ "description": "Filtres aplicats als atributs detectats per reduir falsos positius (àrea, relació, confiança).",
+ "min_area": {
+ "label": "Àrea mínima de l'atribut",
+ "description": "Es requereix una àrea de caixa contenidora mínima (píxels o percentatge) per a aquest atribut. Pot ser píxels (int) o percentatge (float entre 0,000001 i 0.99)."
+ },
+ "max_area": {
+ "label": "Àrea màxima de l'atribut",
+ "description": "Es permet l'àrea màxima del contenidor (píxels o percentatge) per a aquest atribut. Pot ser píxels (int) o percentatge (float entre 0,000001 i 0.99)."
+ },
+ "min_ratio": {
+ "label": "Relació mínima d'aspecte",
+ "description": "Relació mínima d'amplada/alçada requerida per a la casella contenidora a qualificar."
+ },
+ "max_ratio": {
+ "label": "Relació màxima d'aspecte",
+ "description": "Es permet la relació màxima d'amplada/alçada per a la casella contenidora a qualificar."
+ },
+ "threshold": {
+ "label": "Llindar de confiança",
+ "description": "Es requereix un llindar de confiança mitjà per a la detecció perquè l'atribut es consideri un veritable positiu."
+ },
+ "min_score": {
+ "label": "Confiança mínima",
+ "description": "Es requereix una confiança mínima de detecció d'un sol fotograma per a associar aquest atribut amb el seu objecte pare."
+ },
+ "mask": {
+ "label": "Màscara de filtre",
+ "description": "Coordenades de polígon que defineixen on s'aplica aquest filtre dins del marc."
+ },
+ "raw_mask": {
+ "label": "Màscara en brut"
+ }
}
},
"record": {
@@ -341,6 +376,10 @@
"hwaccel_args": {
"label": "Exporta els arguments de l'hwaccel",
"description": "Args d'acceleració de maquinari a utilitzar per a operacions d'exportació/transcodificació."
+ },
+ "max_concurrent": {
+ "label": "Màxim d'exportacions concurrents",
+ "description": "Nombre màxim de treballs d'exportació a processar al mateix temps."
}
},
"preview": {
@@ -485,11 +524,11 @@
},
"reindex": {
"label": "Reindexa en iniciar",
- "description": "Activa un reíndex complet d'objectes rastrejats històrics a la base de dades d'incrustacions."
+ "description": "Activa un reindexat complet d'objectes rastrejats històrics a la base de dades d'incrustacions."
},
"model": {
"label": "Model de cerca semàntica o nom del proveïdor GenAI",
- "description": "El model d'incrustació a utilitzar per a la cerca semàntica (per exemple 'jinav1'), o el nom d'un proveïdor de GenAI amb el rol d'incrustació."
+ "description": "El model de vectors a utilitzar per a la cerca semàntica (per exemple 'jinav1'), o el nom d'un proveïdor de GenAI amb el rol de vectors."
},
"model_size": {
"label": "Mida del model",
@@ -769,7 +808,7 @@
},
"model_size": {
"label": "Mida del model",
- "description": "Mida del model a utilitzar per a incrustacions facials (petit/gran); més gran pot requerir GPU."
+ "description": "Mida del model a utilitzar per als vectors facials (petit/gran); més gran pot requerir GPU."
},
"unknown_score": {
"label": "Llindar de puntuació desconegut",
@@ -945,7 +984,7 @@
},
"default_role": {
"label": "Rol predeterminat",
- "description": "Rol predeterminat assignat als usuaris intermediaris autenticats quan no s'aplica cap mapatge de rols (administrador o visor)."
+ "description": "Rol predeterminat assignat als usuaris intermediaris autenticats quan no s'aplica cap mapatge de rols."
},
"separator": {
"label": "Caràcter separador",
@@ -975,8 +1014,8 @@
"description": "Habilita el monitoratge d'amplada de banda per procés per als processos i detectors de ffmpeg de càmera (requereix capacitats)."
},
"intel_gpu_device": {
- "label": "Dispositiu SR-IOV",
- "description": "Identificador de dispositiu utilitzat quan es tracten les GPU d'Intel com a SR-IOV per corregir les estadístiques de GPU."
+ "label": "Dispositiu GPU d'Intel",
+ "description": "Adreça de bus PCI o camí del dispositiu DRM (p. ex. /dev/dri/card1) utilitzat per fixar les estadístiques de GPU d'Intel a un dispositiu específic quan hi ha múltiples."
}
},
"version_check": {
@@ -1951,7 +1990,7 @@
},
"roles": {
"label": "Rols",
- "description": "Funcions genAI (eines, visió, incrustacions); un proveïdor per rol."
+ "description": "Rols de GenAI (xat, descripcions, incrustacions); un proveïdor per rol."
},
"provider_options": {
"label": "Opcions del proveïdor",
@@ -1963,7 +2002,7 @@
}
},
"audio": {
- "label": "Esdeveniments d'àudio",
+ "label": "Detecció d'àudio",
"description": "Configuració per a la detecció d'esdeveniments basats en àudio per a totes les càmeres; es pot substituir per càmera.",
"enabled": {
"label": "Habilita la detecció d'àudio",
@@ -1979,11 +2018,15 @@
},
"listen": {
"label": "Tipus d'escoltes",
- "description": "Llista de tipus d'esdeveniment d'àudio a detectar (per exemple: escorça, focarmalarma, crit, parla, crida)."
+ "description": "Llista de tipus d'esdeveniment d'àudio a detectar (per exemple: escorça, focarmalarma, parla, crida)."
},
"filters": {
"label": "Filtres d'àudio",
- "description": "Paràmetres de filtre per-àudio-tipus, com ara llindars de confiança utilitzats per reduir falsos positius."
+ "description": "Paràmetres de filtre per-àudio-tipus, com ara llindars de confiança utilitzats per reduir falsos positius.",
+ "threshold": {
+ "label": "Confiança mínima de l'àudio",
+ "description": "Llindar mínim de confiança per a l'esdeveniment d'àudio a comptar."
+ }
},
"enabled_in_config": {
"label": "Estat d'àudio original",
@@ -2203,7 +2246,7 @@
},
"match_distance": {
"label": "Distància de la coincidència",
- "description": "Nombre de desajustos de caràcters permesos quan es comparen les plaques detectades amb les plaques conegudes."
+ "description": "Nombre de discrepàncies de caràcters permesos en comparar les plaques detectades amb les plaques conegudes."
},
"known_plates": {
"label": "Matricules conegudes",
@@ -2294,6 +2337,10 @@
"dashboard": {
"label": "Mostra a la interfície",
"description": "Estableix si aquesta càmera és visible a tot arreu a la interfície d'usuari de Frigate. Desactivar això requerirà editar manualment la configuració per tornar a veure aquesta càmera a la interfície d'usuari."
+ },
+ "review": {
+ "label": "Mostra en la revisió",
+ "description": "Alterna si aquesta càmera és visible a la revisió (la pàgina de revisió i el seu filtre de càmera, la revisió de moviment i la vista de l'historial)."
}
},
"profiles": {
diff --git a/web/public/locales/ca/config/validation.json b/web/public/locales/ca/config/validation.json
index bcf1093c17..b087d5a892 100644
--- a/web/public/locales/ca/config/validation.json
+++ b/web/public/locales/ca/config/validation.json
@@ -28,5 +28,8 @@
"detectRequired": "Almenys un flux d'entrada ha de tenir assignat el rol «detecta».",
"hwaccelDetectOnly": "Només el flux d'entrada amb el rol detect pot definir arguments d'acceleració del maquinari."
}
+ },
+ "detect": {
+ "dimensionMustBeEven": "Ha de ser un nombre parell."
}
}
diff --git a/web/public/locales/ca/objects.json b/web/public/locales/ca/objects.json
index 456f522ab0..17378dfe09 100644
--- a/web/public/locales/ca/objects.json
+++ b/web/public/locales/ca/objects.json
@@ -121,5 +121,10 @@
"royal_mail": "Royal Mail",
"school_bus": "Bus escolar",
"skunk": "Mofeta",
- "kangaroo": "Cangur"
+ "kangaroo": "Cangur",
+ "baby": "Nadó",
+ "baby_stroller": "Cotxet",
+ "rickshaw": "Ricksaw",
+ "Rodent": "Rosegador",
+ "rodent": "Rosegador"
}
diff --git a/web/public/locales/ca/views/chat.json b/web/public/locales/ca/views/chat.json
new file mode 100644
index 0000000000..cb618cebc4
--- /dev/null
+++ b/web/public/locales/ca/views/chat.json
@@ -0,0 +1,72 @@
+{
+ "documentTitle": "Xat - Frigate",
+ "title": "Xat Frigate",
+ "subtitle": "El teu assistent d'AI per a gestionar càmeres i coneixements",
+ "placeholder": "Pregunta qualsevol cosa...",
+ "error": "Alguna cosa ha fallat. Torna-ho a provar.",
+ "processing": "Processant...",
+ "toolsUsed": "Usades: {{tools}}",
+ "showTools": "Mostra eines ({{count}})",
+ "hideTools": "Amaga eines",
+ "call": "Truca",
+ "result": "Resultat",
+ "arguments": "Variables:",
+ "response": "Resposta:",
+ "attachment_chip_label": "{{label}} a {{camera}}",
+ "attachment_chip_remove": "Elimina l'adjunt",
+ "open_in_explore": "Obre en l'explorador",
+ "attach_event_aria": "Adjunta l'esdeveniment {{eventId}}",
+ "attachment_picker_paste_label": "O enganxa l'ID de l'esdeveniment",
+ "attachment_picker_attach": "Adjunta",
+ "attachment_picker_placeholder": "Adjunta un esdeveniment",
+ "quick_reply_find_similar": "Troba albiraments similars",
+ "quick_reply_tell_me_more": "Explica'm més sobre això",
+ "quick_reply_when_else": "Quan més es va veure?",
+ "quick_reply_find_similar_text": "Troba albiraments similars a això.",
+ "quick_reply_tell_me_more_text": "Parla'm més d'aquest.",
+ "quick_reply_when_else_text": "Quan més es va veure això?",
+ "anchor": "Referència",
+ "similarity_score": "Similitud",
+ "no_similar_objects_found": "No s'ha trobat cap objecte similar.",
+ "semantic_search_required": "La cerca semàntica ha d'estar habilitada per trobar objectes similars.",
+ "send": "Envia",
+ "suggested_requests": "Proveu de preguntar:",
+ "starting_requests": {
+ "show_recent_events": "Mostra els esdeveniments recents",
+ "show_camera_status": "Mostra l'estat de la càmera",
+ "recap": "Què va passar mentre jo era fora?",
+ "watch_camera": "Observa una càmera per a l'activitat"
+ },
+ "starting_requests_prompts": {
+ "show_recent_events": "Mostra'm els esdeveniments recents de l'última hora",
+ "show_camera_status": "Quin és l'estat actual de les meves càmeres?",
+ "recap": "Què va passar mentre jo era fora?",
+ "watch_camera": "Vigila la porta d'entrada i fes-me saber si algú apareix"
+ },
+ "new_chat": "Xat nou",
+ "settings": {
+ "title": "Configuració del xat",
+ "show_stats": {
+ "title": "Mostra les estadístiques",
+ "desc": "Mostra la velocitat de generació i la mida del context per a les respostes del xat.",
+ "while_generating": "En generar",
+ "always": "Sempre"
+ },
+ "auto_scroll": {
+ "title": "Desplaçament automàtic",
+ "desc": "Segueix els missatges nous a mesura que arriben."
+ }
+ },
+ "stats": {
+ "context": "{{tokens}} tokens",
+ "tokens_per_second": "{{rate}} t/s"
+ },
+ "reasoning": {
+ "active": "Raonant…",
+ "show": "Mostra el raonament",
+ "hide": "Amaga el raonament"
+ },
+ "thinking": {
+ "toggle": "Commuta el pensament"
+ }
+}
diff --git a/web/public/locales/ca/views/events.json b/web/public/locales/ca/views/events.json
index afacccbf9b..a0563a991f 100644
--- a/web/public/locales/ca/views/events.json
+++ b/web/public/locales/ca/views/events.json
@@ -27,7 +27,9 @@
},
"documentTitle": "Revisió - Frigate",
"recordings": {
- "documentTitle": "Enregistraments - Frigate"
+ "documentTitle": "Enregistraments - Frigate",
+ "invalidSharedLink": "No s'ha pogut obrir l'enllaç d'enregistrament amb marques de temps a causa d'un error d'anàlisi.",
+ "invalidSharedCamera": "No s'ha pogut obrir l'enllaç d'enregistrament amb marques de temps a causa d'una càmera desconeguda o no autoritzada."
},
"calendarFilter": {
"last24Hours": "Últimes 24 hores"
diff --git a/web/public/locales/ca/views/explore.json b/web/public/locales/ca/views/explore.json
index a923baa954..a6a2844799 100644
--- a/web/public/locales/ca/views/explore.json
+++ b/web/public/locales/ca/views/explore.json
@@ -2,10 +2,10 @@
"exploreIsUnavailable": {
"downloadingModels": {
"tips": {
- "context": "Potser voldreu reindexar les incrustacions dels objectes seguits un cop s'hagin descarregat els models.",
+ "context": "Potser voldreu reindexar els vectors dels objectes seguits un cop s'hagin descarregat els models.",
"documentation": "Llegir la documentació"
},
- "context": "Frigate està descarregant els models d'embeddings necessaris per a donar suport a la funció de cerca semàntica. Això pot trigar diversos minuts, depenent de la velocitat de la teva connexió de xarxa.",
+ "context": "El Frigate està baixant els models de vectors necessaris per a admetre la funció de Cerca Semàntica. Això pot trigar uns quants minuts depenent de la velocitat de la vostra connexió de xarxa.",
"setup": {
"visionModel": "Model de visió",
"visionModelFeatureExtractor": "Extractor de característiques del model de visió",
@@ -248,7 +248,7 @@
"dialog": {
"confirmDelete": {
"title": "Confirmar la supressió",
- "desc": "Eliminant aquest objecte seguit borrarà l'snapshot, qualsevol embedding gravat, i qualsevol detall de seguiment. Les imatges gravades d'aquest objecte seguit en l'historial NO seràn eliminades. Estas segur que vols continuar?"
+ "desc": "En eliminar aquest objecte detectat, s'esborrarà la instantània, els vectors desats i qualsevol entrada associada als detalls de seguiment d'aquest objecte. El metratge enregistrat d'aquest objecte detectat a la vista de l'Historial NO s'esborrarà. Segur que voleu continuar?"
},
"toast": {
"error": "S'ha produït un error en suprimir aquest objecte rastrejat: {{errorMessage}}"
@@ -282,14 +282,17 @@
"faceOrLicense_plate": "{{attribute}} detectat per {{label}}",
"other": "{{label}} reconegut com a {{attribute}}"
},
- "gone": "{{label}} esquerra",
+ "gone": "{{label}} ha sortit",
"heard": "{{label}} sentit",
"external": "{{label}} detectat",
"header": {
"zones": "Zones",
"ratio": "Ràtio",
"area": "Àrea",
- "score": "Puntuació"
+ "score": "Puntuació",
+ "computedScore": "Puntuació calculada",
+ "topScore": "Puntuació superior",
+ "toggleAdvancedScores": "Commuta les puntuacions avançades"
}
},
"annotationSettings": {
diff --git a/web/public/locales/ca/views/exports.json b/web/public/locales/ca/views/exports.json
index ccb5366b55..194d87ae40 100644
--- a/web/public/locales/ca/views/exports.json
+++ b/web/public/locales/ca/views/exports.json
@@ -14,7 +14,9 @@
"toast": {
"error": {
"renameExportFailed": "Error al canviar el nom de l’exportació: {{errorMessage}}",
- "assignCaseFailed": "No s'ha pogut actualitzar l'assignació de cas:{{errorMessage}}"
+ "assignCaseFailed": "No s'ha pogut actualitzar l'assignació de cas:{{errorMessage}}",
+ "caseSaveFailed": "No s'ha pogut desar el cas: {{errorMessage}}",
+ "caseDeleteFailed": "No s'ha pogut suprimir el cas: {{errorMessage}}"
}
},
"tooltip": {
@@ -22,7 +24,8 @@
"downloadVideo": "Baixa el vídeo",
"editName": "Edita el nom",
"deleteExport": "Suprimeix l'exportació",
- "assignToCase": "Afegeix al cas"
+ "assignToCase": "Afegeix al cas",
+ "removeFromCase": "Elimina del cas"
},
"headings": {
"cases": "Casos",
@@ -35,5 +38,91 @@
"newCaseOption": "Crea un cas nou",
"nameLabel": "Nom del cas",
"descriptionLabel": "Descripció"
+ },
+ "toolbar": {
+ "newCase": "Cas nou",
+ "addExport": "Afegeix una exportació",
+ "editCase": "Edita el cas",
+ "deleteCase": "Suprimeix el cas"
+ },
+ "deleteCase": {
+ "label": "Suprimeix el cas",
+ "desc": "Esteu segur que voleu suprimir {{caseName}}?",
+ "descKeepExports": "Les exportacions continuaran estant disponibles com a exportacions sense categoria.",
+ "descDeleteExports": "Totes les exportacions en aquest cas s'eliminaran permanentment.",
+ "deleteExports": "Elimina també les exportacions"
+ },
+ "caseCard": {
+ "emptyCase": "Encara no hi ha exportacions"
+ },
+ "jobCard": {
+ "defaultName": "Exportació de {{camera}}",
+ "queued": "En cua",
+ "running": "En execució",
+ "preparing": "Preparant",
+ "copying": "Copiant",
+ "encoding": "Codificant",
+ "encodingRetry": "Codificant (reintent)",
+ "finalizing": "Finalitzant"
+ },
+ "caseView": {
+ "noDescription": "Sense descripció",
+ "createdAt": "{{value}} creat",
+ "exportCount_one": "1 exportació",
+ "exportCount_other": "{{count}} exportacions",
+ "cameraCount_one": "1 càmera",
+ "cameraCount_other": "{{count}} càmeres",
+ "showMore": "Mostra'n més",
+ "showLess": "Mostra menys",
+ "emptyTitle": "Aquest cas és buit",
+ "emptyDescription": "Afegeix les exportacions no categoritzades existents per mantenir el cas organitzat.",
+ "emptyDescriptionNoExports": "Encara no hi ha exportacions sense categoria per afegir."
+ },
+ "caseEditor": {
+ "createTitle": "Crea un cas",
+ "editTitle": "Edita el cas",
+ "namePlaceholder": "Nom del cas",
+ "descriptionPlaceholder": "Afegeix notes o context per a aquest cas"
+ },
+ "addExportDialog": {
+ "title": "Afegeix l'exportació a {{caseName}}",
+ "searchPlaceholder": "Cerca exportacions sense categoria",
+ "empty": "No hi ha exportacions sense categoria que coincideixin amb aquesta cerca.",
+ "addButton_one": "Afegeix 1 exportació",
+ "addButton_other": "Afegeix {{count}} exportacions",
+ "adding": "S'està afegint..."
+ },
+ "selected_one": "{{count}} seleccionats",
+ "selected_other": "{{count}} seleccionats",
+ "bulkActions": {
+ "addToCase": "Afegeix al cas",
+ "moveToCase": "Mou al cas",
+ "removeFromCase": "Elimina del cas",
+ "delete": "Suprimeix",
+ "deleteNow": "Suprimeix ara"
+ },
+ "bulkDelete": {
+ "title": "Suprimeix les exportacions",
+ "desc_one": "Esteu segur que voleu suprimir {{count}} l'exportació?",
+ "desc_other": "steu segur que voleu suprimir {{count}} exportacions?"
+ },
+ "bulkRemoveFromCase": {
+ "title": "Elimina del cas",
+ "desc_one": "Voleu suprimir {{count}} d'aquest cas?",
+ "desc_other": "Voleu eliminar {{count}} exportacions d'aquest cas?",
+ "descKeepExports": "Les exportacions es mouran a sense categoria.",
+ "descDeleteExports": "Les exportacions s'eliminaran permanentment.",
+ "deleteExports": "Suprimeix les exportacions"
+ },
+ "bulkToast": {
+ "success": {
+ "delete": "Exportacions suprimides amb èxit",
+ "reassign": "Assignació de cas actualitzada amb èxit",
+ "remove": "S'han eliminat les exportacions del cas"
+ },
+ "error": {
+ "deleteFailed": "No s'han pogut suprimir les exportacions: {{errorMessage}}",
+ "reassignFailed": "No s'ha pogut actualitzar l'assignació de cas: {{errorMessage}}"
+ }
}
}
diff --git a/web/public/locales/ca/views/faceLibrary.json b/web/public/locales/ca/views/faceLibrary.json
index 1cc77f1a60..5f0546ecc8 100644
--- a/web/public/locales/ca/views/faceLibrary.json
+++ b/web/public/locales/ca/views/faceLibrary.json
@@ -14,7 +14,11 @@
"empty": "No hi ha intents recents de reconeixement de rostres",
"title": "Reconeixements recents",
"aria": "Selecciona els reconeixements recents",
- "titleShort": "Recent"
+ "titleShort": "Recent",
+ "emptyNoLibrary": {
+ "title": "Puja una cara",
+ "description": "Heu d'afegir com a mínim una cara a la biblioteca perquè el reconeixement de la cara funcioni."
+ }
},
"description": {
"addFace": "Afegiu una col·lecció nova a la biblioteca de cares pujant la vostra primera imatge.",
@@ -38,7 +42,7 @@
"uploadFace": "Puja una imatge del rostre",
"nextSteps": "Següents passos",
"description": {
- "uploadFace": "Puja una imatge de {{name}} que mostri el seu rostre de cares. No cal que la imatge estigui retallada només al rostre."
+ "uploadFace": "Pugeu una imatge de {{name}} que mostra la seva cara des d'un angle frontal. La imatge no necessita ser retallada a la seva cara."
}
},
"selectFace": "Seleccionar rostre",
diff --git a/web/public/locales/ca/views/live.json b/web/public/locales/ca/views/live.json
index b40f02e35a..b2f7fda188 100644
--- a/web/public/locales/ca/views/live.json
+++ b/web/public/locales/ca/views/live.json
@@ -58,7 +58,9 @@
},
"camera": {
"enable": "Habilitar la càmera",
- "disable": "Deshabilita la càmera"
+ "disable": "Deshabilita la càmera",
+ "turnOn": "Activa la càmera",
+ "turnOff": "Apaga la càmera"
},
"muteCameras": {
"enable": "Silencia totes les càmeres",
@@ -70,7 +72,8 @@
},
"recording": {
"enable": "Habilitar gravació",
- "disable": "Deshabilita l'enregistrament"
+ "disable": "Deshabilita l'enregistrament",
+ "disabledInConfig": "L'enregistrament primer s'ha d'habilitar a la configuració d'aquesta càmera."
},
"snapshots": {
"enable": "Habilita captura d'instantània",
@@ -150,7 +153,8 @@
"autotracking": "Seguiment automàtic",
"objectDetection": "Detecció d'objectes",
"audioDetection": "Detecció d'àudio",
- "transcription": "Transcripció d'audio"
+ "transcription": "Transcripció d'audio",
+ "camera": "Càmera"
},
"history": {
"label": "Mostrar gravacions històriques"
diff --git a/web/public/locales/ca/views/motionSearch.json b/web/public/locales/ca/views/motionSearch.json
new file mode 100644
index 0000000000..585f6cbb69
--- /dev/null
+++ b/web/public/locales/ca/views/motionSearch.json
@@ -0,0 +1,82 @@
+{
+ "documentTitle": "Busca Deteccións - Frigate",
+ "title": "Búsqueda de Deteccions",
+ "selectCamera": "Búsqueda de Deteccions s'esta carregant",
+ "startSearch": "Començar Búsqueda",
+ "searchStarted": "Búsqueda inicada",
+ "searchCancelled": "Búsqueda cancel·lada",
+ "cancelSearch": "Cancel·lar",
+ "searching": "Búsqueda en progrés.",
+ "searchComplete": "Búsqueda completa",
+ "description": "Dibuixa un polígon per definir la regió d'interès, i especifica un interval de temps per cercar canvis de moviment dins d'aquesta regió.",
+ "noResultsYet": "Executa una cerca per a trobar canvis de moviment a la regió seleccionada",
+ "noChangesFound": "No s'ha detectat cap canvi de píxel a la regió seleccionada",
+ "changesFound_one": "S'ha trobat el canvi de moviment {{count}}",
+ "changesFound_many": "S'han trobat {{count}} canvis de moviment",
+ "changesFound_other": "S'han trobat {{count}} canvis de moviment",
+ "framesProcessed": "{{count}} fotogrames processats",
+ "jumpToTime": "Salta a aquesta hora",
+ "results": "Resultats",
+ "showSegmentHeatmap": "Mapa de calor",
+ "newSearch": "Cerca nova",
+ "clearResults": "Neteja els resultats",
+ "clearROI": "Neteja el polígon",
+ "polygonControls": {
+ "points_one": "{{count}} punt",
+ "points_many": "{{count}} punts",
+ "points_other": "{{count}} punts",
+ "undo": "Desfés l'últim punt",
+ "reset": "Restableix el polígon",
+ "drawMode": "Dibuxa",
+ "moveMode": "Moure"
+ },
+ "motionHeatmapLabel": "Mapa de calor del moviment",
+ "dialog": {
+ "title": "Cerca de moviment",
+ "cameraLabel": "Càmara",
+ "previewAlt": "Vista prèvia de la càmera per a {{camera}}"
+ },
+ "timeRange": {
+ "title": "Interval de cerca",
+ "start": "Hora d'inici",
+ "end": "Hora final"
+ },
+ "settings": {
+ "title": "Configuració de la cerca",
+ "parallelMode": "Mode paral·lel",
+ "parallelModeDesc": "Escaneja múltiples intervals d'enregistrament al mateix temps (més ràpid; utilitza més recursos de descodificació)",
+ "threshold": "Llindar de la sensibilitat",
+ "thresholdDesc": "Els valors més baixos detecten canvis més petits (1-255)",
+ "minArea": "Àrea de canvi mínim",
+ "minAreaDesc": "Mida mínima d'una sola regió en moviment, com a percentatge de la regió d'interès",
+ "frameSkip": "Omet el fotograma",
+ "frameSkipDesc": "Processa cada N fotograma. Establiu això a la velocitat de fotogrames de la càmera per processar un fotograma per segon (p. ex. 5 per a una càmera de 5 FPS, 30 per a una càmera de 30 FPS). Els valors més alts seran més ràpids, però poden perdre els esdeveniments de curt moviment.",
+ "maxResults": "Resultats màxims",
+ "maxResultsDesc": "Atura després d'aquestes quantes marques horàries coincidents"
+ },
+ "errors": {
+ "noCamera": "Seleccioneu una càmera",
+ "noROI": "Dibuixeu una regió d'interès",
+ "noTimeRange": "Seleccioneu un interval de temps",
+ "invalidTimeRange": "L'hora de finalització ha de ser posterior a l'hora d'inici",
+ "searchFailed": "Ha fallat la cerca: {{message}}",
+ "polygonTooSmall": "El polígon ha de tenir almenys 3 punts",
+ "unknown": "Error desconegut"
+ },
+ "changePercentage": "{{percentage}}% canviat",
+ "metrics": {
+ "title": "Cerca les mètriques",
+ "segmentsScanned": "Segments escanejats",
+ "segmentsProcessed": "Processat",
+ "segmentsSkippedInactive": "S'ha omès (sense activitat)",
+ "segmentsSkippedHeatmap": "S'ha omès (sense superposició ROI)",
+ "fallbackFullRange": "Escaneig de rang complet alternatiu",
+ "framesDecoded": "Fotogrames descodificats",
+ "wallTime": "Temps de cerca",
+ "segmentErrors": "Errors del segment",
+ "seconds": "{{seconds}}s",
+ "scanSummary": "{{segments}} segments · {{time}}",
+ "minutesSeconds": "{{minutes}}m {{seconds}}s"
+ },
+ "scanning": "S'està analitzant {{time}}"
+}
diff --git a/web/public/locales/ca/views/replay.json b/web/public/locales/ca/views/replay.json
new file mode 100644
index 0000000000..8dc6730cc3
--- /dev/null
+++ b/web/public/locales/ca/views/replay.json
@@ -0,0 +1,59 @@
+{
+ "page": {
+ "startError": {
+ "back": "Torna a l'Historial",
+ "title": "No s'ha pogut iniciar la repetició de la depuració"
+ },
+ "sourceCamera": "Camera d'origen",
+ "replayCamera": "Reproduïr Càmera",
+ "initializingReplay": "Inicialitzant depurar repetició...",
+ "stoppingReplay": "Parant depurar repetició...",
+ "stopReplay": "Parar Repetició",
+ "confirmStop": {
+ "title": "Parar Depurar Repetició?",
+ "description": "Aixó pararà la sessió i netejarà les dades temporals. Estás segur?",
+ "confirm": "Parar Repetició",
+ "cancel": "Cancel·lar"
+ },
+ "activity": "Activitat",
+ "objects": "Llista d'Objectes",
+ "audioDetections": "Deteccions d'Audio",
+ "noActivity": "Sense activitat detectada",
+ "activeTracking": "Tracking Actiu",
+ "noActiveTracking": "Sense tracking actiu",
+ "configuration": "Configuració",
+ "configurationDesc": "Configuració d'ajust fi de detecció de moviment i tracking d'objectes per a la depuració de reproducció de càmera. Cap canvi es graba en el teu arxiu de configuració de Frigate.",
+ "noSession": "No hi ha una sessió activa de reproducció de depuració",
+ "noSessionDesc": "Inicia una reproducció de depuració des de la vista Historial fent clic al botó Accions a la barra d'eines i escollint Depura Repeteix.",
+ "goToRecordings": "Ves a l'historial",
+ "preparingClip": "S'està preparant el clip…",
+ "preparingClipDesc": "Frigate està cosint enregistraments per a l'interval de temps seleccionat. Això pot trigar un minut en intervals més llargs.",
+ "startingCamera": "S'està iniciant la repetició de la depuració…"
+ },
+ "title": "Repetició de depuració",
+ "websocket_messages": "Missatges",
+ "dialog": {
+ "title": "Iniciar Depuració de Repeticions",
+ "camera": "Càmera Font",
+ "timeRange": "Rang de Temps",
+ "preset": {
+ "1m": "Últim 1 Minut",
+ "5m": "Últims 5 Minuts",
+ "timeline": "Desde la Línia de Temps",
+ "custom": "Personalitzat"
+ },
+ "description": "Crea una càmera de reproducció temporal que fa bucles de metratge històric per depurar la detecció d'objectes i els problemes de seguiment. La càmera de reproducció tindrà la mateixa configuració de detecció que la càmera d'origen. Trieu un interval de temps per començar.",
+ "startButton": "Inicia la repetició",
+ "selectFromTimeline": "Selecciona",
+ "starting": "S'està iniciant la repetició...",
+ "startLabel": "Inici",
+ "endLabel": "Final",
+ "toast": {
+ "error": "No s'ha pogut iniciar la repetició de depuració: {{error}}",
+ "alreadyActive": "Ja hi ha activada una sessió de reproducció",
+ "stopError": "No s'ha pogut aturar la repetició de depuració: {{error}}",
+ "goToReplay": "Ves a la repetició"
+ }
+ },
+ "description": "Reprodueix els enregistraments de la càmera per a la depuració. La llista d'objectes mostra un resum retardat en el temps dels objectes detectats i la pestanya Missatges mostra un flux de missatges interns de frigate a partir del metratge de reproducció."
+}
diff --git a/web/public/locales/ca/views/settings.json b/web/public/locales/ca/views/settings.json
index 187132bf8e..97905e75cc 100644
--- a/web/public/locales/ca/views/settings.json
+++ b/web/public/locales/ca/views/settings.json
@@ -15,7 +15,8 @@
"globalConfig": "Configuració global - Frigate",
"cameraConfig": "Configuració de la càmera - Frigate",
"maintenance": "Manteniment - Frigate",
- "profiles": "Perfils - Frigate"
+ "profiles": "Perfils - Frigate",
+ "detectorsAndModel": "Detectors i model - Frigate"
},
"menu": {
"ui": "Interfície d'usuari",
@@ -28,7 +29,7 @@
"frigateplus": "Frigate+",
"enrichments": "Enriquiments",
"triggers": "Disparadors",
- "cameraManagement": "Gestió",
+ "cameraManagement": "Gestió de la càmera",
"cameraReview": "Revisió",
"roles": "Rols",
"general": "General",
@@ -43,7 +44,7 @@
"globalMotion": "Detecció de moviment",
"globalObjects": "Objectes",
"globalReview": "Revisió",
- "globalAudioEvents": "Esdeveniments d'àudio",
+ "globalAudioEvents": "Detecció d'àudio",
"globalLivePlayback": "Reproducció en directe",
"globalTimestampStyle": "Estil de la marca horària",
"systemDatabase": "Base de dades",
@@ -67,13 +68,13 @@
"integrationObjectClassification": "Classificació de l'objecte",
"integrationAudioTranscription": "Transcripció d'àudio",
"cameraDetect": "Detecció d'objectes",
- "cameraFfmpeg": "FFmpeg",
+ "cameraFfmpeg": "Fluxos (FFmpeg)",
"cameraRecording": "Enregistrament",
"cameraSnapshots": "Instantànies",
"cameraMotion": "Detecció de moviment",
"cameraObjects": "Objectes",
"cameraConfigReview": "Revisió",
- "cameraAudioEvents": "Esdeveniments d'àudio",
+ "cameraAudioEvents": "Detecció d'àudio",
"cameraAudioTranscription": "Transcripció d'àudio",
"cameraNotifications": "Notificacions",
"cameraLivePlayback": "Reproducció en directe",
@@ -90,7 +91,8 @@
"regionGrid": "Quadrícula de la regió",
"uiSettings": "Paràmetres de la IU",
"profiles": "Perfils",
- "systemGo2rtcStreams": "go2rtc streams"
+ "systemGo2rtcStreams": "go2rtc streams",
+ "systemDetectorsAndModel": "Detectors i model"
},
"dialog": {
"unsavedChanges": {
@@ -134,7 +136,7 @@
"clearAll": "Esborra tots els paràmetres de transmissió"
},
"recordingsViewer": {
- "title": "Visor d'enregistraments",
+ "title": "Visualitzador d'enregistraments",
"defaultPlaybackRate": {
"label": "Velocitat de reproducció predeterminada",
"desc": "Velocitat de reproducció predeterminada per a la reproducció de gravacions."
@@ -424,7 +426,8 @@
"notificationUnavailable": {
"title": "Notificacions no disponibles",
"documentation": "Llegir la documentació",
- "desc": "Les notificacions push web requereixen un context segur (https://…). Aquesta és una limitació del navegador. Accedeix a Frigate de manera segura per utilitzar les notificacions."
+ "desc": "Les notificacions push web requereixen un context segur (https://…). Aquesta és una limitació del navegador. Accedeix a Frigate de manera segura per utilitzar les notificacions.",
+ "descPwa": "A iOS, les notificacions push web només estàn disponibles quan Frigate està instalat a la pantalla principal. Obre el menú Compartir , selecciona Afegir a la pantalla , i obre Frigate des del nou icona per registrar les notificacions en aquest dispositiu."
},
"unsavedChanges": "Canvis de notificació no desats",
"globalSettings": {
@@ -526,7 +529,7 @@
},
"title": "Afinador de detecció de moviment",
"toast": {
- "success": "Els ajustos de la detecció de moviment s'han desat."
+ "success": "S'han desat els paràmetres del moviment."
},
"unsavedChanges": "Canvis no desats en l'ajust de moviment {{camera}}"
},
@@ -724,7 +727,7 @@
"trainDate": "Data d'entrenament",
"title": "Informació del model",
"supportedDetectors": "Detectors compatibles",
- "availableModels": "Models disponibles",
+ "availableModels": "Models Frigate+ disponibles",
"cameras": "Càmeres",
"plusModelType": {
"userModel": "Afinat",
@@ -733,7 +736,15 @@
"loadingAvailableModels": "Carregant models disponibles…",
"loading": "Carregant informació del model…",
"error": "No s'ha pogut carregar la informació del model",
- "modelSelect": "Els models disponibles a Frigate+ es poden seleccionar aquí. Tingues en compte que només es poden triar els models compatibles amb la configuració actual del detector."
+ "modelSelect": "Els models disponibles a Frigate+ es poden seleccionar aquí. Tingues en compte que només es poden triar els models compatibles amb la configuració actual del detector.",
+ "noModelLoaded": "Actualment no s'ha carregat cap model Frigate+.",
+ "selectModel": "Selecciona un model",
+ "noModelsAvailable": "No hi ha models disponibles",
+ "filter": {
+ "ariaLabel": "Filtra els models per tipus",
+ "baseModels": "Models de base",
+ "fineTunedModels": "Models ajustats"
+ }
},
"apiKey": {
"plusLink": "Llegeix més sobre Frigate+",
@@ -755,29 +766,30 @@
"currentModel": "Model actual",
"otherModels": "Altres models",
"configuration": "Configuració"
- }
+ },
+ "changeInDetectorsAndModel": "Canviar model"
},
"enrichments": {
"semanticSearch": {
"modelSize": {
"small": {
"title": "petit",
- "desc": "L’opció small fa servir una versió quantitzada del model que consumeix menys RAM i s’executa més ràpidament a la CPU, amb una diferència gairebé inapreciable en la qualitat de les incrustacions (embeddings)."
+ "desc": "Si s'utilitza small , s'empra una versió quantitzada del model que consumeix menys memòria RAM i s'executa més ràpidament a la CPU, amb una diferència inapreciable en la qualitat dels vectors."
},
"label": "Mida del model",
"large": {
"title": "gran",
"desc": "L’opció large fa servir el model complet de Jina i s’executarà automàticament a la GPU si està disponible."
},
- "desc": "La mida del model utilitzat per incrustacions de cerca semàntica."
+ "desc": "La mida del model utilitzat per als vectors de la cerca semàntica."
},
"reindexNow": {
"confirmButton": "Reindexar",
"success": "La reindexació ha començat amb èxit.",
"label": "Reindexar ara",
"confirmTitle": "Confirmar la reindexació",
- "desc": "La reindexació regenerarà les incrustacions per a tots els objectes rastrejats. Aquest procés s'executa en segon pla i pot treure el màxim de la CPU i prendre una quantitat de temps raonable depenent del nombre d'objectes rastrejats que tingueu.",
- "confirmDesc": "Estàs segur que vols reindexar totes les incrustacions (embeddings) dels objectes seguits? Aquest procés s’executarà en segon pla, però pot arribar a saturar la CPU i trigar bastant temps. Pots seguir-ne el progrés a la pàgina d’Explora.",
+ "desc": "La reindexació tornarà a generar els vectors de tots els objectes detectats. Aquest procés s'executa en segon pla, pot posar la CPU al màxim i trigar una bona estona segons el nombre d'objectes detectats que tingueu.",
+ "confirmDesc": "Segur que voleu tornar a indexar els vectors de tots els objectes detectats? Aquest procés s'executa en segon pla, però pot posar la CPU al màxim i trigar una bona estona. En podeu veure el progrés a la pàgina Explora.",
"alreadyInProgress": "La reindexació ja està en curs.",
"error": "Error en iniciar la reindexació: {{errorMessage}}"
},
@@ -1048,7 +1060,7 @@
"brands": {
"reolink-rtsp": "No es recomana Reolink RST. Es recomana habilitar HTTP a la configuració de la càmera i reiniciar l'assistent de la càmera."
},
- "customUrlRtspRequired": "Els URL personalitzats han de començar amb \"rtsp://\". Es requereix configuració manual per a fluxos de càmera no RTSP."
+ "customUrlRtspRequired": "Els URL personalitzats han de començar amb \"rtsp://\" o \"rtsps://\". Es requereix configuració manual per a fluxos de càmera no RTSP."
},
"selectBrand": "Seleccioneu la marca de la càmera per a la plantilla d'URL",
"customUrl": "URL de flux personalitzat",
@@ -1280,7 +1292,8 @@
},
"hikvision": {
"substreamWarning": "El substream 1 està bloquejat a una resolució baixa. Moltes càmeres Hikvision suporten subfluxos addicionals que han d'estar habilitats a la configuració de la càmera. Es recomana comprovar i utilitzar aquests corrents si estan disponibles."
- }
+ },
+ "resolutionUnknown": "La resolució d'aquest flux no s'ha pogut investigar. Heu d'establir manualment la resolució de detecció a Configuració o a la configuració."
}
}
},
@@ -1291,13 +1304,45 @@
"selectCamera": "Selecciona una càmera",
"backToSettings": "Torna a la configuració de la càmera",
"streams": {
- "title": "Habilita / Inhabilita les càmeres",
+ "title": "Estat i detalls de la càmera",
"desc": "Inhabilita temporalment una càmera fins que es reiniciï la fragata. La inhabilitació d'una càmera atura completament el processament de Frigate dels fluxos d'aquesta càmera. La detecció, l'enregistrament i la depuració no estaran disponibles. Nota: això no desactiva les retransmissions de go2rtc. ",
"enableLabel": "Càmeres habilitades",
- "enableDesc": "Inhabilita temporalment una càmera habilitada fins que es reiniciï Frigate. La inhabilitació d'una càmera atura completament el processament de Frigate dels fluxos d'aquesta càmera. La detecció, l'enregistrament i la depuració no estaran disponibles. Nota: això no desactiva les retransmissions de go2rtc. ",
+ "enableDesc": "Inhabilita temporalment una càmera habilitada fins que es reiniciï Frigate. La inhabilitació d'una càmera atura completament el processament de Frigate dels fluxos d'aquesta càmera. La detecció, l'enregistrament i la depuració no estaran disponibles. Nota: això no inhabilita els restreams go2rtc. Drag el handle per reordenar les càmeres tal com apareixen a la interfície d'usuari. L'ordre de les càmeres habilitades es reflectirà en tota la interfície d'usuari, incloent el tauler en viu i els desplegables de selecció de càmeres.",
"disableLabel": "Càmeres inhabilitades",
"disableDesc": "Habilita una càmera que actualment no és visible a la interfície d'usuari i està desactivada a la configuració. Es requereix un reinici de Frigate després d'activar-la.",
- "enableSuccess": "{{cameraName}} activat a la configuració. Reinicia Frigate per aplicar els canvis."
+ "enableSuccess": "{{cameraName}} activat. Reinicia Frigate a aplicar.",
+ "friendlyName": {
+ "edit": "Edita el nom de la pantalla de la càmera",
+ "title": "Edita el nom de la pantalla",
+ "description": "Estableix el nom amigable que es mostra per a aquesta càmera a tota la interfície d'usuari de la Fragata. Deixeu-ho en blanc per utilitzar l'ID de la càmera.",
+ "rename": "Canvia el nom"
+ },
+ "reorderHandle": "Arrossega per reordenar",
+ "saving": "S'està desant…",
+ "saved": "Desat",
+ "details": {
+ "edit": "Edita els detalls de la càmera",
+ "title": "Edita els detalls de la càmera",
+ "description": "Actualitza el nom de visualització, l'URL extern i la visibilitat utilitzada per a aquesta càmera a tota la interfície d'usuari de la Fragata.",
+ "friendlyNameLabel": "Nom a mostrar",
+ "friendlyNameHelp": "Nom amistós que es mostra per a aquesta càmera a tota la interfície d'usuari de Frigate. Deixeu-ho en blanc per utilitzar l'ID de la càmera.",
+ "webuiUrlLabel": "URL de la interfície web de la càmera",
+ "webuiUrlHelp": "URL per a visitar la interfície d'usuari web de la càmera directament des de la vista de depuració. Deixeu-ho en blanc per desactivar l'enllaç.",
+ "webuiUrlInvalid": "Ha de ser un URL vàlid (p. ex., https://example.com).",
+ "dashboardLabel": "Mostra al tauler en directe",
+ "dashboardHelp": "Mostra aquesta càmera al Tauler en viu.",
+ "reviewLabel": "Mostra a la ressenya",
+ "reviewHelp": "Mostra aquesta càmera a Revisió, incloent el filtre de càmera, la revisió de moviment i la vista de l'historial."
+ },
+ "label": "Estat de la càmera",
+ "description": "Estableix l'estat operatiu de cada càmera.A : els fluxos es processen normalment.Off : pausa temporalment el processament. No persisteix a través de reinicis de Frigate.Inhabilitat : deixa de processar i desa el canvi a la configuració. Es requereix un reinici per a tornar a habilitar una càmera inhabilitada.Nota: La inhabilitació no afecta els restreams de go2rtc. Arrossegueu l'ansa per a reordenar les càmeres actives a mesura que apareguin a tota la interfície d'usuari, inclosos els desplegables de selecció de quadres en viu i de càmera.",
+ "disabledSubheading": "Desactivat en la configuració",
+ "status": {
+ "on": "Engegat",
+ "off": "Apagat",
+ "disabled": "Desactivat"
+ },
+ "disableSuccess": "{{cameraName}} desactivat i desat a la configuració."
},
"cameraConfig": {
"add": "Afegeix una càmera",
@@ -1343,10 +1388,109 @@
"profiles": {
"title": "Sobreescriu la càmera de perfil",
"selectLabel": "Seleccioneu el perfil",
- "description": "Configura quines càmeres estan habilitades o desactivades quan s'activa un perfil. Les càmeres establertes a «Inherit» mantenen el seu estat base habilitat.",
+ "description": "Configura quines càmeres estan activades o desactivades quan s'activa un perfil. Les càmeres establertes a «herit» mantenen el seu estat per defecte.",
"inherit": "Hereta",
"enabled": "Habilitat",
- "disabled": "Desactivat"
+ "disabled": "Desactivat",
+ "on": "Engegat",
+ "off": "Apagat"
+ },
+ "cameraType": {
+ "title": "Tipus de càmera",
+ "label": "Tipus de càmera",
+ "description": "Estableix el tipus per a cada càmera. Les càmeres LPR dedicades són càmeres d'un sol ús amb un potent zoom òptic per capturar matrícules en vehicles distants. La majoria de les càmeres haurien d'utilitzar el tipus de càmera normal llevat que la càmera sigui específicament per a LPR i tingui una vista molt centrada en les matrícules.",
+ "dedicatedLpr": "LPR dedicat",
+ "saveSuccess": "Tipus de càmera actualitzat per {{cameraName}}. Reinicia Frigate per aplicar els canvis.",
+ "normal": "Normal"
+ },
+ "description": "Afegiu, editeu i suprimiu les càmeres, controleu l'estat de cada càmera, i configureu les superposicions per perfil i tipus de càmera. Per a configurar fluxos, detecció, moviment i altres paràmetres específics de la càmera, trieu la secció específica a Configuració de la càmera.",
+ "clone": {
+ "sectionTitle": "Clona la configuració",
+ "sectionDescription": "Copia la configuració d'una càmera a una altra càmera o una de nova.",
+ "button": "Clona la configuració",
+ "title": "Clona la configuració de la càmera",
+ "description": "Copia la configuració d'una càmera a una o més càmeres o a una càmera nova. La identitat (nom, nom amigable, URL de la interfície d'usuari web, ordre de visualització) no es copia mai.",
+ "source": {
+ "label": "Càmera d'origen",
+ "placeholder": "Seleccioneu una càmera d'origen",
+ "required": "Seleccioneu una càmera d'origen"
+ },
+ "target": {
+ "legend": "Objectiu",
+ "newRadio": "Càmara nova",
+ "newNameLabel": "Nom de la càmera",
+ "newNamePlaceholder": "p. ex., porta enrere orporta o porta posterior",
+ "newNameInvalid": "Es requereix el nom de la càmera",
+ "newNameCollision": "Ja existeix una càmera amb aquest nom",
+ "newStreamsForced": "Els fluxos sempre es copien per a una càmera nova.",
+ "existingCamerasRadio": "Càmeres existents",
+ "allCameras": "Totes les càmeres",
+ "existingPlaceholder": "Selecciona almenys una càmera",
+ "existingDisabled": "No hi ha cap altra càmera a la qual copiar",
+ "newNameRequired": "Es requereix el nom de la càmera"
+ },
+ "categories": {
+ "legend": "Configuració per clonar",
+ "description": "Trieu quina configuració voleu copiar de la càmera d'origen.",
+ "selectAll": "Selecciona-ho tot",
+ "selectNone": "No en seleccioneu cap",
+ "resetDefaults": "Restableix als valors predeterminats",
+ "general": "General",
+ "spatial": "Paràmetres espacials",
+ "streams": "Fluxos",
+ "spatialWarningTitle": "La resolució no coincideix",
+ "spatialWarning": "La càmera d'origen {{srcCamera}} detecta la resolució ({{srcWidth}}.{{srcHeight}}) difereix de: {{cameras}}. És possible que els polígons no s'alineïn en aquestes càmeres. Aquests valors predeterminats estan desactivats; habiliteu-ho per a copiar tal qual.",
+ "restartHint": "Reinicia requerit",
+ "items": {
+ "record": "Enregistrament",
+ "snapshots": "Instantànies",
+ "review": "Revisió",
+ "motion": "Detecció de moviment",
+ "objects": "Objectes",
+ "audio": "Detecció d'àudio",
+ "audio_transcription": "Transcripció d'àudio",
+ "notifications": "Notificacions",
+ "birdseye": "Birdseye",
+ "timestamp_style": "Estil de la marca horària",
+ "lpr": "Reconeixement de la matrícula",
+ "face_recognition": "Reconeixement de cares",
+ "semantic_search": "Cerca semàntica",
+ "genai": "IA generativa",
+ "type": "Tipus de càmera (LPR normal / dedicat)",
+ "profiles": "Perfils",
+ "detect": "Detecta les dimensions",
+ "zones": "Zones",
+ "motion_mask": "Màscares de moviment",
+ "object_masks": "Màscares d'objecte",
+ "ffmpeg_live": "URL i rols de flux",
+ "mqtt": "MQTT",
+ "onvif": "ONVIF"
+ }
+ },
+ "footer": {
+ "changeCount_one": "{{count}} s'aplicarà el canvi",
+ "changeCount_many": "{{count}} canvis s'aplicaran",
+ "changeCount_other": "{{count}} canvis s'aplicaran",
+ "restartNeeded": "Es requerirà reiniciar per a alguns canvis.",
+ "liveOnly": "Tots els canvis s'aplicaran en viu sense reiniciar.",
+ "submit": "Clona",
+ "submitting": "S'està clonant…"
+ },
+ "toast": {
+ "success": "Configuració copiada a {{cameraName}}",
+ "successWithRestart": "Configuració copiada a {{cameraName}}. Reinicia Frigate per aplicar tots els canvis.",
+ "successMulti_one": "Configuració copiada a la càmera {{count}}",
+ "successMulti_many": "Configuració copiada a {{count}} càmeres",
+ "successMulti_other": "Configuració copiada a {{count}} càmeres",
+ "successMultiWithRestart_one": "Configuració copiada a la càmera {{count}}. Reinicia Frigate per aplicar tots els canvis.",
+ "successMultiWithRestart_many": "Configuració copiada a {{count}} càmeres. Reinicia Frigate per aplicar tots els canvis.",
+ "successMultiWithRestart_other": "Configuració copiada a {{count}} càmeres. Reinicia la fragata per aplicar tots els canvis.",
+ "partialFailure": "{{successCount}} seccions aplicades; «{{failedSection}}» ha fallat: {{errorMessage}}",
+ "partialFailureMulti": "S'ha copiat a {{successCount}} càmera(es); ha fallat {{failed}}: {{errorMessage}}",
+ "newCameraPartialFailure": "S'ha creat la càmera {{cameraName}} però no s'han pogut copiar alguns paràmetres: {{errorMessage}}",
+ "sourceMissing": "La càmera d'origen ja no existeix",
+ "submitError": "No s'ha pogut clonar la càmera: {{errorMessage}}"
+ }
}
},
"cameraReview": {
@@ -1469,7 +1613,7 @@
"desc": "La quadrícula de regions és una optimització que aprèn on solen aparèixer objectes de diferents mides en el camp de visió de cada càmera. Frigate utilitza aquestes dades per detectar regions de mida eficient. La quadrícula es construeix automàticament amb el temps a partir de dades d'objectes rastrejats.",
"clear": "Neteja la quadrícula de la regió",
"clearConfirmTitle": "Neteja la quadrícula de la regió",
- "clearConfirmDesc": "No es recomana netejar la quadrícula de la regió tret que hagi canviat recentment la mida del model del detector o hagi canviat la posició física de la càmera i tingui problemes de seguiment d'objectes. La quadrícula es reconstruirà automàticament amb el temps a mesura que els objectes siguin rastrejats. Es requereix un reinici de la fragata perquè els canvis tinguin efecte.",
+ "clearConfirmDesc": "No es recomana netejar la quadrícula de la regió tret que hagi canviat recentment la mida del model del detector o hagi canviat la posició física de la càmera i tingui problemes de seguiment d'objectes. La quadrícula es reconstruirà automàticament amb el temps a mesura que els objectes siguin rastrejats. Es requereix un reinici de Frigate perquè els canvis tinguin efecte.",
"clearSuccess": "La quadrícula de la regió s'ha netejat correctament",
"clearError": "Ha fallat en netejar la graella de la regió",
"restartRequired": "Cal reiniciar per a que els canvis de la quadrícula de la regió tinguin efecte"
@@ -1545,7 +1689,17 @@
}
},
"cameraInputs": {
- "itemTitle": "Flux {{index}}"
+ "itemTitle": "Flux {{index}}",
+ "sourceMode": {
+ "restream": "Restream (go2rtc)",
+ "manual": "Camí d'entrada manual",
+ "go2rtcStreamLabel": "flux go2rtc",
+ "go2rtcStreamPlaceholder": "Selecciona un flux go2rtc",
+ "noGo2rtcStreams": "No s'ha configurat cap flux go2rtc",
+ "go2rtcStreamSearch": "Cerca fluxos...",
+ "availableStreams": "Fluxos disponibles",
+ "noMatchingStreams": "No hi ha fluxos coincidents"
+ }
},
"restartRequiredField": "Reinicia requerit",
"restartRequiredFooter": "S'ha canviat la configuració - es requereix reiniciar",
@@ -1644,9 +1798,11 @@
"searchPlaceholder": "Cerca...",
"genaiRoles": {
"options": {
- "embeddings": "Incrustació",
+ "embeddings": "Vectors",
"vision": "Visió",
- "tools": "Eines"
+ "tools": "Eines",
+ "descriptions": "Descripcions",
+ "chat": "Xat"
}
},
"semanticSearchModel": {
@@ -1659,7 +1815,46 @@
"empty": "No hi ha etiquetes disponibles",
"allNonAlertDetections": "Totes les activitats no alertes s'inclouran com a deteccions."
},
- "addCustomLabel": "Afegeix una etiqueta personalitzada..."
+ "addCustomLabel": "Afegeix una etiqueta personalitzada...",
+ "genaiModel": {
+ "placeholder": "Seleccioneu o introduïu un model…",
+ "search": "Cerca o introdueix un model…",
+ "noModels": "No hi ha models disponibles",
+ "available": "Models disponibles",
+ "useCustom": "Utilitza \"{{value}}\"",
+ "refresh": "Actualitza els models",
+ "probeFailed": "No s'han pogut investigar els models",
+ "fetchedModels": "S'ha obtingut correctament la llista de models"
+ },
+ "knownPlates": {
+ "namePlaceholder": "per exemple. Cotxe de la parella",
+ "platePlaceholder": "Matricula o regex"
+ },
+ "semanticSearchModelSize": {
+ "notApplicable": "No aplicable als proveïdors de GenAI"
+ },
+ "liveStreams": {
+ "streamNameLabel": "Nom del flux",
+ "streamNamePlaceholder": "p. ex., corrent HD principal",
+ "go2rtcStreamLabel": "flux go2rtc",
+ "go2rtcStreamPlaceholder": "Selecciona un flux go2rtc",
+ "go2rtcStreamSearch": "Cerca o introdueix un nom de flux…",
+ "noGo2rtcStreams": "No s'ha configurat cap flux go2rtc",
+ "availableStreams": "Fluxos disponibles",
+ "useCustom": "Utilitza \"{{value}}\"",
+ "addStream": "Afegeix un flux"
+ },
+ "ptzPresets": {
+ "placeholder": "Selecciona o entra una configuració...",
+ "search": "Busca o entra una configuració...",
+ "noPresets": "No hi ha configuracions disponibles",
+ "available": "Parámetres de Cámera",
+ "useCustom": "Usa \"{{value}}\""
+ },
+ "defaultRole": {
+ "admin": "Administrar",
+ "viewer": "Visor"
+ }
},
"globalConfig": {
"title": "Configuració global",
@@ -1694,7 +1889,10 @@
"saveAllPartial_many": "{{successCount}} de {{totalCount}} seccions desades. {{failCount}} ha fallat.",
"saveAllPartial_other": "{{successCount}} de {{totalCount}} seccions desades. {{failCount}} ha fallat.",
"saveAllFailure": "Ha fallat en desar totes les seccions.",
- "applied": "La configuració s'ha aplicat correctament"
+ "applied": "La configuració s'ha aplicat correctament",
+ "saveAllSuccessRestartRequired_one": "S'ha desat la secció {{count}} correctament. Reinicia Frigate per aplicar els canvis.",
+ "saveAllSuccessRestartRequired_many": "Totes les {{count}} seccions s'han desat correctament. Reinicia Frigate per aplicar els canvis.",
+ "saveAllSuccessRestartRequired_other": "Totes les {{count}} seccions s'han desat correctament. Reinicia Frigate per aplicar els canvis."
},
"unsavedChanges": "Teniu canvis sense desar",
"confirmReset": "Confirma el restabliment",
@@ -1704,7 +1902,30 @@
"overriddenGlobal": "Sobreescrit (Global)",
"overriddenGlobalTooltip": "Aquesta càmera anul·la la configuració global d'aquesta secció",
"overriddenBaseConfig": "Sobreescrit (Configuració base)",
- "overriddenBaseConfigTooltip": "El perfil {{profile}} substitueix la configuració d'aquesta secció"
+ "overriddenBaseConfigTooltip": "El perfil {{profile}} substitueix la configuració d'aquesta secció",
+ "overriddenInCameras": {
+ "label_one": "Sobreescrit a la càmera {{count}}",
+ "label_many": "Sobreescrit en {{count}} càmeres",
+ "label_other": "Sobreescrit en {{count}} càmeres",
+ "tooltip_one": "{{count}} la càmera anul·la els valors d'aquesta secció. Feu clic per veure els detalls.",
+ "tooltip_many": "{{count}} càmeres substitueixen els valors d'aquesta secció. Feu clic per veure els detalls.",
+ "tooltip_other": "{{count}} càmeres substitueixen els valors d'aquesta secció. Feu clic per veure els detalls.",
+ "heading_one": "Aquesta secció global té camps que estan sobreescrits a la càmera {{count}}.",
+ "heading_many": "Aquesta secció global té camps que estan sobreescrits en {{count}} càmeres.",
+ "heading_other": "Aquesta secció global té camps que estan sobreescrits en {{count}} càmeres.",
+ "othersField_one": "{{count}} altre",
+ "othersField_many": "{{count}} altres",
+ "othersField_other": "{{count}} altres",
+ "profilePrefix": "Perfil {{profile}}: {{fields}}"
+ },
+ "overriddenGlobalHeading_one": "Aquesta càmera substitueix el camp {{count}} de la configuració global:",
+ "overriddenGlobalHeading_many": "Aquesta càmera anul·la {{count}} camps de la configuració global:",
+ "overriddenGlobalHeading_other": "Aquesta càmera anul·la {{count}} camps de la configuració global:",
+ "overriddenGlobalNoDeltas": "Aquesta càmera anul·la la configuració global, però no hi ha valors de camp diferents.",
+ "overriddenBaseConfigHeading_one": "El perfil {{profile}} substitueix el camp {{count}} de la configuració base:",
+ "overriddenBaseConfigHeading_many": "El perfil {{profile}} substitueix {{count}} camps de la configuració base:",
+ "overriddenBaseConfigHeading_other": "El perfil {{profile}} substitueix {{count}} camps de la configuració base:",
+ "overriddenBaseConfigNoDeltas": "El perfil {{profile}} substitueix aquesta secció, però no hi ha valors de camp diferents de la configuració base."
},
"profiles": {
"title": "Perfils",
@@ -1788,8 +2009,18 @@
"audioMp3": "Transcodifica a MP3",
"audioExclude": "Exclou",
"hardwareNone": "Sense acceleració de hardware",
- "hardwareAuto": "Acceleració de hardware automàtica"
- }
+ "hardwareAuto": "Automàtic (recomanat)",
+ "addVideoCodec": "Afegeix un còdec de vídeo",
+ "addAudioCodec": "Afegeix un còdec d'àudio",
+ "removeCodec": "Elimina el còdec",
+ "hardwareVaapi": "VAAPI",
+ "hardwareCuda": "CUDA",
+ "hardwareV4l2m2m": "V4L2 M2M",
+ "hardwareDxva2": "DXVA2",
+ "hardwareVideotoolbox": "VideoToolbox"
+ },
+ "streamNumber": "Flux {{index}}",
+ "sourceNumber": "Font {{index}}"
},
"timestampPosition": {
"tl": "A dalt a l'esquerra",
@@ -1799,13 +2030,21 @@
},
"onvif": {
"profileAuto": "Automàtic",
- "profileLoading": "S'estan carregant perfils..."
+ "profileLoading": "S'estan carregant perfils...",
+ "autotracking": {
+ "zooming": {
+ "disabled": "Desactivat",
+ "absolute": "Absolut",
+ "relative": "Relatiu"
+ }
+ }
},
"configMessages": {
"review": {
"recordDisabled": "L'enregistrament està desactivat, els elements de revisió no es generaran.",
"detectDisabled": "La detecció d'objectes està desactivada. Els elements de revisió requereixen objectes detectats per categoritzar alertes i deteccions.",
- "allNonAlertDetections": "Totes les activitats no alertes s'inclouran com a deteccions."
+ "allNonAlertDetections": "Totes les activitats no alertes s'inclouran com a deteccions.",
+ "genaiImageSourceRecordingsRecordDisabled": "La font d'imatges està configurada com a 'enregistraments', però l'enregistrament està desactivat. Frigate tornarà a la vista prèvia de les imatges."
},
"audio": {
"noAudioRole": "Cap flux té definit el rol d'àudio. Heu d'habilitar el rol d'àudio per a la detecció d'àudio perquè funcioni."
@@ -1814,15 +2053,24 @@
"audioDetectionDisabled": "La detecció d'àudio no està activada per a aquesta càmera. La transcripció d'àudio requereix que la detecció d'àudio estigui activa."
},
"detect": {
- "fpsGreaterThanFive": "No es recomana establir el detect FPS superior a 5."
+ "fpsGreaterThanFive": "No es recomana establir el detect FPS superior a 5. Els valors més alts poden causar problemes de rendiment i no proporcionaran cap benefici.",
+ "disabled": "La detecció d'objectes està desactivada. Les instantànies, articles de revisió i enriquiments com el reconeixement de rostres, el reconeixement de matrícules i la IA Generativa no funcionaran.",
+ "resolutionShouldBeMultipleOfFour": "Per obtenir els millors resultats, detectar l'amplada i l'alçada han de ser múltiples de 4. Altres valors parells poden produir artefactes visuals o una lleugera distorsió en el flux de detecció.",
+ "aspectRatioMismatch": "L'amplada i alçada que heu introduït no coincideixen amb la relació d'aspecte de la resolució de detecció actual. Això pot produir una imatge estirada o distorsionada.",
+ "maxFramesSet": "La configuració dels fotogrames màxims anul·la el comportament predeterminat i desactiva el seguiment d'objectes estacionaris. Hi ha molt poques situacions en què això sigui necessari, utilitzeu-lo amb precaució.",
+ "squareResolution": "Una resolució de detecció quadrada és inusual. L'amplada i l'alçada de la detecció han de coincidir amb la relació d'aspecte de la càmera (per exemple, 16:9), no amb les dimensions del model de detecció d'objectes. Una relació d'aspecte no coincident pot estirar la imatge i reduir la precisió de detecció.",
+ "resolutionHigh": "Aquesta resolució de detecció és més alta del recomanat i pot causar un ús més elevat dels recursos sense millorar la precisió de detecció. Es recomana una resolució de detecció a o per sota de 1080p per a la majoria de les càmeres.",
+ "globalResolutionMultipleCameras": "S'estableix una resolució de detecció global mentre es configuren diverses càmeres. Tret que totes les càmeres comparteixin la mateixa resolució i relació d'aspecte, l'amplada i l'alçada de la detecció s'haurien de definir per càmera perquè coincideixi amb la relació d'aspecte nativa de cada càmera."
},
"faceRecognition": {
- "globalDisabled": "El reconeixement de cares no està habilitat a nivell global. Habilita-ho en la configuració global per al reconeixement facial a nivell de càmera per funcionar.",
- "personNotTracked": "El reconeixement de cares requereix que l'objecte 'persona' sigui rastrejat. Assegureu-vos que «persona» estigui a la llista de seguiment d'objectes."
+ "globalDisabled": "L'enriquiment del reconeixement facial s'ha d'habilitar perquè les funcions de reconeixement facial funcionin en aquesta càmera.",
+ "personNotTracked": "El reconeixement de cares requereix que l'objecte 'persona' sigui rastrejat. Habilita «persona» en objectes per a aquesta càmera.",
+ "modelSizeLarge": "El model 'gran' requereix una GPU o NPU per a un rendiment raonable. Usa «petit» en sistemes només de CPU."
},
"lpr": {
- "globalDisabled": "El reconeixement de la matrícula no està habilitat a nivell global. Habilita-ho en la configuració global per al funcionament de LPR a nivell de càmera.",
- "vehicleNotTracked": "El reconeixement de la matrícula requereix que es faci un seguiment del 'cotxe' o de la 'motocicleta'."
+ "globalDisabled": "L'enriquiment de reconeixement de matrícules ha d'estar habilitat perquè les funcions LPR funcionin en aquesta càmera.",
+ "vehicleNotTracked": "El reconeixement de la matrícula requereix que es faci un seguiment del 'cotxe' o de la 'motocicleta'.",
+ "modelSizeLarge": "El model 'gran' està optimitzat per a matrícules multilínies. El model 'petit' proporciona un millor rendiment sobre 'gran' i s'ha d'utilitzar tret que la vostra regió utilitzi formats de placa multilínia."
},
"record": {
"noRecordRole": "Cap flux té el rol de registre definit. L'enregistrament no funcionarà."
@@ -1836,6 +2084,122 @@
"detectors": {
"mixedTypes": "Tots els detectors han d'utilitzar el mateix tipus. Elimina els detectors existents per utilitzar un tipus diferent.",
"mixedTypesSuggestion": "Tots els detectors han d'utilitzar el mateix tipus. Suprimiu detectors existents o seleccioneu {{type}}."
+ },
+ "objects": {
+ "genaiNoDescriptionsProvider": "Heu de configurar un proveïdor de GenAI amb el rol 'descripcions' per a les descripcions que es generaran."
+ },
+ "semanticSearch": {
+ "jinav2SmallModelSize": "La mida 'petita' amb el model Jina V2 té un alt cost de RAM i d'inferència. Es recomana el model 'gran' amb una GPU discreta.",
+ "modelSizeIgnoredForProvider": "La mida del model només s'aplica als models de Jina incorporats. Aquest valor s'ignorarà quan s'utilitzi un proveïdor d'incrustació GenAI."
+ },
+ "onvif": {
+ "autotrackingNoZones": "Autotraquejar requereix al menys una zona. Defineix una zona per aquesta cámera a Mascares/Zones, després usa'l com a requerit a la part inferior."
+ },
+ "ffmpeg": {
+ "hwaccelManualNotRecommended": "No es recomanen arguments manuals d'acceleració de maquinari. Tret que existeixi un requisit específic, seleccioneu el predefinit que coincideixi amb el vostre maquinari."
+ },
+ "model": {
+ "optimizedFor320": "Frigate està optimitzada per a un model 320x320, que és la millor opció per a la majoria de configuracions. Un model 640x640 és més lent i només ajuda en escenaris específics.",
+ "inputDimensionsNotDetectResolution": "L'amplada i l'alçada del model d'entrada són les dimensions d'entrada del model de detecció d'objectes, no la resolució de detecció de la càmera. Haurien de coincidir amb les dimensions del model que esteu utilitzant - típicament una mida quadrada com 320x320 o 640x640."
}
+ },
+ "modelSize": {
+ "large": "Gran",
+ "small": "Petit"
+ },
+ "birdseye": {
+ "trackingMode": {
+ "objects": "Objectes",
+ "motion": "Moviment",
+ "continuous": "Continu"
+ },
+ "cameraOrder": {
+ "label": "Ordre de la càmera",
+ "description": "Arrossega les càmeres per establir el seu ordre en la disposició Birdseye.",
+ "reorderHandle": "Arrossega per reordenar",
+ "saving": "S'està desant…",
+ "saved": "Desat"
+ }
+ },
+ "snapshot": {
+ "retainMode": {
+ "all": "Tots",
+ "motion": "Moviment",
+ "active_objects": "Objectes Actius"
+ }
+ },
+ "ui": {
+ "timeFormat": {
+ "browser": "Visor",
+ "12hour": "12 hores",
+ "24hour": "24 hores"
+ },
+ "TimeOrDateStyle": {
+ "full": "Complet",
+ "long": "Llarg",
+ "medium": "Mitjà",
+ "short": "Curt"
+ },
+ "unitSystem": {
+ "metric": "Métric",
+ "imperial": "Imperial"
+ }
+ },
+ "review": {
+ "imageSource": {
+ "recordings": "Gravacions",
+ "previews": "Previsualitzacions"
+ }
+ },
+ "logger": {
+ "logLevel": {
+ "debug": "Depurar",
+ "info": "Informació",
+ "warning": "Avís",
+ "error": "Error",
+ "critical": "Crític"
+ }
+ },
+ "retainMode": {
+ "all": "Tots",
+ "motion": "Moviment",
+ "active_objects": "Objectes actius"
+ },
+ "previewQuality": {
+ "very_high": "Molt alta",
+ "high": "Alta",
+ "medium": "Mitja",
+ "low": "Baix",
+ "very_low": "Molt baix"
+ },
+ "detectorsAndModel": {
+ "restartRequired": "Reinici requerit (canvi en detector o model)",
+ "title": "Detectors i model",
+ "description": "Configuri el detector final que corre la detecció d'objectes i el model que usa. Els canvis es gravaràn junts i així el detector i el model estan sincronitzats.",
+ "cardTitles": {
+ "detector": "Detector Hardware",
+ "model": "Model de detecció"
+ },
+ "tabs": {
+ "plus": "Frigate+",
+ "custom": "Model personalitzat"
+ },
+ "mismatch": {
+ "warning": "El model actual de Frigate+ \"{{model}}\" requereix el detector {{required}}. Selecciona un model compatible a baix o canvía e model personalitzat abans de gravar."
+ },
+ "plusModel": {
+ "requiresDetector": "Requereix: {{detector}}",
+ "noModelSelected": "Selecciona un model Frigate+"
+ },
+ "toast": {
+ "saveSuccess": "Configuració de detectors i model guardats. Reinicia Frigate per aplicar els canvis.",
+ "saveError": "Fallo en gravar la configuració de detector i model"
+ },
+ "unsavedChanges": "Canvis de detector i model no gravats"
+ },
+ "menuDot": {
+ "overrideGlobal": "Aquesta secció substitueix la configuració global",
+ "overrideProfile": "Aquesta secció està substituïda pel perfil {{profile}}",
+ "unsaved": "Aquesta secció té canvis sense desar"
}
}
diff --git a/web/public/locales/ca/views/system.json b/web/public/locales/ca/views/system.json
index 22ecd1fa81..c472937c24 100644
--- a/web/public/locales/ca/views/system.json
+++ b/web/public/locales/ca/views/system.json
@@ -66,10 +66,10 @@
},
"general": {
"detector": {
- "memoryUsage": "Ús de memòria del detector",
+ "memoryUsage": "Ús de la memòria del detector",
"title": "Detectors",
"inferenceSpeed": "Velocitat d'inferència del detector",
- "cpuUsage": "Ús de CPU del detector",
+ "cpuUsage": "Ús de la CPU del detector",
"temperature": "Temperatura del detector",
"cpuUsageInformation": "CPU usada en la preparació d'entrades i sortides desde/cap als models de detecció. Aquest valor no mesura l'utilització d'inferència, encara que usis una GPU o accelerador."
},
@@ -118,11 +118,11 @@
"otherProcesses": {
"title": "Altres processos",
"processMemoryUsage": "Ús de memòria de procés",
- "processCpuUsage": "Ús de la CPU del procés",
+ "processCpuUsage": "Ús de la CPU per procés",
"series": {
"recording": "gravant",
"review_segment": "segment de revisió",
- "embeddings": "incrustacions",
+ "embeddings": "Vectors",
"audio_detector": "detector d'àudio",
"go2rtc": "go2rtc"
}
@@ -191,7 +191,22 @@
},
"audio": "Àudio:",
"cameraProbeInfo": "Informació del sondeig de la càmera {{camera}}",
- "streamDataFromFFPROBE": "Les dades de la transmissió són obtingudes mitjançant ffprobe."
+ "streamDataFromFFPROBE": "Les dades de la transmissió són obtingudes mitjançant ffprobe.",
+ "keyframes": {
+ "title": "Anàlisi de fotogrames clau",
+ "analyzing": "S'estan analitzant els fotogrames clau... queden {{seconds}} segons",
+ "stillAnalyzing": "Encara s'estan analitzant els fotogrames clau...",
+ "recordStream": "Registre de flux:",
+ "keyframeCount": "Fotogrames clau observats:",
+ "observedDuration": "Durada observada:",
+ "gap": "Espai de fotogrames clau (mín / avg / max):",
+ "segmentLength": "Longitud del segment d'enregistrament:",
+ "ok": "Fotogrames clau cada ,{{seconds}}s, bons per enregistrar i reproduir.",
+ "warning": "Els fotogrames clau dispersos o variables (espai més llarg .{{seconds}}s), probablement un còdec intel·ligent (H.264+/H.265+), això no és recomanable.",
+ "error": "El buit dels fotogrames clau ( the{{seconds}}s) excedeix la longitud del segment d'enregistrament ({{segmentTime}}s). Alguns segments poden no tenir un fotograma clau, el qual trenca la reproducció. Desactiva el còdec intel·ligent/+ a la càmera o escurça el seu interval de fotogrames clau.",
+ "unknown": "No s'ha pogut determinar l'espaiat dels fotogrames clau.",
+ "recordDisabled": "L'enregistrament està desactivat per a aquesta càmera."
+ }
},
"title": "Càmeres",
"overview": "Visió general",
@@ -213,11 +228,14 @@
"expectedFps": "FPS esperat",
"reconnectsLastHour": "Reconnecta (última hora)",
"stallsLastHour": "Parades (última hora)"
+ },
+ "noCameras": {
+ "title": "No s'ha trobat cap càmera"
}
},
"lastRefreshed": "Darrera actualització: ",
"stats": {
- "reindexingEmbeddings": "Reindexant incrustacions ({{processed}}% completat)",
+ "reindexingEmbeddings": "Reindexant vectors ({{processed}}% completat)",
"healthy": "El sistema és saludable",
"cameraIsOffline": "{{camera}} està fora de línia",
"ffmpegHighCpuUsage": "{{camera}} te un ús elevat de CPU per FFmpeg ({{ffmpegAvg}}%)",
@@ -231,14 +249,14 @@
"title": "Enriquiments",
"embeddings": {
"face_recognition_speed": "Velocitat de reconeixement facial",
- "image_embedding": "Incrustació d'imatges",
- "text_embedding": "Incrustació de text",
+ "image_embedding": "Vectors d'imatges",
+ "text_embedding": "Vectors de text",
"face_recognition": "Reconeixement de rostres",
"plate_recognition": "Reconeixemnt de matrícules",
- "image_embedding_speed": "Velocitat d'ncrustació d'imatges",
- "face_embedding_speed": "Velocitat d'incrustació de rostres",
+ "image_embedding_speed": "Velocitat de generació de vectors",
+ "face_embedding_speed": "Velocitat de generació de vectors facials",
"plate_recognition_speed": "Velocitat de reconeixement de matrícules",
- "text_embedding_speed": "Velocitat d'incrustació de text",
+ "text_embedding_speed": "Velocitat de generació de vectors de text",
"yolov9_plate_detection": "Detecció de matrícules YOLOv9",
"yolov9_plate_detection_speed": "Velocitat de detecció de matrícules YOLOv9",
"review_description": "Descripció de la revisió",
diff --git a/web/public/locales/cs/views/chat.json b/web/public/locales/cs/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/cs/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/cs/views/explore.json b/web/public/locales/cs/views/explore.json
index e789b0f1dd..2e2532ffc9 100644
--- a/web/public/locales/cs/views/explore.json
+++ b/web/public/locales/cs/views/explore.json
@@ -116,7 +116,9 @@
"error": "Sledovaný objekt se nepodařilo smazat: {{errorMessage}}",
"success": "Sledovaný objekt úspěšně smazán."
}
- }
+ },
+ "previousTrackedObject": "Předchozí sledovaný objekt",
+ "nextTrackedObject": "Následující sledovaný objekt"
},
"objectLifecycle": {
"count": "{{first}} z {{second}}",
@@ -202,6 +204,12 @@
"audioTranscription": {
"label": "Přepsat",
"aria": "Požádat o přepis zvukového záznamu"
+ },
+ "showObjectDetails": {
+ "label": "Zobrazit trasu objektu"
+ },
+ "hideObjectDetails": {
+ "label": "Skrýt trasu objektu"
}
},
"dialog": {
diff --git a/web/public/locales/cs/views/motionSearch.json b/web/public/locales/cs/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/cs/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/cs/views/replay.json b/web/public/locales/cs/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/cs/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/da/audio.json b/web/public/locales/da/audio.json
index 168ef4c679..0d73b074f8 100644
--- a/web/public/locales/da/audio.json
+++ b/web/public/locales/da/audio.json
@@ -180,7 +180,7 @@
"rats": "Rotter",
"patter": "Dråbelyd",
"insect": "Insekt",
- "cricket": "Cricket",
+ "cricket": "Fårekylling",
"guitar": "Guitar",
"electric_guitar": "Elektrisk Guitar",
"bass_guitar": "Basguitar",
@@ -195,5 +195,16 @@
"rimshot": "Kantslag",
"drum_roll": "Trommehvirvel",
"bass_drum": "Stortromme",
- "techno": "Techno"
+ "techno": "Techno",
+ "mosquito": "Myg",
+ "fly": "Flue",
+ "buzz": "Summen",
+ "frog": "Frø",
+ "croak": "Kvæk",
+ "snake": "Slange",
+ "rattle": "Klapren",
+ "whale_vocalization": "Hvallyde",
+ "music": "Musik",
+ "musical_instrument": "Musikinstrument",
+ "plucked_string_instrument": "Strengeinstrument"
}
diff --git a/web/public/locales/da/common.json b/web/public/locales/da/common.json
index 62b6d70361..9b096cabad 100644
--- a/web/public/locales/da/common.json
+++ b/web/public/locales/da/common.json
@@ -24,7 +24,7 @@
"am": "am",
"year_one": "{{time}} år",
"year_other": "{{time}} år",
- "mo": "{{time}}må",
+ "mo": "{{time}}md.",
"month_one": "{{time}} måned",
"month_other": "{{time}} måneder",
"d": "{{time}}d",
@@ -121,18 +121,18 @@
"back": "Tilbage",
"history": "Historik",
"fullscreen": "Fuldskærm",
- "exitFullscreen": "Afslut Fludskærm",
- "pictureInPicture": "Billede i Billede",
- "twoWayTalk": "2 vejs samtale",
- "cameraAudio": "Kamera Lyd",
- "on": "ON",
- "off": "OFF",
+ "exitFullscreen": "Afslut Fuldskærm",
+ "pictureInPicture": "Billede i billede",
+ "twoWayTalk": "Samtale",
+ "cameraAudio": "Kameralyd",
+ "on": "Til",
+ "off": "Fra",
"edit": "Rediger",
"copyCoordinates": "Kopier koordinater",
"delete": "Slet",
"yes": "Ja",
"no": "Nej",
- "download": "Download",
+ "download": "Hent",
"info": "Info",
"suspended": "Sat på pause",
"unsuspended": "Genoptag",
@@ -141,7 +141,19 @@
"export": "Eksporter",
"deleteNow": "Slet nu",
"next": "Næste",
- "continue": "Fortsæt"
+ "continue": "Fortsæt",
+ "add": "Tilføj",
+ "applying": "Anvender…",
+ "undo": "Annuler",
+ "copiedToClipboard": "Kopieret til udklipsholder",
+ "modified": "Ændret",
+ "overridden": "Overskrevet",
+ "resetToGlobal": "Gendan til global",
+ "resetToDefault": "Gendan standard",
+ "saveAll": "Gem alle",
+ "savingAll": "Gemmer alle…",
+ "undoAll": "Fortryd alle",
+ "retry": "Prøv igen"
},
"menu": {
"system": "System",
@@ -194,7 +206,9 @@
"gl": "Galego (Galisisk)",
"id": "Bahasa Indonesia (Indonesisk)",
"ur": "اردو (Urdu)",
- "hr": "Hrvatski (Kroatisk)"
+ "hr": "Hrvatski (Kroatisk)",
+ "zhHant": "繁體中文 (Traditionel Kinesisk)",
+ "bs": "Bosanski (Bosnisk)"
},
"appearance": "Udseende",
"darkMode": {
@@ -243,7 +257,11 @@
"logout": "Log ud",
"setPassword": "Vælg kodeord"
},
- "classification": "Kategorisering"
+ "classification": "Kategorisering",
+ "profiles": "Profiler",
+ "actions": "Handlinger",
+ "features": "Funktioner",
+ "chat": "Chat"
},
"toast": {
"copyUrlToClipboard": "Kopieret URL til udklipsholder.",
@@ -252,13 +270,14 @@
"error": {
"title": "Ændringer kunne ikke gemmes: {{errorMessage}}",
"noMessage": "Kunne ikke gemme konfigurationsændringer"
- }
+ },
+ "success": "Gemte ændringerne i konfigurationen."
}
},
"role": {
"title": "Rolle",
"admin": "Admin",
- "viewer": "Viewer",
+ "viewer": "Seer",
"desc": "Admins har fuld adgang til Frigate UI. Viewers er begrænset til at se kameraer, gennemse items, og historik i UI."
},
"pagination": {
@@ -296,5 +315,10 @@
},
"information": {
"pixels": "{{area}}px"
+ },
+ "no_items": "Intet fundet",
+ "validation_errors": "Valideringsfejl",
+ "credentialField": {
+ "savedPlaceholder": "Gemt - efterlad blank for at bevare nuværende"
}
}
diff --git a/web/public/locales/da/components/camera.json b/web/public/locales/da/components/camera.json
index cc5244b8a2..faff7a09ee 100644
--- a/web/public/locales/da/components/camera.json
+++ b/web/public/locales/da/components/camera.json
@@ -31,7 +31,7 @@
"setting": {
"label": "Kamera Streaming Indstillinger",
"title": "{{cameraName}} Streaming Indstillinger",
- "desc": "Skift de live streaming muligheder for denne kameragruppes dashboard. Disse indstillinger er enheds- og browserspecifikke. ",
+ "desc": "Skift indstillingerne for direkte visning af denne kameragruppes dashboard. Disse indstillinger er enheds- og browserspecifikke. ",
"audioIsAvailable": "Lyd er tilgængelig for denne stream",
"audioIsUnavailable": "Lyd er ikke tilgængelig for denne strøm",
"audio": {
@@ -67,7 +67,10 @@
"desc": "Aktivér kun denne mulighed, hvis kameraets live stream viser farve artefakter og har en diagonal linje på højre side af billedet."
}
}
- }
+ },
+ "showAll": "Vis alle kameragrupper",
+ "showLess": "Vis mindre",
+ "editGroups": "Rediger kameragrupper"
},
"debug": {
"options": {
@@ -81,6 +84,7 @@
"zones": "Zoner",
"mask": "Maske",
"motion": "Bevægelse",
- "regions": "Regioner"
+ "regions": "Regioner",
+ "paths": "Stier"
}
}
diff --git a/web/public/locales/da/components/dialog.json b/web/public/locales/da/components/dialog.json
index a498a33f54..1ab0861830 100644
--- a/web/public/locales/da/components/dialog.json
+++ b/web/public/locales/da/components/dialog.json
@@ -1,6 +1,6 @@
{
"restart": {
- "title": "Er du sikker på at du vil genstarte Frigate?",
+ "title": "Er du sikker på, at du vil genstarte Frigate?",
"button": "Genstart",
"restarting": {
"title": "Frigate genstarter",
@@ -21,8 +21,46 @@
"ask_a": "Er dette objekt et {{label}}?",
"ask_an": "Er dette objekt en {{label}}?",
"ask_full": "Er dette objekt en {{untranslatedLabel}} ({{translatedLabel}})?"
+ },
+ "state": {
+ "submitted": "Indsendt"
+ },
+ "toast": {
+ "error": "Fejl under indsendelse til Frigate+. Kontroller venligst din netværksforbindelse og prøv igen."
}
}
+ },
+ "video": {
+ "viewInHistory": "Se i historik"
}
+ },
+ "export": {
+ "time": {
+ "fromTimeline": "Vælg fra tidslinje",
+ "lastHour_one": "Sidste time",
+ "lastHour_other": "Sidste {{count}} timer",
+ "custom": "Tidsinterval",
+ "start": {
+ "title": "Starttidspunkt",
+ "label": "Vælg starttidspunkt"
+ },
+ "end": {
+ "title": "Sluttidspunkt",
+ "label": "Vælg sluttidspunkt"
+ }
+ },
+ "name": {
+ "placeholder": "Navngiv eksport"
+ },
+ "case": {
+ "newCaseOption": "Opret ny sag",
+ "newCaseNamePlaceholder": "Nyt sagsnavn",
+ "newCaseDescriptionPlaceholder": "Sagsbeskrivelse",
+ "label": "Sag",
+ "nonAdminHelp": "En ny sag oprettes til disse eksporter.",
+ "placeholder": "Vælg en sag"
+ },
+ "select": "Vælg",
+ "export": "Eksporter"
}
}
diff --git a/web/public/locales/da/views/chat.json b/web/public/locales/da/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/da/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/da/views/motionSearch.json b/web/public/locales/da/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/da/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/da/views/replay.json b/web/public/locales/da/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/da/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/da/views/settings.json b/web/public/locales/da/views/settings.json
index 7b5d669ed4..816d0d60e9 100644
--- a/web/public/locales/da/views/settings.json
+++ b/web/public/locales/da/views/settings.json
@@ -14,6 +14,7 @@
"notifications": "Notifikations indstillinger - Frigate"
},
"menu": {
- "ui": "Brugergrænseflade"
+ "ui": "Brugergrænseflade",
+ "profiles": "Profiler"
}
}
diff --git a/web/public/locales/de/audio.json b/web/public/locales/de/audio.json
index 78f4eabb3b..045beee914 100644
--- a/web/public/locales/de/audio.json
+++ b/web/public/locales/de/audio.json
@@ -23,7 +23,7 @@
"toothbrush": "Zahnbürste",
"bicycle": "Fahrrad",
"door": "Tür",
- "keyboard": "Klaviatur",
+ "keyboard": "Klavier",
"bus": "Bus",
"horse": "Pferd",
"cat": "Katze",
@@ -123,7 +123,7 @@
"chicken": "Huhn",
"sitar": "Sitar",
"ukulele": "Ukulele",
- "tapping": "Klopfen",
+ "tapping": "Tippen",
"flapping_wings": "Flügelschlagen",
"strum": "Herumklimpern",
"electronic_organ": "Elektrische Orgel",
diff --git a/web/public/locales/de/common.json b/web/public/locales/de/common.json
index 8924da381e..1ddb0488ba 100644
--- a/web/public/locales/de/common.json
+++ b/web/public/locales/de/common.json
@@ -42,7 +42,7 @@
"untilRestart": "Bis zum Neustart",
"justNow": "Gerade",
"pm": "nachmittags",
- "mo": "{{time}} Mon",
+ "mo": "{{time}} Mon.",
"formattedTimestamp": {
"12hour": "d. MMM, hh:mm:ss aaa",
"24hour": "dd. MMM, hh:mm:ss aaa"
@@ -192,7 +192,9 @@
"bg": "Български (bulgarisch)",
"gl": "Galego (Galicisch)",
"id": "Bahasa Indonesia (Indonesisch)",
- "hr": "Hrvatski (Kroatisch)"
+ "hr": "Hrvatski (Kroatisch)",
+ "bs": "Bosanski (Bosnisch)",
+ "zhHant": "Traditional Chinese"
},
"appearance": "Erscheinung",
"theme": {
@@ -250,7 +252,8 @@
"classification": "Klassifizierung",
"actions": "Aktion",
"chat": "Chat",
- "profiles": "Profile"
+ "profiles": "Profile",
+ "features": "Funktionen"
},
"unit": {
"speed": {
@@ -324,5 +327,8 @@
"separatorWithSpace": ", "
},
"no_items": "Keine Artikel",
- "validation_errors": "Validierungsfehler"
+ "validation_errors": "Validierungsfehler",
+ "credentialField": {
+ "savedPlaceholder": "Gespeichert – leer lassen, um den aktuellen Stand beizubehalten"
+ }
}
diff --git a/web/public/locales/de/components/camera.json b/web/public/locales/de/components/camera.json
index e9f39cb8e7..ff98559e66 100644
--- a/web/public/locales/de/components/camera.json
+++ b/web/public/locales/de/components/camera.json
@@ -66,9 +66,12 @@
"label": "Kameras",
"desc": "Wähle Kameras für diese Gruppe aus."
},
- "label": "Kameragruppen",
+ "label": "Kamera Gruppen",
"edit": "Kameragruppe bearbeiten",
- "success": "Kameragruppe {{name}} wurde gespeichert."
+ "success": "Kameragruppe {{name}} wurde gespeichert.",
+ "showAll": "Alle Kameragruppen anzeigen",
+ "showLess": "Weniger anzeigen",
+ "editGroups": "Kameragruppen bearbeiten"
},
"debug": {
"options": {
diff --git a/web/public/locales/de/components/dialog.json b/web/public/locales/de/components/dialog.json
index e91a68fe4f..fe0db22dc8 100644
--- a/web/public/locales/de/components/dialog.json
+++ b/web/public/locales/de/components/dialog.json
@@ -64,20 +64,80 @@
"toast": {
"error": {
"endTimeMustAfterStartTime": "Die Endzeit darf nicht vor der Startzeit liegen",
- "failed": "Fehler beim Starten des Exports: {{error}}",
+ "failed": "Fehler beim Export in die Warteschlange: {{error}}",
"noVaildTimeSelected": "Kein gültiger Zeitraum ausgewählt"
},
"success": "Export erfolgreich gestartet. Die Datei befindet sich auf der Exportseite.",
- "view": "Ansicht"
+ "view": "Ansicht",
+ "queued": "Export in Warteschlange gestellt. Fortschritt auf der Exportseite verfolgen.",
+ "batchSuccess_one": "1 Export gestartet. Öffne den Fall jetzt.",
+ "batchSuccess_other": "{{count}} Exports gestartet. Öffne den Fall jetzt.",
+ "batchPartial": "{{successful}} von {{total}} Exporten gestartet. Fehlgeschlagene Kameras: {{failedCameras}}",
+ "batchFailed": "Fehler beim Starten der {{total}} Exporte. Fehlgeschlagene Kameras: {{failedCameras}}",
+ "batchQueuedSuccess_one": "1 Export in die Warteschlange gestellt. Fall wird jetzt geöffnet.",
+ "batchQueuedSuccess_other": "{{count}} Exporte in der Warteschlange. Fall wird jetzt geöffnet.",
+ "batchQueuedPartial": "{{successful}} von {{total}} Exporten in die Warteschlange gestellt. Fehlerhafte Kameras: {{failedCameras}}",
+ "batchQueueFailed": "Fehler beim Einreihen von {{total}} Exporten in die Warteschlange. Fehlerhafte Kameras: {{failedCameras}}"
},
"fromTimeline": {
"saveExport": "Export speichern",
- "previewExport": "Exportvorschau"
+ "previewExport": "Exportvorschau",
+ "queueingExport": "Export wird in die Warteschlange gestellt...",
+ "useThisRange": "Nutzen Sie diesen Bereich"
},
"export": "Exportieren",
"case": {
"label": "Fall",
- "placeholder": "Einen Fall auswählen"
+ "placeholder": "Einen Fall auswählen",
+ "newCaseOption": "Einen neuen Fall erstellen",
+ "newCaseNamePlaceholder": "Neuer Fallname",
+ "newCaseDescriptionPlaceholder": "Fall Beschreibung",
+ "nonAdminHelp": "Für diese Exporte wird ein neuer Fall angelegt."
+ },
+ "queueing": "Export wird in die Warteschlange gestellt...",
+ "tabs": {
+ "export": "Einzelne Kamera",
+ "multiCamera": "Mehrere-Kameras"
+ },
+ "multiCamera": {
+ "timeRange": "Zeitbereich",
+ "selectFromTimeline": "Wählen Sie aus der Zeitleiste aus",
+ "cameraSelection": "Kameras",
+ "cameraSelectionHelp": "Kameras, die in diesem Zeitbereich Objekte verfolgen, sind vorausgewählt",
+ "checkingActivity": "Kameraaktivität wird überprüft...",
+ "noCameras": "Keine Kameras verfügbar",
+ "detectionCount_one": "1 verfolgtes Objekt",
+ "detectionCount_other": "{{count}} verfolgte Objekte",
+ "nameLabel": "Exportname",
+ "namePlaceholder": "Optionaler Basisname für diese Exporte",
+ "queueingButton": "Exporte werden in die Warteschlange gestellt...",
+ "exportButton_one": "Export 1 Kamera",
+ "exportButton_other": "xport {{count}} Kameras",
+ "searchOrSelectGroup": "Suchen, oder Kameragruppe auswählen...",
+ "selectAll": "Alle Kameras auswählen",
+ "clearSelection": "Auswahl löschen",
+ "selectWithActivity": "Kameras mit verfolgten Objekten",
+ "selectGroup": "Gruppe auswählen",
+ "noMatchingCameras": "Der Suche entsprechen keine Kameras",
+ "selectedCount": "{{selected}} / {{total}} ausgewählt"
+ },
+ "multi": {
+ "title_one": "1 Bewertung exportieren",
+ "title_other": "{{count}} Bewertung exportieren",
+ "description": "Exportieren Sie jede ausgewählte Rezension. Alle Exporte werden in einem einzigen Fall zusammengefasst.",
+ "descriptionNoCase": "Jede ausgewählte Bewertung exportieren.",
+ "caseNamePlaceholder": "Export prüfen - {{date}}",
+ "exportButton_one": "1 Bewertung exportieren",
+ "exportButton_other": "{{count}} Bewertung exportieren",
+ "exportingButton": "Exportieren...",
+ "toast": {
+ "started_one": "1 Export gestartet. Fall wird jetzt geöffnet.",
+ "started_other": "{{count}} Exporte gestartet. Fall wird jetzt geöffnet.",
+ "startedNoCase_one": "1 Export gestartet.",
+ "startedNoCase_other": "{{count}} Exports gestartet.",
+ "partial": "{{successful}} von {{total}} Exporten gestartet. Fehlgeschlagen: {{failedItems}}",
+ "failed": "Fehler beim Starten der {{total}} Exporte. Fehler: {{failedItems}}"
+ }
}
},
"streaming": {
@@ -125,6 +185,14 @@
"markAsReviewed": "Als geprüft markieren",
"deleteNow": "Jetzt löschen",
"markAsUnreviewed": "Als ungeprüft markieren"
+ },
+ "shareTimestamp": {
+ "label": "Zeitstempel teilen",
+ "title": "Zeitstempel teilen",
+ "description": "Teile eine URL mit Zeitstempel, die die aktuelle Position des Players angibt, oder wähle einen benutzerdefinierten Zeitstempel aus. Beachte, dass es sich hierbei nicht um eine öffentliche Freigabe-URL handelt und dass nur Benutzer Zugriff darauf haben, die Zugriff auf Frigate und diese Kamera haben.",
+ "custom": "Benutzerdefinierter Zeitstempel",
+ "button": "URL des Zeitstempels teilen",
+ "shareTitle": "Zeitstempel der Fregattenbewertung: {{camera}}"
}
},
"imagePicker": {
diff --git a/web/public/locales/de/components/player.json b/web/public/locales/de/components/player.json
index 56a1950539..6294301446 100644
--- a/web/public/locales/de/components/player.json
+++ b/web/public/locales/de/components/player.json
@@ -3,7 +3,8 @@
"noPreviewFound": "Keine Vorschau gefunden",
"submitFrigatePlus": {
"title": "Dieses Bild an Frigate+ senden?",
- "submit": "Senden"
+ "submit": "Absenden",
+ "previewError": "Schnappschuss Vorschau konnte nicht geladen werden. Die Aufnahme ist möglicherweise derzeit nicht verfügbar."
},
"livePlayerRequiredIOSVersion": "iOS 17.1 oder höher ist für diesen Typ eines Live-Streams erforderlich.",
"streamOffline": {
@@ -47,5 +48,6 @@
"submittedFrigatePlus": "Bild erfolgreich an Frigate+ gesendet"
}
},
- "noPreviewFoundFor": "Keine Vorschau für {{cameraName}} gefunden"
+ "noPreviewFoundFor": "Keine Vorschau für {{cameraName}} gefunden",
+ "cameraOff": "Kamera ist ausgeschaltet"
}
diff --git a/web/public/locales/de/config/cameras.json b/web/public/locales/de/config/cameras.json
index 9a0ab8b174..7865c4957c 100644
--- a/web/public/locales/de/config/cameras.json
+++ b/web/public/locales/de/config/cameras.json
@@ -20,12 +20,16 @@
"description": "Mindest-RMS-Lautstärkeschwelle, die für die Audioerkennung erforderlich ist; niedrigere Werte erhöhen die Empfindlichkeit (z. B. 200 hoch, 500 mittel, 1000 niedrig)."
},
"listen": {
- "description": "Liste der zu erkennenden Audioereignisse (z.B: bellen, Feueralarm, schreien, sprechen, rufen).",
+ "description": "Liste der zu erkennenden Audioereignisse (z.B: bellen, Feueralarm, Gespräche, Rufen).",
"label": "Hörtypen"
},
"filters": {
"label": "Audiofilter",
- "description": "Filtereinstellungen pro Audiotyp, wie z. B. Konfidenzschwellenwerte, die zur Reduzierung von Fehlalarmen verwendet werden."
+ "description": "Filtereinstellungen pro Audiotyp, wie z. B. Konfidenzschwellenwerte, die zur Reduzierung von Fehlalarmen verwendet werden.",
+ "threshold": {
+ "label": "Mindestvertrauensgrad für Audio",
+ "description": "Mindestschwellenwert für die Zuverlässigkeit, damit das Audioereignis gezählt wird."
+ }
},
"max_not_heard": {
"label": "Ende Timeout",
@@ -200,11 +204,11 @@
"description": "Einstellungen zum Aktivieren und Verwalten von Benachrichtigungen für diese Kamera."
},
"ffmpeg": {
- "label": "FFmpeg",
- "description": "FFmpeg-Einstellungen, einschließlich Binärpfad, Argumente, hwaccel-Optionen und rollenspezifische Ausgabeargumente.",
+ "label": "Streams (FFmpeg)",
+ "description": "Kamera-Stream-Eingaben und FFmpeg-Optionen, einschließlich Binärpfad, Argumente, hwaccel und rollenspezifische Ausgabeargumente.",
"path": {
"label": "FFmpeg-Pfad",
- "description": "Pfad zur zu verwendenden FFmpeg-Binärdatei oder ein Versionsalias („5.0” oder „7.0”)."
+ "description": "Pfad zur zu verwendenden FFmpeg-Binärdatei oder ein Versionsalias („7.0” oder „8.0”)."
},
"global_args": {
"label": "Globale Argumente von FFmpeg",
@@ -537,6 +541,10 @@
"hwaccel_args": {
"label": "hwaccel-Argumente exportieren",
"description": "Argumente für die Hardwarebeschleunigung bei Export- und Transkodierungsvorgängen."
+ },
+ "max_concurrent": {
+ "label": "Maximale Anzahl gleichzeitiger Exporte",
+ "description": "Maximale Anzahl der gleichzeitig zu verarbeitenden Exportaufträge."
}
},
"preview": {
@@ -762,6 +770,10 @@
"dashboard": {
"label": "In der Benutzeroberfläche anzeigen",
"description": "Schalte ein, ob diese Kamera überall in der Benutzeroberfläche von „Frigate“ sichtbar ist. Wenn du diese Option deaktivierst, musst du die Konfiguration manuell bearbeiten, um diese Kamera wieder in der Benutzeroberfläche anzuzeigen."
+ },
+ "review": {
+ "label": "In der Überprüfung anzeigen",
+ "description": "Legen Sie fest, ob diese Kamera in der Übersicht angezeigt wird (auf der Übersichtsseite sowie im Kamerafilter, in der Bewegungsübersicht und in der Verlaufsansicht)."
}
},
"snapshots": {
@@ -818,7 +830,7 @@
},
"timestamp_style": {
"label": "Format für Zeitstempel",
- "description": "Gestaltungsmöglichkeiten für Zeitstempel im Feed, die auf Aufzeichnungen und Momentaufnahmen angewendet werden.",
+ "description": "Gestaltungsoptionen für Zeitstempel, die auf Momentaufnahmen und die Debug-Ansicht angewendet werden.",
"position": {
"label": "Position des Zeitstempels",
"description": "Position des Zeitstempels auf dem Bild (tl/tr/bl/br)."
diff --git a/web/public/locales/de/config/global.json b/web/public/locales/de/config/global.json
index b7758bfeab..785a503bc4 100644
--- a/web/public/locales/de/config/global.json
+++ b/web/public/locales/de/config/global.json
@@ -18,12 +18,16 @@
"description": "Mindest-RMS-Lautstärkeschwelle, die für die Audioerkennung erforderlich ist; niedrigere Werte erhöhen die Empfindlichkeit (z. B. 200 hoch, 500 mittel, 1000 niedrig)."
},
"listen": {
- "description": "Liste der zu erkennenden Audioereignisse (z.B: bellen, Feueralarm, schreien, sprechen, rufen).",
+ "description": "Liste der zu erkennenden Audioereignisse (z.B: bellen, Feueralarm, Gespräche, Rufen).",
"label": "Hörtypen"
},
"filters": {
"label": "Audiofilter",
- "description": "Filtereinstellungen pro Audiotyp, wie z. B. Konfidenzschwellenwerte, die zur Reduzierung von Fehlalarmen verwendet werden."
+ "description": "Filtereinstellungen pro Audiotyp, wie z. B. Konfidenzschwellenwerte, die zur Reduzierung von Fehlalarmen verwendet werden.",
+ "threshold": {
+ "label": "Mindestvertrauensgrad für Audio",
+ "description": "Mindestschwellenwert für die Zuverlässigkeit, damit das Audioereignis gezählt wird."
+ }
},
"max_not_heard": {
"label": "Ende Timeout",
@@ -376,7 +380,7 @@
"description": "FFmpeg-Einstellungen, einschließlich Binärpfad, Argumente, hwaccel-Optionen und rollenspezifische Ausgabeargumente.",
"path": {
"label": "FFmpeg-Pfad",
- "description": "Pfad zur zu verwendenden FFmpeg-Binärdatei oder ein Versionsalias („5.0” oder „7.0”)."
+ "description": "Pfad zur zu verwendenden FFmpeg-Binärdatei oder ein Versionsalias („7.0” oder „8.0”)."
},
"global_args": {
"label": "Globale Argumente von FFmpeg",
@@ -492,7 +496,7 @@
},
"default_role": {
"label": "Standardrolle",
- "description": "Standardrolle, die proxy-authentifizierten Benutzern zugewiesen wird, wenn keine Rollenzuordnung gilt (Admin oder Betrachter)."
+ "description": "Standardrolle, die proxy-authentifizierten Benutzern zugewiesen wird, wenn keine Rollenzuordnung vorliegt."
},
"separator": {
"label": "Trennzeichen",
@@ -538,8 +542,8 @@
"description": "Aktivieren Sie die prozessbezogene Überwachung der Netzwerkbandbreite für Kamera-FFmpeg-Prozesse und Detektoren (erfordert entsprechende Funktionen)."
},
"intel_gpu_device": {
- "label": "SR-IOV-Gerät",
- "description": "Gerätekennung, die verwendet wird, wenn Intel-GPUs als SR-IOV behandelt werden, um die GPU-Statistiken zu korrigieren."
+ "label": "Intel GPU",
+ "description": "PCI-Bus-Adresse oder DRM-Gerätepfad (z. B. /dev/dri/card1), der verwendet wird, um die Intel-GPU-Statistiken einem bestimmten Gerät zuzuordnen, wenn mehrere vorhanden sind."
}
},
"version_check": {
@@ -1275,6 +1279,41 @@
},
"raw_mask": {
"label": "Rohmaske"
+ },
+ "filters_attribute": {
+ "label": "Attributfilter",
+ "description": "Auf erkannte Attribute angewendete Filter zur Reduzierung von Fehlalarmen (Fläche, Verhältnis, Konfidenz).",
+ "min_area": {
+ "label": "Mindestfläche des Attributs",
+ "description": "Für dieses Attribut erforderliche Mindestfläche des Begrenzungsrahmens (in Pixeln oder Prozent). Kann als Pixelwert (Ganzzahl) oder als Prozentwert (Gleitkommawert zwischen 0,000001 und 0,99) angegeben werden."
+ },
+ "max_area": {
+ "label": "Maximale Attributfläche",
+ "description": "Maximal zulässige Fläche des Begrenzungsrahmens (in Pixeln oder Prozent) für dieses Attribut. Kann als Pixelwert (Ganzzahl) oder als Prozentwert (Gleitkommawert zwischen 0,000001 und 0,99) angegeben werden."
+ },
+ "min_ratio": {
+ "label": "Mindestseitenverhältnis",
+ "description": "Erforderliches Mindestverhältnis von Breite zu Höhe, damit die Begrenzungsbox die Anforderungen erfüllt."
+ },
+ "max_ratio": {
+ "label": "Maximales Seitenverhältnis",
+ "description": "Maximal zulässiges Verhältnis von Breite zu Höhe für die Begrenzungsbox, damit diese die Anforderungen erfüllt."
+ },
+ "threshold": {
+ "label": "Konfidenzschwelle",
+ "description": "Durchschnittlicher Schwellenwert für die Erkennungssicherheit, der erforderlich ist, damit das Merkmal als echtes Positiv gewertet wird."
+ },
+ "min_score": {
+ "label": "Mindestvertrauen",
+ "description": "Mindestwert für die Erkennungssicherheit eines einzelnen Bildes, der erforderlich ist, um dieses Attribut seinem übergeordneten Objekt zuzuordnen."
+ },
+ "mask": {
+ "label": "Filtermaske",
+ "description": "Polygonkoordinaten, die festlegen, wo dieser Filter innerhalb des Bildausschnitts angewendet wird."
+ },
+ "raw_mask": {
+ "label": "Rohmaske"
+ }
}
},
"record": {
@@ -1357,6 +1396,10 @@
"hwaccel_args": {
"label": "hwaccel-Argumente exportieren",
"description": "Argumente für die Hardwarebeschleunigung bei Export- und Transkodierungsvorgängen."
+ },
+ "max_concurrent": {
+ "label": "Maximale Anzahl gleichzeitiger Exporte",
+ "description": "Maximale Anzahl der gleichzeitig zu verarbeitenden Exportaufträge."
}
},
"preview": {
@@ -1710,7 +1753,7 @@
},
"roles": {
"label": "Rollen",
- "description": "GenAI-Rollen (Tools, Vision, Einbettungen); ein Anbieter pro Rolle."
+ "description": "GenAI-Rollen (Nachrichten, Beschreibung, Einbettungen); ein Anbieter pro Rolle."
},
"provider_options": {
"label": "Anbieter Optionen",
@@ -1891,6 +1934,10 @@
"dashboard": {
"label": "In der Benutzeroberfläche anzeigen",
"description": "Schalte ein, ob diese Kamera überall in der Benutzeroberfläche von „Frigate“ sichtbar ist. Wenn du diese Option deaktivierst, musst du die Konfiguration manuell bearbeiten, um diese Kamera wieder in der Benutzeroberfläche anzuzeigen."
+ },
+ "review": {
+ "label": "In der Überprüfung anzeigen",
+ "description": "Legen Sie fest, ob diese Kamera in der Übersicht angezeigt wird (auf der Übersichtsseite sowie im Kamerafilter, in der Bewegungsübersicht und in der Verlaufsansicht)."
}
}
}
diff --git a/web/public/locales/de/config/validation.json b/web/public/locales/de/config/validation.json
index 2bdc76da33..db40fc022c 100644
--- a/web/public/locales/de/config/validation.json
+++ b/web/public/locales/de/config/validation.json
@@ -28,5 +28,8 @@
"detectRequired": "Es muss mindestens ein input stream die Rolle 'erkennen' tragen.",
"hwaccelDetectOnly": "Nur der input-stream mit der Rolle 'erkennen' kann Hardwarebeschleunigungs Argumente definieren."
}
+ },
+ "detect": {
+ "dimensionMustBeEven": "Es muss eine gerade Zahl sein."
}
}
diff --git a/web/public/locales/de/objects.json b/web/public/locales/de/objects.json
index ae767c61db..e2236fc5c8 100644
--- a/web/public/locales/de/objects.json
+++ b/web/public/locales/de/objects.json
@@ -90,7 +90,7 @@
"laptop": "Laptop",
"mouse": "Maus",
"goat": "Ziege",
- "keyboard": "Klaviatur",
+ "keyboard": "Klavier",
"cell_phone": "Handy",
"remote": "Fernbedienung",
"airplane": "Flugzeug",
@@ -121,5 +121,9 @@
"royal_mail": "Royal-Mail",
"school_bus": "Schulbus",
"skunk": "Stinktier",
- "kangaroo": "Känguruh"
+ "kangaroo": "Känguruh",
+ "baby": "Baby",
+ "baby_stroller": "Kinderwagen",
+ "rickshaw": "Rikscha",
+ "rodent": "Nagetier"
}
diff --git a/web/public/locales/de/views/chat.json b/web/public/locales/de/views/chat.json
new file mode 100644
index 0000000000..9f4dcb7f2d
--- /dev/null
+++ b/web/public/locales/de/views/chat.json
@@ -0,0 +1,72 @@
+{
+ "documentTitle": "Chat - Frigate",
+ "title": "Frigate Chat",
+ "subtitle": "Ihr KI-Assistent für die Kameraverwaltung und Analysen",
+ "placeholder": "Frag mich alles...",
+ "error": "Es ist ein Fehler aufgetreten. Bitte versuche es erneut.",
+ "processing": "Wird verarbeitet...",
+ "toolsUsed": "Verwendet: {{tools}}",
+ "showTools": "Werkzeuge anzeigen ({{count}})",
+ "hideTools": "Werkzeuge ausblenden",
+ "call": "Anruf",
+ "result": "Ergebnis",
+ "arguments": "Argumente:",
+ "response": "Antwort:",
+ "attachment_chip_label": "{{label}} auf der {{camera}}",
+ "attachment_chip_remove": "Anhang entfernen",
+ "open_in_explore": "In „Explore“ öffnen",
+ "attach_event_aria": "Ereignis {{eventId}} hinzufügen",
+ "attachment_picker_paste_label": "Oder fügen Sie die Ereignis-ID ein",
+ "attachment_picker_attach": "Anhängen",
+ "attachment_picker_placeholder": "Ereignis hinzufügen",
+ "quick_reply_find_similar": "Ähnliche Sichtungen finden",
+ "quick_reply_tell_me_more": "Erzähl mir mehr darüber",
+ "quick_reply_when_else": "Wann wurde es sonst noch gesehen?",
+ "quick_reply_find_similar_text": "Ähnliche Sichtungen finden.",
+ "quick_reply_tell_me_more_text": "Erzähl mir mehr darüber.",
+ "quick_reply_when_else_text": "Wann gab es das sonst noch?",
+ "anchor": "Referenz",
+ "similarity_score": "Ähnlichkeit",
+ "no_similar_objects_found": "Es wurden keine ähnlichen Objekte gefunden.",
+ "semantic_search_required": "Die semantische Suche muss aktiviert sein, um ähnliche Objekte zu finden.",
+ "send": "Senden",
+ "suggested_requests": "Versuchen Sie doch mal zu fragen:",
+ "starting_requests": {
+ "show_recent_events": "Aktuelle Ereignisse anzeigen",
+ "show_camera_status": "Kamerastatus anzeigen",
+ "recap": "Was ist passiert, während ich weg war?",
+ "watch_camera": "Beobachten Sie eine Kamera auf Bewegungen"
+ },
+ "starting_requests_prompts": {
+ "show_recent_events": "Zeige mir die Ereignisse der letzten Stunde",
+ "show_camera_status": "Wie ist der aktuelle Status meiner Kameras?",
+ "recap": "Was ist passiert, während ich weg war?",
+ "watch_camera": "Pass auf die Haustür auf und sag mir Bescheid, wenn jemand kommt"
+ },
+ "new_chat": "Neuer Chat",
+ "settings": {
+ "title": "Chat Einstellung",
+ "show_stats": {
+ "title": "Statistiken anzeigen",
+ "desc": "Generierungsrate und Kontextgröße für Chat-Antworten anzeigen.",
+ "while_generating": "Während der Erstellung",
+ "always": "Immer"
+ },
+ "auto_scroll": {
+ "title": "Auto scrollen",
+ "desc": "Verfolgen Sie neue Nachrichten, sobald sie eintreffen."
+ }
+ },
+ "stats": {
+ "context": "{{tokens}} tokens",
+ "tokens_per_second": "{{rate}} t/s"
+ },
+ "reasoning": {
+ "active": "Begründung…",
+ "show": "Begründung anzeigen",
+ "hide": "Begründung ausblenden"
+ },
+ "thinking": {
+ "toggle": "Umschalten"
+ }
+}
diff --git a/web/public/locales/de/views/events.json b/web/public/locales/de/views/events.json
index 589a6e1a16..c943bec24a 100644
--- a/web/public/locales/de/views/events.json
+++ b/web/public/locales/de/views/events.json
@@ -25,7 +25,9 @@
},
"documentTitle": "Überprüfung - Frigate",
"recordings": {
- "documentTitle": "Aufnahmen - Frigate"
+ "documentTitle": "Aufnahmen - Frigate",
+ "invalidSharedLink": "Der Link zur zeitgestempelten Aufzeichnung kann aufgrund eines Parsing-Fehlers nicht geöffnet werden.",
+ "invalidSharedCamera": "Der Link zur zeitgestempelten Aufzeichnung kann nicht geöffnet werden, da es sich um eine unbekannte oder nicht autorisierte Kamera handelt."
},
"calendarFilter": {
"last24Hours": "Letzte 24 Stunden"
diff --git a/web/public/locales/de/views/explore.json b/web/public/locales/de/views/explore.json
index 5ca822d746..071d887905 100644
--- a/web/public/locales/de/views/explore.json
+++ b/web/public/locales/de/views/explore.json
@@ -73,7 +73,7 @@
"label": "Schnappschuss Bewertung"
},
"score": {
- "label": "Ergebnis"
+ "label": "Treffer"
},
"editAttributes": {
"title": "Attribute bearbeiten",
@@ -282,7 +282,10 @@
"zones": "Zonen",
"ratio": "Verhältnis",
"area": "Bereich",
- "score": "Bewertung"
+ "score": "Bewertung",
+ "computedScore": "Berechnetes Ergebnis",
+ "topScore": "Bester Treffer",
+ "toggleAdvancedScores": "Erweiterte Ergebnisse umschalten"
}
},
"annotationSettings": {
diff --git a/web/public/locales/de/views/exports.json b/web/public/locales/de/views/exports.json
index 26d3eae16a..da604de91a 100644
--- a/web/public/locales/de/views/exports.json
+++ b/web/public/locales/de/views/exports.json
@@ -14,7 +14,9 @@
"toast": {
"error": {
"renameExportFailed": "Umbenennen des Exports fehlgeschlagen: {{errorMessage}}",
- "assignCaseFailed": "Aktualisierung der Fallzuweisung fehlgeschlagen: {{errorMessage}}"
+ "assignCaseFailed": "Aktualisierung der Fallzuweisung fehlgeschlagen: {{errorMessage}}",
+ "caseSaveFailed": "Fehler beim speichern vom Fall: {{errorMessage}}",
+ "caseDeleteFailed": "Fehler beim löschem vom Fall: {{errorMessage}}"
}
},
"tooltip": {
@@ -22,7 +24,8 @@
"downloadVideo": "Video herunterladen",
"editName": "Name ändern",
"deleteExport": "Export löschen",
- "assignToCase": "Hinzufügen zum Fall"
+ "assignToCase": "Hinzufügen zum Fall",
+ "removeFromCase": "Vom Gehäuse entfernen"
},
"headings": {
"cases": "Fälle",
@@ -35,5 +38,91 @@
"newCaseOption": "Neuen Fall erstellen",
"nameLabel": "Fallname",
"descriptionLabel": "Beschreibung"
+ },
+ "toolbar": {
+ "newCase": "Neuer Fall",
+ "addExport": "Zum expotieren hinzufügen",
+ "editCase": "Fall bearbeiten",
+ "deleteCase": "Fall löschen"
+ },
+ "deleteCase": {
+ "label": "Fall löschen",
+ "desc": "Sind sie sich sicher löschen von{{caseName}}?",
+ "descKeepExports": "Exporte bleiben als nicht kategorisierte Exporte verfügbar.",
+ "descDeleteExports": "Alle Exporte werden in diesem Fall endgültig gelöscht.",
+ "deleteExports": "Exporte auch löschen"
+ },
+ "caseCard": {
+ "emptyCase": "Noch keine Exporte"
+ },
+ "jobCard": {
+ "defaultName": "{{camera}} export",
+ "queued": "In der Warteschlange",
+ "running": "läuft",
+ "preparing": "Vorbereitung",
+ "copying": "kopieren",
+ "encoding": "Codierung",
+ "encodingRetry": "Kodierung (Wiederholung)",
+ "finalizing": "Abschließen"
+ },
+ "caseView": {
+ "noDescription": "keine Beschreibung",
+ "createdAt": "Erstellt {{value}}",
+ "exportCount_one": "1 Export",
+ "exportCount_other": "{{count}} Exports",
+ "cameraCount_one": "1 Kamera",
+ "cameraCount_other": "{{count}} Kameras",
+ "showMore": "Mehr anzeigen",
+ "showLess": "Weniger Anzeigen",
+ "emptyTitle": "Der Fall ist leer",
+ "emptyDescription": "Fügen Sie vorhandene, nicht kategorisierte Exporte hinzu, um den Fall übersichtlich zu halten.",
+ "emptyDescriptionNoExports": "Es sind noch keine nicht kategorisierten Exporte zum Hinzufügen verfügbar."
+ },
+ "caseEditor": {
+ "createTitle": "Fall erstellen",
+ "editTitle": "Fall bearbeiten",
+ "namePlaceholder": "Fall Name",
+ "descriptionPlaceholder": "Fügen Sie Anmerkungen oder Kontext zu diesem Fall hinzu"
+ },
+ "addExportDialog": {
+ "title": "Export zum {{caseName}} hinzufügen",
+ "searchPlaceholder": "Suche nach nicht kategorisierten Exporten",
+ "empty": "Es wurden keine nicht kategorisierten Exporte gefunden, die dieser Suche entsprechen.",
+ "addButton_one": "1 Export hinzufügen",
+ "addButton_other": "Fügen Sie {{count}} Exporte hinzu",
+ "adding": "Hinzufügen..."
+ },
+ "selected_one": "{{count}} ausgewählt",
+ "selected_other": "{{count}} ausgewählt",
+ "bulkActions": {
+ "addToCase": "Zum Fall hinzufügen",
+ "moveToCase": "Zum Fall wechseln",
+ "removeFromCase": "Aus dem Fall nehmen",
+ "delete": "löschen",
+ "deleteNow": "jetzt löschen"
+ },
+ "bulkDelete": {
+ "title": "Exporte löschen",
+ "desc_one": "Möchten Sie den Export {{count}} wirklich löschen?",
+ "desc_other": "Möchten Sie wirklich {{count}} Exporte löschen?"
+ },
+ "bulkRemoveFromCase": {
+ "title": "Aus dem Fall nehmen",
+ "desc_one": "{{count}}-Export aus diesem Fall entfernen?",
+ "desc_other": "{{count}} Exporte aus diesem Fall entfernen?",
+ "descKeepExports": "Die Exporte werden in die Kategorie „Nicht kategorisiert“ verschoben.",
+ "descDeleteExports": "Exporte werden endgültig gelöscht.",
+ "deleteExports": "Löschen Sie stattdessen Exporte"
+ },
+ "bulkToast": {
+ "success": {
+ "delete": "Exporte erfolgreich gelöscht",
+ "reassign": "Fallzuweisung erfolgreich aktualisiert",
+ "remove": "Exporte erfolgreich aus dem Fall entfernt"
+ },
+ "error": {
+ "deleteFailed": "Fehler beim Löschen der Exporte: {{errorMessage}}",
+ "reassignFailed": "Fehler beim Aktualisieren der Fallzuordnung: {{errorMessage}}"
+ }
}
}
diff --git a/web/public/locales/de/views/faceLibrary.json b/web/public/locales/de/views/faceLibrary.json
index d9269fd0ee..7ece861a78 100644
--- a/web/public/locales/de/views/faceLibrary.json
+++ b/web/public/locales/de/views/faceLibrary.json
@@ -48,7 +48,11 @@
"title": "Neueste Erkennungen",
"aria": "Wähle aktuelle Erkennungen",
"empty": "Es gibt keine aktuellen Versuche zur Gesichtserkennung",
- "titleShort": "frisch"
+ "titleShort": "frisch",
+ "emptyNoLibrary": {
+ "title": "Gesicht hinzufügen",
+ "description": "Sie müssen mindestens ein Gesicht zur Bibliothek hinzufügen, damit die Gesichtserkennung funktioniert."
+ }
},
"deleteFaceLibrary": {
"title": "Lösche Name",
diff --git a/web/public/locales/de/views/live.json b/web/public/locales/de/views/live.json
index 854886b363..cfee8b7233 100644
--- a/web/public/locales/de/views/live.json
+++ b/web/public/locales/de/views/live.json
@@ -62,7 +62,8 @@
},
"recording": {
"disable": "Aufzeichnung deaktivieren",
- "enable": "Aufzeichnung aktivieren"
+ "enable": "Aufzeichnung aktivieren",
+ "disabledInConfig": "Aufnahme muss erst in den Einstellung für diese Kamera aktiviert werden."
},
"snapshots": {
"enable": "Schnappschüsse aktivieren",
@@ -143,7 +144,9 @@
},
"camera": {
"enable": "Kamera aktivieren",
- "disable": "Kamera deaktivieren"
+ "disable": "Kamera deaktivieren",
+ "turnOn": "Schalte die Kamera ein",
+ "turnOff": "Schalte die Kamera aus"
},
"audioDetect": {
"enable": "Audioerkennung aktivieren",
@@ -161,7 +164,8 @@
"autotracking": "Autotracking",
"audioDetection": "Audioerkennung",
"title": "{{camera}} Einstellungen",
- "transcription": "Audio Transkription"
+ "transcription": "Audio Transkription",
+ "camera": "Kamera"
},
"history": {
"label": "Historisches Filmmaterial zeigen"
diff --git a/web/public/locales/de/views/motionSearch.json b/web/public/locales/de/views/motionSearch.json
new file mode 100644
index 0000000000..d6aaf98fb1
--- /dev/null
+++ b/web/public/locales/de/views/motionSearch.json
@@ -0,0 +1,80 @@
+{
+ "documentTitle": "Bewegungssuche - Frigate",
+ "title": "Bewegungssuche",
+ "description": "Zeichnen Sie ein Polygon, um den gewünschten Bereich zu definieren, und geben Sie einen Zeitbereich an, um innerhalb dieses Bereichs nach Bewegungsänderungen zu suchen.",
+ "selectCamera": "Die Bewegungssuche wird geladen",
+ "startSearch": "Suche starten",
+ "searchStarted": "Die Suche wurde gestartet",
+ "searchCancelled": "Suche abgebrochen",
+ "cancelSearch": "Abbrechen",
+ "searching": "Suche läuft.",
+ "searchComplete": "Suche abgeschlossen",
+ "noResultsYet": "Führen Sie eine Suche durch, um Bewegungsänderungen im ausgewählten Bereich zu finden",
+ "noChangesFound": "Im ausgewählten Bereich wurden keine Pixeländerungen festgestellt",
+ "changesFound_one": "Es wurde {{count}} Bewegungsänderungen gefunden",
+ "changesFound_other": "Es wurden {{count}} Bewegungsänderungen gefunden",
+ "framesProcessed": "{{count}} Bilder verarbeitet",
+ "jumpToTime": "Zu diesem Zeitpunkt springen",
+ "results": "Ergebnisse",
+ "showSegmentHeatmap": "Heatmap",
+ "newSearch": "Neue Suche",
+ "clearResults": "Eindeutige Ergebnisse",
+ "clearROI": "Polygon löschen",
+ "polygonControls": {
+ "points_one": "{{count}} Punkt",
+ "points_other": "{{count}} Punkte",
+ "undo": "Letzten Schritt rückgängig machen",
+ "reset": "Polygon zurücksetzen",
+ "drawMode": "ziehen",
+ "moveMode": "bewegen"
+ },
+ "motionHeatmapLabel": "Bewegungs-Heatmap",
+ "dialog": {
+ "title": "Bewegungssuche",
+ "cameraLabel": "Kamera",
+ "previewAlt": "Kamera-Vorschau für {{camera}}"
+ },
+ "timeRange": {
+ "title": "Suchbereich",
+ "start": "Startzeit",
+ "end": "Endzeit"
+ },
+ "settings": {
+ "title": "Sucheinstellungen",
+ "parallelMode": "Parallelbetrieb",
+ "parallelModeDesc": "Mehrere Aufzeichnungssegmente gleichzeitig scannen (schneller, aber deutlich rechenintensiver)",
+ "threshold": "Empfindlichkeitsschwelle",
+ "thresholdDesc": "Niedrigere Werte erkennen geringere Veränderungen (1–255)",
+ "minArea": "Mindestwechselbereich",
+ "minAreaDesc": "Mindestgröße eines einzelnen sich bewegenden Bereichs, ausgedrückt als Prozentsatz des untersuchten Bereichs",
+ "frameSkip": "Bild überspringen",
+ "frameSkipDesc": "Verarbeite jeden N-ten Frame. Stelle diesen Wert auf die Bildrate deiner Kamera ein, um einen Frame pro Sekunde zu verarbeiten (z. B. 5 für eine Kamera mit 5 FPS, 30 für eine Kamera mit 30 FPS). Höhere Werte sorgen für eine schnellere Verarbeitung, können jedoch kurze Bewegungsabläufe übersehen.",
+ "maxResults": "Maximale Ergebnisse",
+ "maxResultsDesc": "Nach dieser Anzahl übereinstimmender Zeitstempel anhalten"
+ },
+ "errors": {
+ "noCamera": "Bitte wählen Sie eine Kamera aus",
+ "noROI": "Bitte zeichnen Sie einen Bereich von Interesse ein",
+ "noTimeRange": "Bitte wählen Sie einen Zeitraum aus",
+ "invalidTimeRange": "Die Endzeit muss nach der Startzeit liegen",
+ "searchFailed": "Suche fehlgeschlagen: {{message}}",
+ "polygonTooSmall": "Ein Polygon muss mindestens 3 Punkte haben",
+ "unknown": "Unbekannter Fehler"
+ },
+ "changePercentage": "Um {{percentage}} % verändert",
+ "metrics": {
+ "title": "Suchmetriken",
+ "segmentsScanned": "Durchsuchte Segmente",
+ "segmentsProcessed": "Bearbeitet",
+ "segmentsSkippedInactive": "Übersprungen (keine Aktivität)",
+ "segmentsSkippedHeatmap": "Übersprungen (keine Überschneidung der ROI)",
+ "fallbackFullRange": "Ausweich-Vollbereichsscan",
+ "framesDecoded": "Rahmen decodiert",
+ "wallTime": "Suchzeit",
+ "segmentErrors": "Segmentfehler",
+ "seconds": "{{seconds}}s",
+ "minutesSeconds": "{{minutes}}m {{seconds}}s",
+ "scanSummary": "{{segments}} Segmente · {{time}}"
+ },
+ "scanning": "Wird gescannt {{time}}"
+}
diff --git a/web/public/locales/de/views/replay.json b/web/public/locales/de/views/replay.json
new file mode 100644
index 0000000000..6c28045baa
--- /dev/null
+++ b/web/public/locales/de/views/replay.json
@@ -0,0 +1,59 @@
+{
+ "title": "Debug-Wiedergabe",
+ "description": "Spielen Sie Kameraaufnahmen zur Fehlerbehebung ab. Die Objektliste zeigt eine zeitversetzte Übersicht der erkannten Objekte an, und auf der Registerkarte „Meldungen“ wird ein Stream der internen Meldungen von Frigate aus dem Wiedergabematerial angezeigt.",
+ "websocket_messages": "Nachrichten",
+ "dialog": {
+ "title": "Debug-Wiedergabe starten",
+ "description": "Erstellen Sie eine temporäre Wiedergabekamera, die historisches Bildmaterial in einer Schleife wiedergibt, um Probleme bei der Objekterkennung und -verfolgung zu beheben. Die Wiedergabekamera verfügt über dieselbe Erkennungskonfiguration wie die Quellkamera. Wählen Sie einen Zeitbereich aus, ab dem die Wiedergabe beginnen soll.",
+ "camera": "Quellkamera",
+ "timeRange": "Zeitraum",
+ "preset": {
+ "1m": "Letzte Minute",
+ "5m": "Die letzten 5 Minuten",
+ "timeline": "Aus der Zeitleiste",
+ "custom": "Benutzerdefiniert"
+ },
+ "startButton": "Wiedergabe starten",
+ "selectFromTimeline": "Auswählen",
+ "starting": "Wiedergabe wird gestartet...",
+ "startLabel": "Start",
+ "endLabel": "Ende",
+ "toast": {
+ "error": "Fehler beim Starten der Debug-Wiedergabe: {{error}}",
+ "alreadyActive": "Eine Wiederholungssitzung ist bereits aktiv",
+ "stopError": "Die Wiedergabe der Debug-Daten konnte nicht beendet werden: {{error}}",
+ "goToReplay": "Zur Aufzeichnung"
+ }
+ },
+ "page": {
+ "noSession": "Keine aktive Debug-Wiedergabesitzung",
+ "noSessionDesc": "Starten Sie eine Debug-Wiedergabe aus der Verlaufsansicht, indem Sie in der Symbolleiste auf die Schaltfläche „Aktionen“ klicken und „Debug-Wiedergabe“ auswählen.",
+ "goToRecordings": "Zur Historie",
+ "preparingClip": "Clip wird vorbereitet…",
+ "preparingClipDesc": "Frigate fasst die Aufzeichnungen für den ausgewählten Zeitraum zusammen. Bei längeren Zeiträumen kann dies eine Minute dauern.",
+ "startingCamera": "Debug-Wiedergabe wird gestartet…",
+ "startError": {
+ "title": "Debug Replay konnte nicht gestartet werden",
+ "back": "Zurück zur Übersicht"
+ },
+ "sourceCamera": "Quell Kamera",
+ "replayCamera": "Wiederholungskamera",
+ "initializingReplay": "Debug-Wiedergabe wird initialisiert...",
+ "stoppingReplay": "Debug-Wiedergabe wird angehalten...",
+ "stopReplay": "Stopp Wiederholung",
+ "confirmStop": {
+ "title": "Debug-Wiedergabe anhalten?",
+ "description": "Dadurch wird die Sitzung beendet und alle temporären Daten werden gelöscht. Sind Sie sicher?",
+ "confirm": "Anhalten Wiederholen",
+ "cancel": "Abbrechen"
+ },
+ "activity": "Aktivität",
+ "objects": "Objektliste",
+ "audioDetections": "Audioerkennungen",
+ "noActivity": "Es wurde keine Aktivität festgestellt",
+ "activeTracking": "Aktive Verfolgung",
+ "noActiveTracking": "Keine aktive Nachverfolgung",
+ "configuration": "Konfiguration",
+ "configurationDesc": "Passen Sie die Einstellungen für die Bewegungserkennung und die Objektverfolgung der Debug-Replay-Kamera an. Es werden keine Änderungen in Ihrer Frigate-Konfigurationsdatei gespeichert."
+ }
+}
diff --git a/web/public/locales/de/views/settings.json b/web/public/locales/de/views/settings.json
index 81606f16ee..a0fcfd731f 100644
--- a/web/public/locales/de/views/settings.json
+++ b/web/public/locales/de/views/settings.json
@@ -16,7 +16,8 @@
"globalConfig": "Grundeinstellungen - Frigate",
"cameraConfig": "Kameraeinstellungen - Frigate",
"maintenance": "Wartung - Frigate",
- "profiles": "Profile - Frigate"
+ "profiles": "Profile - Frigate",
+ "detectorsAndModel": "Sensoren und Modell – Frigate"
},
"menu": {
"ui": "Benutzeroberfläche",
@@ -31,7 +32,7 @@
"enrichments": "Erkennungsfunktionen",
"triggers": "Auslöser",
"roles": "Rollen",
- "cameraManagement": "Verwaltung",
+ "cameraManagement": "Kamera Verwaltung",
"cameraReview": "Überprüfung",
"system": "System",
"general": "allgemein",
@@ -45,7 +46,7 @@
"globalMotion": "Bewegungserkennung",
"globalObjects": "Objekte",
"globalReview": "Überprüfung",
- "globalAudioEvents": "Audio Events",
+ "globalAudioEvents": "Audioerkennung",
"globalLivePlayback": "Live-Wiedergabe",
"globalTimestampStyle": "Zeitstempelformat",
"systemDatabase": "Datenbank",
@@ -69,13 +70,13 @@
"integrationObjectClassification": "Objekt Klassifizierung",
"integrationAudioTranscription": "Audio-Transkription",
"cameraDetect": "Objekterkennung",
- "cameraFfmpeg": "FFmpeg",
+ "cameraFfmpeg": "Streams (FFmpeg)",
"cameraRecording": "Aufnahme",
"cameraSnapshots": "Momentaufnahme",
"cameraMotion": "Bewegungserkennung",
"cameraObjects": "Objekte",
"cameraConfigReview": "Überprüfung",
- "cameraAudioEvents": "Audio Evente",
+ "cameraAudioEvents": "Audioerkennung",
"cameraAudioTranscription": "Audio-Transkription",
"cameraNotifications": "Benachrichtigung",
"cameraLivePlayback": "Live-Wiedergabe",
@@ -92,7 +93,8 @@
"uiSettings": "Benutzeroberfläche Einstellung",
"profiles": "Profile",
"systemGo2rtcStreams": "go2rtc-streams",
- "maintenance": "Wartung"
+ "maintenance": "Wartung",
+ "systemDetectorsAndModel": "Detektoren und Modell"
},
"dialog": {
"unsavedChanges": {
@@ -345,6 +347,10 @@
"zone": "Zone",
"motion_mask": "Bewegungsmaske",
"object_mask": "Objektmaske"
+ },
+ "revertOverride": {
+ "title": "Auf Standardkonfiguration zurücksetzen",
+ "desc": "Dadurch wird die Profilüberschreibung für {{type}}{{name}} aufgehoben und die Grundkonfiguration wiederhergestellt."
}
},
"speed": {
@@ -507,7 +513,8 @@
"title": "Aktiviert",
"description": "Ob diese Maske in der Konfigurationsdatei aktiviert ist. Ist sie deaktiviert, kann sie nicht über MQTT aktiviert werden. Deaktivierte Masken werden zur Laufzeit ignoriert."
}
- }
+ },
+ "addDisabledProfile": "Fügen Sie es zuerst der Basiskonfiguration hinzu und überschreiben Sie es dann im Profil"
},
"debug": {
"objectShapeFilterDrawing": {
@@ -723,7 +730,8 @@
"notificationUnavailable": {
"title": "Benachrichtigungen nicht verfügbar",
"desc": "Web Push Benachrichtigungen erfordern einen sicheren Kontext (https://…). Das ist eine Vorgabe des Browsers. Greife auf Frigate gesichert zu um Benachrichtigungen zu nutzen.",
- "documentation": "Dokumentation lesen"
+ "documentation": "Dokumentation lesen",
+ "descPwa": "Unter iOS sind Web-Push-Benachrichtigungen nur verfügbar, wenn Frigate auf Ihrem Startbildschirm installiert ist. Öffnen Sie das Menü Teilen , wählen Sie Zum Startbildschirm hinzufügen und öffnen Sie Frigate über das neue Symbol, um dieses Gerät für Benachrichtigungen zu registrieren."
},
"cameras": {
"desc": "Wähle aus für welche Kameras Benachrichtigungen aktiviert werden sollen.",
@@ -795,10 +803,18 @@
"cameras": "Kameras",
"loading": "Lade Model Informationen…",
"error": "Model Informationen laden fehlgeschlagen",
- "availableModels": "Verfügbare Modelle",
+ "availableModels": "Verfügbare Frigate+ Modelle",
"loadingAvailableModels": "Lade verfügbare Modelle…",
"baseModel": "Basis Model",
- "title": "Model Informationen"
+ "title": "Model Informationen",
+ "noModelLoaded": "Derzeit ist kein „Frigate+“-Modell geladen.",
+ "selectModel": "Wählen Sie ein Modell aus",
+ "noModelsAvailable": "Keine Modelle verfügbar",
+ "filter": {
+ "ariaLabel": "Modelle nach Typ filtern",
+ "baseModels": "Basismodelle",
+ "fineTunedModels": "Optimierte Modelle"
+ }
},
"toast": {
"error": "Speichern der Konfigurationsänderungen fehlgeschlagen: {{errorMessage}}",
@@ -812,7 +828,8 @@
"currentModel": "Aktuelles Modell",
"otherModels": "Anderes Modell",
"configuration": "Konfiguration"
- }
+ },
+ "changeInDetectorsAndModel": "Modell wechseln"
},
"enrichments": {
"birdClassification": {
@@ -1120,7 +1137,7 @@
"brands": {
"reolink-rtsp": "Reolink RTSP wird nicht empfohlen. Es wird empfohlen, http in den Kameraeinstellungen zu aktivieren und den Kamera-Assistenten neu zu starten."
},
- "customUrlRtspRequired": "Benutzerdefinierte URLs müssen mit „rtsp://“ beginnen. Für Nicht-RTSP-Kamerastreams ist eine manuelle Konfiguration erforderlich."
+ "customUrlRtspRequired": "Benutzerdefinierte URLs müssen mit „rtsp://“ oder \"rtsps://\" beginnen. Für Nicht-RTSP-Kamerastreams ist eine manuelle Konfiguration erforderlich."
},
"docs": {
"reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras"
@@ -1328,7 +1345,8 @@
},
"hikvision": {
"substreamWarning": "Substream 1 ist auf eine niedrige Auflösung festgelegt. Viele Hikvision-Kameras unterstützen zusätzliche Substreams, die in den Kameraeinstellungen aktiviert werden müssen. Es wird empfohlen, diese Streams zu überprüfen und zu nutzen, sofern sie verfügbar sind."
- }
+ },
+ "resolutionUnknown": "Die Auflösung dieses Streams konnte nicht ermittelt werden. Sie sollten die Erkennungsauflösung manuell in den Einstellungen oder in Ihrer Konfiguration festlegen."
}
}
},
@@ -1339,13 +1357,45 @@
"selectCamera": "Wähle eine Kamera",
"backToSettings": "Zurück zu Kameraeinstellungen",
"streams": {
- "title": "Kameras aktivieren / deaktivieren",
+ "title": "Kamerastatus und Details",
"desc": "Deaktiviere eine Kamera vorübergehend, bis Frigate neu gestartet wird. Deaktivierung einer Kamera stoppt die Verarbeitung der Streams dieser Kamera durch Frigate vollständig. Erkennung, Aufzeichnung und Debugging sind dann nicht mehr verfügbar. Hinweis: Dies deaktiviert nicht die go2rtc restreams. ",
"enableLabel": "Aktivierte Kameras",
"enableDesc": "Eine aktivierte Kamera vorübergehend deaktivieren, bis Frigate neu gestartet wird. Durch das Deaktivieren einer Kamera wird die Verarbeitung der Streams dieser Kamera durch Frigate vollständig unterbrochen. Erkennung, Aufzeichnung und Fehlerbehebung stehen dann nicht mehr zur Verfügung. Hinweis: go2rtc-Restreams werden dadurch nicht deaktiviert. ",
"disableLabel": "Deaktivierte Kameras",
"disableDesc": "Aktivieren Sie eine Kamera, die derzeit in der Benutzeroberfläche nicht sichtbar und in der Konfiguration deaktiviert ist. Nach der Aktivierung ist ein Neustart von Frigate erforderlich.",
- "enableSuccess": "{{cameraName}} wurde in der Konfiguration aktiviert. Starte Frigate neu, um die Änderungen zu übernehmen."
+ "enableSuccess": "{{cameraName}} wurde aktiviert. Starte Frigate neu, um die Änderung zu übernehmen.",
+ "friendlyName": {
+ "edit": "Anzeigenamen der Kamera bearbeiten",
+ "title": "Anzeigenamen bearbeiten",
+ "description": "Legen Sie den Anzeigenamen fest, der für diese Kamera in der gesamten Benutzeroberfläche von „Frigate“ angezeigt wird. Lassen Sie das Feld leer, um die Kamera-ID zu verwenden.",
+ "rename": "Umbenennen"
+ },
+ "reorderHandle": "Zum Neuanordnen ziehen",
+ "saving": "Speichern…",
+ "saved": "gespeichert",
+ "details": {
+ "edit": "Kameradaten bearbeiten",
+ "title": "Kameradaten bearbeiten",
+ "description": "Aktualisieren Sie den Anzeigenamen, die externe URL und die Sichtbarkeit, die für diese Kamera in der gesamten Frigate-Benutzeroberfläche verwendet werden.",
+ "friendlyNameLabel": "Display Name",
+ "friendlyNameHelp": "Der in der Benutzeroberfläche von „Frigate“ für diese Kamera angezeigte Spitzname. Lassen Sie das Feld leer, um die Kamera-ID zu verwenden.",
+ "webuiUrlLabel": "URL der Web-Benutzeroberfläche",
+ "webuiUrlHelp": "URL, um die Web-Benutzeroberfläche der Kamera direkt aus der Debug-Ansicht aufzurufen. Lassen Sie das Feld leer, um den Link zu deaktivieren.",
+ "webuiUrlInvalid": "Es muss sich um eine gültige URL handeln (z. B. https://example.com).",
+ "dashboardLabel": "Im Live-Dashboard anzeigen",
+ "dashboardHelp": "Diese Kamera im Live-Dashboard anzeigen.",
+ "reviewLabel": "In der Überprüfung anzeigen",
+ "reviewHelp": "Zeige diese Kamera in der Übersicht an, einschließlich des Kamerafilters, der Bewegungsübersicht und der Verlaufsansicht."
+ },
+ "label": "Kamerazustand",
+ "description": "Legen Sie den Betriebszustand für jede Kamera fest.Ein : Streams werden normal verarbeitet.Aus : Die Verarbeitung wird vorübergehend angehalten. Diese Einstellung bleibt bei einem Neustart von Frigate nicht erhalten.Deaktiviert : Die Verarbeitung wird beendet und die Änderung in Ihrer Konfiguration gespeichert. Um eine deaktivierte Kamera wieder zu aktivieren, ist ein Neustart erforderlich.Hinweis: Die Deaktivierung hat keine Auswirkungen auf go2rtc-Restreams. Ziehen Sie den Griff, um die Reihenfolge der aktiven Kameras in der Benutzeroberfläche anzupassen, einschließlich des Live-Dashboards und der Dropdown-Menüs zur Kameraauswahl.",
+ "disabledSubheading": "In der Konfiguration deaktiviert",
+ "status": {
+ "on": "Eingeschaltet",
+ "off": "Ausgeschaltet",
+ "disabled": "Deaktiviert"
+ },
+ "disableSuccess": "{{cameraName}} wurde deaktiviert und in der Konfiguration gespeichert."
},
"cameraConfig": {
"add": "Kamera hinzufügen",
@@ -1391,10 +1441,106 @@
"profiles": {
"title": "Profilkameraumschaltungen",
"selectLabel": "Profil auswählen",
- "description": "Legen Sie fest, welche Kameras bei der Aktivierung eines Profils aktiviert oder deaktiviert werden sollen. Kameras, für die „Übernehmen“ eingestellt ist, behalten ihren ursprünglichen Aktivierungsstatus bei.",
+ "description": "Legen Sie fest, welche Kameras bei der Aktivierung eines Profils ein- oder ausgeschaltet werden. Kameras, die auf „Übernehmen“ eingestellt sind, behalten ihren Standardzustand bei.",
"inherit": "Erben",
"enabled": "Aktiviert",
- "disabled": "Deaktiviert"
+ "disabled": "Deaktiviert",
+ "on": "Eingeschaltet",
+ "off": "Ausgeschaltet"
+ },
+ "cameraType": {
+ "title": "Kamera Art",
+ "label": "Kamera Art",
+ "description": "Legen Sie den Kameratyp für jede Kamera fest. Spezielle LPR-Kameras sind Kameras mit leistungsstarkem optischen Zoom, um Kennzeichen von weit entfernten Fahrzeugen zu erfassen. Für die meisten Kameras sollte der normale Kameratyp verwendet werden, es sei denn, die Kamera ist speziell für LPR vorgesehen und verfügt über einen stark fokussierten Blickwinkel auf die Kennzeichen.",
+ "normal": "Normal",
+ "dedicatedLpr": "Spezielles LPR-System",
+ "saveSuccess": "Der Kameratyp für {{cameraName}} wurde aktualisiert. Starte Frigate neu, um die Änderungen zu übernehmen."
+ },
+ "description": "Fügen Sie Kameras hinzu, bearbeiten und löschen Sie sie, steuern Sie den Status jeder einzelnen Kamera und konfigurieren Sie profil- und kameratypabhängige Übersteuerungen. Um Streams, Erkennung, Bewegung und andere kameraspezifische Einstellungen zu konfigurieren, wählen Sie den entsprechenden Abschnitt unter „Kamerakonfiguration“ aus.",
+ "clone": {
+ "sectionTitle": "Einstellungen klonen",
+ "sectionDescription": "Konfiguration von einer Kamera auf eine andere oder eine neue Kamera kopieren.",
+ "button": "Einstellungen klonen",
+ "title": "Kameraeinstellungen kopieren",
+ "description": "Kopieren Sie die Konfiguration einer Kamera auf eine oder mehrere andere Kameras oder auf eine neue Kamera. Die Identitätsdaten (Name, Anzeigename, URL der Web-Benutzeroberfläche, Anzeigereihenfolge) werden dabei nicht kopiert.",
+ "source": {
+ "label": "Quellkamera",
+ "placeholder": "Wählen Sie eine Quellkamera aus",
+ "required": "Wählen Sie eine Quellkamera aus"
+ },
+ "target": {
+ "legend": "Ziel",
+ "newRadio": "Neue Kamera",
+ "newNameLabel": "Kamera Name",
+ "newNamePlaceholder": "z. B. back_door oder Back Door",
+ "newNameRequired": "Kamera Name ist erforderlich",
+ "newNameInvalid": "ungültiger Kamera Name",
+ "newNameCollision": "Eine Kamera mit diesem Namen gibt es bereits",
+ "newStreamsForced": "Streams werden bei einer neuen Kamera immer kopiert.",
+ "existingCamerasRadio": "Vorhandene Kameras",
+ "allCameras": "Alle Kameras",
+ "existingPlaceholder": "Wählen Sie mindestens eine Kamera aus",
+ "existingDisabled": "Es gibt keine weiteren Kameras, auf die kopiert werden kann"
+ },
+ "categories": {
+ "legend": "Zu klonende Einstellungen",
+ "description": "Wählen Sie aus, welche Einstellungen von der Quellkamera kopiert werden sollen.",
+ "selectAll": "Alle auswählen",
+ "selectNone": "Keine auswählen",
+ "resetDefaults": "Auf Standardwerte zurücksetzen",
+ "general": "Allgemeines",
+ "spatial": "Räumliche Rahmenbedingungen",
+ "streams": "Streams",
+ "spatialWarningTitle": "Auflösungsdiskrepanz",
+ "spatialWarning": "Die Quellkamera {{srcCamera}} hat eine andere Auflösung ({{srcWidth}}×{{srcHeight}}) als: {{cameras}}. Die Polygone sind möglicherweise nicht auf diese Kameras ausgerichtet. Diese Standardeinstellungen sind deaktiviert; aktivieren Sie sie, um die Daten unverändert zu kopieren.",
+ "restartHint": "Neustart erforderlich",
+ "items": {
+ "record": "Aufnahme",
+ "snapshots": "Momentaufnahmen",
+ "motion": "Bewegungserkennung",
+ "objects": "Objekte",
+ "audio": "Tonerkennung",
+ "audio_transcription": "Audio-Transkription",
+ "notifications": "Benachrichtigungen",
+ "birdseye": "Birdseye",
+ "mqtt": "MQTT",
+ "timestamp_style": "Format für Zeitstempel",
+ "onvif": "ONVIF",
+ "lpr": "Kennzeichenerkennung",
+ "face_recognition": "Gesichtserkennung",
+ "semantic_search": "Semantische Suche",
+ "genai": "Generative AI",
+ "type": "Kameratyp (Standard / speziell für Kennzeichenerkennung)",
+ "profiles": "Profile",
+ "detect": "Abmessungen ermitteln",
+ "zones": "Zonen",
+ "motion_mask": "Bewegungsmaske",
+ "object_masks": "Objektmaske",
+ "ffmpeg_live": "Stream-URLs und Rollen",
+ "review": "Rezension"
+ }
+ },
+ "footer": {
+ "restartNeeded": "Für einige Änderungen ist ein Neustart erforderlich.",
+ "liveOnly": "Alle Änderungen werden sofort wirksam, ohne dass ein Neustart erforderlich ist.",
+ "submit": "Klon",
+ "submitting": "Klonen…",
+ "changeCount_one": "Die Änderung von {{count}} wird übernommen",
+ "changeCount_other": "Die Änderungen von {{count}} werden übernommen"
+ },
+ "toast": {
+ "success": "Einstellungen wurden auf {{cameraName}} kopiert",
+ "successWithRestart": "Die Einstellungen wurden auf {{cameraName}} kopiert. Starte Frigate neu, um alle Änderungen zu übernehmen.",
+ "successMulti_one": "Einstellungen wurden auf {{count}} Kamera kopiert",
+ "successMulti_other": "Einstellungen wurden auf {{count}} Kameras kopiert",
+ "successMultiWithRestart_one": "Die Einstellungen wurden auf die Kamera {{count}} kopiert. Starte Frigate neu, um alle Änderungen zu übernehmen.",
+ "successMultiWithRestart_other": "Die Einstellungen wurden auf {{count}} Kameras kopiert. Starten Sie Frigate neu, um alle Änderungen zu übernehmen.",
+ "partialFailure": "{{successCount}} Abschnitte wurden angewendet; '{{failedSection}}' ist fehlgeschlagen: {{errorMessage}}",
+ "partialFailureMulti": "{{successCount}} Kamera(s) wurden kopiert; bei {{failed}} ist ein Fehler aufgetreten: {{errorMessage}}",
+ "newCameraPartialFailure": "Die Kamera {{cameraName}} wurde erstellt, einige Einstellungen konnten jedoch nicht kopiert werden: {{errorMessage}}",
+ "sourceMissing": "Die Quellkamera existiert nicht mehr",
+ "submitError": "Das Klonen der Kamera ist fehlgeschlagen: {{errorMessage}}"
+ }
}
},
"cameraReview": {
@@ -1458,7 +1604,24 @@
"overriddenGlobalTooltip": "Diese Kamera überschreibt globale Konfigurationseinstellungen in diesem Abschnitt",
"overriddenBaseConfig": "Überschrieben (Basiskonfiguration)",
"overriddenBaseConfigTooltip": "Das {{profile}}-Profil überschreibt Konfigurationseinstellungen in diesem Abschnitt",
- "overriddenGlobal": "Überschrieben (Global)"
+ "overriddenGlobal": "Überschrieben (Global)",
+ "overriddenInCameras": {
+ "label_one": "In {{count}} Kamera überschrieben",
+ "label_other": "In {{count}} Kameras überschrieben",
+ "tooltip_one": "Die Kamera mit der Nummer {{count}} überschreibt die Werte in diesem Abschnitt. Klicken Sie hier, um Details anzuzeigen.",
+ "tooltip_other": "Die Kamera mit der Nummer {{count}} überschreibt die Werte in diesem Abschnitt. Klicken Sie hier, um Details anzuzeigen.",
+ "heading_one": "Dieser globale Abschnitt enthält Felder, die in {{count}} Kamera überschrieben werden.",
+ "heading_other": "Dieser globale Abschnitt enthält Felder, die bei {{count}} Kameras überschrieben werden.",
+ "othersField_one": "{{count}} andere",
+ "othersField_other": "{{count}} weitere",
+ "profilePrefix": "{{profile}} Profile: {{fields}}"
+ },
+ "overriddenGlobalHeading_one": "Diese Kamera überschreibt das Feld {{count}} aus der globalen Konfiguration:",
+ "overriddenGlobalHeading_other": "Diese Kamera überschreibt alle Felder {{count}} aus der globalen Konfiguration:",
+ "overriddenGlobalNoDeltas": "Diese Kamera überschreibt die globale Konfiguration, es gibt jedoch keine Abweichungen bei den Feldwerten.",
+ "overriddenBaseConfigHeading_one": "Das Profil {{profile}} überschreibt das Feld {{count}} aus der Basiskonfiguration:",
+ "overriddenBaseConfigHeading_other": "Das Profil {{profile}} überschreibt di Felder {{count}} aus der Basiskonfiguration:",
+ "overriddenBaseConfigNoDeltas": "Das Profil {{profile}} überschreibt diesen Abschnitt, jedoch weichen keine Feldwerte von der Basiskonfiguration ab."
},
"timestampPosition": {
"tl": "Oben links",
@@ -1486,7 +1649,7 @@
"currentStatus": "Status",
"jobId": "Job ID",
"startTime": "Startzeit",
- "endTime": "Endzeit",
+ "endTime": "End Zeit",
"statusLabel": "Status",
"results": "Ergebnisse",
"errorLabel": "Fehler",
@@ -1605,7 +1768,17 @@
}
},
"cameraInputs": {
- "itemTitle": "Stream {{index}}"
+ "itemTitle": "Stream {{index}}",
+ "sourceMode": {
+ "restream": "Restream (go2rtc)",
+ "manual": "Pfad für die manuelle Eingabe",
+ "go2rtcStreamLabel": "go2rtc stream",
+ "go2rtcStreamPlaceholder": "Wählen Sie einen go2rtc-Stream aus",
+ "noGo2rtcStreams": "Es sind keine go2rtc-Streams konfiguriert",
+ "go2rtcStreamSearch": "Suche Streams...",
+ "availableStreams": "Verfügbare Streams",
+ "noMatchingStreams": "Keine passenden Streams"
+ }
},
"restartRequiredField": "Neustart erforderlich",
"restartRequiredFooter": "Konfiguration geändert – Neustart erforderlich",
@@ -1647,7 +1820,8 @@
"keyDuplicate": "Der Name des Detektors ist bereits vorhanden.",
"noSchema": "Es sind keine Detektorschemata verfügbar.",
"none": "Es sind keine Detektorinstanzen konfiguriert.",
- "add": "Detektor hinzufügen"
+ "add": "Detektor hinzufügen",
+ "addCustomKey": "Benutzter Schlüssel hinzufügen"
},
"record": {
"title": "Aufnahmeeinstellungen"
@@ -1694,7 +1868,9 @@
"options": {
"embeddings": "Einbetten",
"vision": "Vision",
- "tools": "Werkzeuge"
+ "tools": "Werkzeuge",
+ "descriptions": "Beschreibung",
+ "chat": "Chat"
}
},
"semanticSearchModel": {
@@ -1718,7 +1894,46 @@
"title": "Einstellungen für Zeitstempel"
},
"searchPlaceholder": "Suche...",
- "addCustomLabel": "Benutzerdefiniertes Etikett hinzufügen..."
+ "addCustomLabel": "Benutzerdefiniertes Etikett hinzufügen...",
+ "knownPlates": {
+ "namePlaceholder": "z.B. das Auto der Frau",
+ "platePlaceholder": "Kennzeichen oder regulärer Ausdruck"
+ },
+ "genaiModel": {
+ "placeholder": "Modell auswählen oder eingeben…",
+ "search": "Modell suchen oder eingeben…",
+ "noModels": "Keine Modelle verfügbar",
+ "available": "Verfügbare Modelle",
+ "useCustom": "Verwende „{{value}}“",
+ "refresh": "Modelle aktualisieren",
+ "probeFailed": "Das Abrufen der Modelle ist fehlgeschlagen",
+ "fetchedModels": "Modellliste erfolgreich abgerufen"
+ },
+ "semanticSearchModelSize": {
+ "notApplicable": "Gilt nicht für GenAI-Anbieter"
+ },
+ "liveStreams": {
+ "streamNameLabel": "Streamname",
+ "streamNamePlaceholder": "z. B. Haupt-HD-Stream",
+ "go2rtcStreamLabel": "go2rtc stream",
+ "go2rtcStreamPlaceholder": "Wählen Sie einen go2rtc-Stream aus",
+ "go2rtcStreamSearch": "Suchen Sie nach einem Streamnamen oder geben Sie ihn ein…",
+ "noGo2rtcStreams": "Es sind keine go2rtc-Streams konfiguriert",
+ "availableStreams": "Verfügbare Streams",
+ "useCustom": "Verwende „{{value}}“",
+ "addStream": "Stream hinzufügen"
+ },
+ "ptzPresets": {
+ "placeholder": "Wählen Sie eine Voreinstellung aus oder geben Sie eine ein...",
+ "search": "Suchen oder eine Voreinstellung eingeben...",
+ "noPresets": "Es sind keine Voreinstellungen verfügbar",
+ "available": "Kamera-Voreinstellungen",
+ "useCustom": "Verwende „{{value}}“"
+ },
+ "defaultRole": {
+ "admin": "Admin",
+ "viewer": "Betrachter"
+ }
},
"globalConfig": {
"title": "Globale Konfiguration",
@@ -1751,7 +1966,9 @@
"saveAllSuccess_other": "Alle {{count}} Abschnitte wurden erfolgreich gespeichert.",
"saveAllPartial_one": "{{successCount}} von {{totalCount}} Abschnitt wurden gespeichert. {{failCount}} sind fehlgeschlagen.",
"saveAllPartial_other": "{{successCount}} von {{totalCount}} Abschnitten wurden gespeichert. {{failCount}} sind fehlgeschlagen.",
- "saveAllFailure": "Es konnten nicht alle Abschnitte gespeichert werden."
+ "saveAllFailure": "Es konnten nicht alle Abschnitte gespeichert werden.",
+ "saveAllSuccessRestartRequired_one": "Der Abschnitt {{count}} wurde erfolgreich gespeichert. Starte Frigate neu, um die Änderungen zu übernehmen.",
+ "saveAllSuccessRestartRequired_other": "Alle {{count}} Abschnitte wurden erfolgreich gespeichert. Starte Frigate neu, um die Änderungen zu übernehmen."
},
"profiles": {
"title": "Profile",
@@ -1838,18 +2055,36 @@
"audioMp3": "Transcode zu MP3",
"audioExclude": "Ausschließen",
"hardwareNone": "Keine Hardwarebeschleunigung",
- "hardwareAuto": "Automatische Hardwarebeschleunigung"
- }
+ "hardwareAuto": "Automatisch (empfohlen)",
+ "hardwareVaapi": "VAAPI",
+ "hardwareCuda": "CUDA",
+ "hardwareV4l2m2m": "V4L2 M2M",
+ "hardwareDxva2": "DXVA2",
+ "hardwareVideotoolbox": "VideoToolbox",
+ "addVideoCodec": "Videocodec hinzufügen",
+ "addAudioCodec": "Audio-Codec hinzufügen",
+ "removeCodec": "Codec entfernen"
+ },
+ "streamNumber": "Stream {{index}}",
+ "sourceNumber": "Quelle {{index}}"
},
"onvif": {
"profileAuto": "Auto",
- "profileLoading": "Profile werden geladen..."
+ "profileLoading": "Profile werden geladen...",
+ "autotracking": {
+ "zooming": {
+ "disabled": "deaktiviert",
+ "absolute": "Absolut",
+ "relative": "Verwandter"
+ }
+ }
},
"configMessages": {
"review": {
"recordDisabled": "Aufnahme ist deaktiviert, Überprüfungspunkt konnte nicht erstellt werden.",
"detectDisabled": "Die Objekterkennung ist deaktiviert. Für die Überprüfung von Elementen müssen Objekte erkannt werden, um Warnmeldungen und Erkennungen zu kategorisieren.",
- "allNonAlertDetections": "Alle Aktivitäten, die keine Warnmeldungen auslösen, werden als Erkennungen erfasst."
+ "allNonAlertDetections": "Alle Aktivitäten, die keine Warnmeldungen auslösen, werden als Erkennungen erfasst.",
+ "genaiImageSourceRecordingsRecordDisabled": "Als Bildquelle ist „Aufnahmen“ eingestellt, die Aufnahmefunktion ist jedoch deaktiviert. Frigate greift in diesem Fall auf Vorschaubilder zurück."
},
"audio": {
"noAudioRole": "Für keinen Stream ist die Audio-Rolle definiert. Sie müssen die Audio-Rolle aktivieren, damit die Audioerkennung funktioniert."
@@ -1858,15 +2093,24 @@
"audioDetectionDisabled": "Die Audioerkennung ist für diese Kamera nicht aktiviert. Für die Audio-Transkription muss die Audioerkennung aktiviert sein."
},
"detect": {
- "fpsGreaterThanFive": "Es wird nicht empfohlen, den Wert für die FPS-Erkennung auf mehr als 5 einzustellen."
+ "fpsGreaterThanFive": "Es wird nicht empfohlen, den Wert für die FPS-Erkennung auf mehr als 5 zu setzen. Höhere Werte können zu Leistungseinbußen führen und bieten keinerlei Vorteile.",
+ "disabled": "Die Objekterkennung ist deaktiviert. Momentaufnahmen, Überprüfungselemente und Erweiterungsfunktionen wie Gesichtserkennung, Kennzeichenerkennung und generative KI funktionieren nicht.",
+ "resolutionShouldBeMultipleOfFour": "Um optimale Ergebnisse zu erzielen, sollten Breite und Höhe ein Vielfaches von 4 sein. Andere gerade Werte können zu visuellen Artefakten oder leichten Verzerrungen im Erkennungsstrom führen.",
+ "aspectRatioMismatch": "Die von Ihnen eingegebene Breite und Höhe stimmen nicht mit dem Seitenverhältnis Ihrer aktuell erkannten Auflösung überein. Dies kann zu einem gestreckten oder verzerrten Bild führen.",
+ "maxFramesSet": "Die Festlegung einer maximalen Bildrate überschreibt das Standardverhalten und deaktiviert die Verfolgung stationärer Objekte. Dies ist nur in sehr wenigen Fällen erforderlich; verwenden Sie diese Option daher mit Bedacht.",
+ "squareResolution": "Eine quadratische Erkennungsauflösung ist ungewöhnlich. Die Erkennungsbreite und -höhe sollten dem Seitenverhältnis Ihrer Kamera entsprechen (zum Beispiel 16:9) und nicht den Abmessungen des Objekterkennungsmodells. Ein nicht übereinstimmendes Seitenverhältnis kann das Bild verzerren und die Erkennungsgenauigkeit beeinträchtigen.",
+ "resolutionHigh": "Diese Erkennungsauflösung liegt über der empfohlenen Wert und kann zu einem erhöhten Ressourcenverbrauch führen, ohne die Erkennungsgenauigkeit zu verbessern. Für die meisten Kameras wird eine Erkennungsauflösung von maximal 1080p empfohlen.",
+ "globalResolutionMultipleCameras": "Bei der Konfiguration mehrerer Kameras wird eine globale Erkennungsauflösung festgelegt. Sofern nicht alle Kameras dieselbe Auflösung und dasselbe Seitenverhältnis aufweisen, sollten die Erkennungsbreite und -höhe für jede Kamera separat festgelegt werden, um dem nativen Seitenverhältnis der jeweiligen Kamera zu entsprechen."
},
"faceRecognition": {
- "globalDisabled": "Die Gesichtserkennung ist auf globaler Ebene nicht aktiviert. Aktivieren Sie sie in den globalen Einstellungen, damit die Gesichtserkennung auf Kameraebene funktioniert.",
- "personNotTracked": "Für die Gesichtserkennung muss das Objekt „person“ verfolgt werden. Stellen Sie sicher, dass „person“ in der Objektverfolgungsliste enthalten ist."
+ "globalDisabled": "Die Gesichtserkennungserweiterung muss aktiviert sein, damit die Gesichtserkennungsfunktionen bei dieser Kamera funktionieren.",
+ "personNotTracked": "Für die Gesichtserkennung muss das Objekt „Person“ verfolgt werden. Aktivieren Sie „Person“ unter „Objekte“ für diese Kamera.",
+ "modelSizeLarge": "Das „große“ Modell erfordert eine GPU oder NPU, um eine angemessene Leistung zu erzielen. Verwenden Sie auf reinen CPU-Systemen die Option „klein“."
},
"lpr": {
- "globalDisabled": "Die Kennzeichenerkennung ist auf globaler Ebene nicht aktiviert. Aktivieren Sie sie in den globalen Einstellungen, damit die Kennzeichenerkennung auf Kameraebene funktioniert.",
- "vehicleNotTracked": "Für die Kennzeichenerkennung muss entweder ein „Pkw“ oder ein „Motorrad“ erfasst werden."
+ "globalDisabled": "Die Erweiterung zur Kennzeichenerkennung muss aktiviert sein, damit die LPR-Funktionen bei dieser Kamera funktionieren.",
+ "vehicleNotTracked": "Für die Kennzeichenerkennung muss entweder „Auto“ oder „Motorrad“ erfasst werden. Aktivieren Sie „Auto“ oder „Motorrad“ unter „Objekte“ für diese Kamera.",
+ "modelSizeLarge": "Das „große“ Modell ist für mehrzeilige Kennzeichen optimiert. Das „kleine“ Modell bietet eine bessere Leistung als das „große“ und sollte verwendet werden, sofern in Ihrer Region keine mehrzeiligen Kennzeichenformate verwendet werden."
},
"record": {
"noRecordRole": "Für keinen Stream ist die Rolle „Record“ definiert. Die Aufzeichnung funktioniert nicht."
@@ -1876,6 +2120,118 @@
},
"snapshots": {
"detectDisabled": "Die Objekterkennung ist deaktiviert. Es werden keine Momentaufnahmen von verfolgten Objekten erstellt."
+ },
+ "detectors": {
+ "mixedTypes": "Alle Detektoren müssen von gleichem Typ sein, Entferne bestehende Detektoren um einen anderen Typ zu benutzen.",
+ "mixedTypesSuggestion": "Alle Detektoren müssen vom gleichem Typ sein. Entferne bestehende oder wähle {{type}}."
+ },
+ "objects": {
+ "genaiNoDescriptionsProvider": "Sie müssen einen GenAI-Anbieter mit der Rolle „Beschreibung“ konfigurieren, damit Beschreibungen generiert werden können."
+ },
+ "semanticSearch": {
+ "jinav2SmallModelSize": "Die „kleine“ Variante des Jina V2-Modells verursacht hohe RAM- und Inferenzkosten. Es wird das „große“ Modell mit einer dedizierten GPU empfohlen."
+ },
+ "onvif": {
+ "autotrackingNoZones": "Für die automatische Verfolgung ist mindestens eine Zone erforderlich. Definieren Sie unter „Masken / Zonen“ eine Zone für diese Kamera und legen Sie diese anschließend unten als erforderliche Zone fest."
+ },
+ "ffmpeg": {
+ "hwaccelManualNotRecommended": "Explizite Definitionen der Hardware-beschleunigungs Variablen sind nicht empfohlen. Wähle die Voreinstellung die zu deiner Hardware passt, außer wenn spezifische Anforderungen eine andere Konfiguration erfordern."
+ },
+ "model": {
+ "optimizedFor320": "Frigate ist für ein 320x320-Modell optimiert, was für die meisten Konfigurationen die beste Wahl ist. Ein 640x640-Modell ist langsamer und bietet nur in bestimmten Szenarien Vorteile.",
+ "inputDimensionsNotDetectResolution": "Die Eingangsbreite und -höhe des Modells beziehen sich auf die Abmessungen des Objekterkennungsmodells und nicht auf die Erkennungsauflösung Ihrer Kamera. Sie sollten mit den Abmessungen des von Ihnen verwendeten Modells übereinstimmen – in der Regel quadratische Abmessungen wie 320×320 oder 640×640."
}
+ },
+ "birdseye": {
+ "trackingMode": {
+ "objects": "Objekte",
+ "motion": "Bewegung",
+ "continuous": "Fortlaufend"
+ },
+ "cameraOrder": {
+ "label": "Kamerabestellung",
+ "description": "Ziehe die Kameras per Drag & Drop, um ihre Reihenfolge im Birdseye-Layout festzulegen.",
+ "reorderHandle": "Zum Neuanordnen ziehen",
+ "saving": "Wird gespeichert…",
+ "saved": "gespeichert"
+ }
+ },
+ "retainMode": {
+ "all": "Alle",
+ "motion": "Bewegung",
+ "active_objects": "Aktive Objekte"
+ },
+ "previewQuality": {
+ "very_high": "sehr hoch",
+ "high": "hoch",
+ "medium": "Mittel",
+ "low": "niedrig",
+ "very_low": "sehr niedrig"
+ },
+ "ui": {
+ "timeFormat": {
+ "browser": "Browser",
+ "12hour": "12 Stunden",
+ "24hour": "24 Stunden"
+ },
+ "TimeOrDateStyle": {
+ "full": "vollständig",
+ "long": "lang",
+ "medium": "mittel",
+ "short": "kurz"
+ },
+ "unitSystem": {
+ "metric": "Metrik",
+ "imperial": "Imperial"
+ }
+ },
+ "review": {
+ "imageSource": {
+ "recordings": "Aufnahmen",
+ "previews": "Vorschau"
+ }
+ },
+ "logger": {
+ "logLevel": {
+ "debug": "Debug",
+ "info": "Info",
+ "warning": "Warnung",
+ "error": "Fehler",
+ "critical": "Kritisch"
+ }
+ },
+ "modelSize": {
+ "small": "klein",
+ "large": "groß"
+ },
+ "menuDot": {
+ "overrideGlobal": "Dieser Abschnitt überschreibt die globale Konfiguration",
+ "overrideProfile": "Dieser Abschnitt wird durch das Profil {{profile}} überschrieben",
+ "unsaved": "Dieser Abschnitt enthält ungespeicherte Änderungen"
+ },
+ "detectorsAndModel": {
+ "title": "Detektoren und Modell",
+ "description": "Konfigurieren Sie das Detektor-Backend, das die Objekterkennung ausführt, sowie das dafür verwendete Modell. Änderungen werden gemeinsam gespeichert, sodass Detektor und Modell synchron bleiben.",
+ "cardTitles": {
+ "detector": "Detektor-Hardware",
+ "model": "Erkennungsmodell"
+ },
+ "tabs": {
+ "plus": "Frigate+",
+ "custom": "Benutzerdefiniertes Modell"
+ },
+ "mismatch": {
+ "warning": "Das aktuelle Frigate+-Modell „{{model}}“ erfordert den {{required}}-Detektor. Wählen Sie unten ein kompatibles Modell aus oder wechseln Sie vor dem Speichern zu „Benutzerdefiniertes Modell“."
+ },
+ "plusModel": {
+ "requiresDetector": "Voraussetzung: {{detector}}",
+ "noModelSelected": "Wählen Sie ein Modell der Frigate+ aus"
+ },
+ "toast": {
+ "saveSuccess": "Detektoren und Modelleinstellungen wurden gespeichert. Starten Sie Frigate neu, um die Änderungen zu übernehmen.",
+ "saveError": "Das Speichern der Detektor- und Modelleinstellungen ist fehlgeschlagen"
+ },
+ "unsavedChanges": "Nicht gespeicherte Änderungen an Detektor und Modell",
+ "restartRequired": "Neustart erforderlich (Detektor oder Modell geändert)"
}
}
diff --git a/web/public/locales/de/views/system.json b/web/public/locales/de/views/system.json
index 3b41b03b7f..567c6ae68e 100644
--- a/web/public/locales/de/views/system.json
+++ b/web/public/locales/de/views/system.json
@@ -173,7 +173,22 @@
"tips": {
"title": "Kamera-Untersuchsungsinfo"
},
- "aspectRatio": "Seitenverhältnis"
+ "aspectRatio": "Seitenverhältnis",
+ "keyframes": {
+ "title": "Keyframe-Analyse",
+ "analyzing": "Keyframes werden analysiert... Noch {{seconds}} Sekunden",
+ "stillAnalyzing": "Keyframes werden noch analysiert...",
+ "recordStream": "Stream aufzeichnen:",
+ "keyframeCount": "Beobachtete Keyframes:",
+ "observedDuration": "Beobachtete Dauer:",
+ "gap": "Keyframe-Abstand (min. / durchschnittlich / max.):",
+ "segmentLength": "Länge des Aufzeichnungssegments:",
+ "ok": "Keyframes alle ~{{seconds}}s, gut geeignet für Aufzeichnung und Wiedergabe.",
+ "warning": "Seltene oder unregelmäßige Keyframes (längste Lücke ~{{seconds}}s), wahrscheinlich ein „Smart“-Codec (H.264+/H.265+); dies wird nicht empfohlen.",
+ "error": "Die Lücke zwischen den Keyframes (~{{seconds}}s) überschreitet die Länge des Aufzeichnungssegments ({{segmentTime}}s). Einige Segmente enthalten möglicherweise keinen Keyframe, was zu einer Unterbrechung der Wiedergabe führt. Deaktivieren Sie den Smart/+-Codec an der Kamera oder verkürzen Sie dessen Keyframe-Intervall.",
+ "unknown": "Der Abstand zwischen den Keyframes konnte nicht ermittelt werden.",
+ "recordDisabled": "Die Aufzeichnung ist für diese Kamera deaktiviert."
+ }
},
"overview": "Übersicht",
"label": {
@@ -213,6 +228,9 @@
"expectedFps": "Erwartete FPS",
"reconnectsLastHour": "Wiederverbindungen (letzte Stunde)",
"stallsLastHour": "Stände (letzte Stunde)"
+ },
+ "noCameras": {
+ "title": "keine Kameras gefunden"
}
},
"enrichments": {
diff --git a/web/public/locales/el/audio.json b/web/public/locales/el/audio.json
index 2bd01b8717..f3344f1186 100644
--- a/web/public/locales/el/audio.json
+++ b/web/public/locales/el/audio.json
@@ -61,5 +61,14 @@
"yodeling": "Λαρυγγισμός",
"chant": "Ύμνος",
"mantra": "Μάντρα",
- "synthetic_singing": "Συνθετικό Τραγούδι"
+ "synthetic_singing": "Συνθετικό Τραγούδι",
+ "rapping": "Χτύπημα",
+ "humming": "Μουρμουρητό",
+ "groan": "Aναστεναγμός",
+ "grunt": "Μούγκρισμα",
+ "gasp": "Ανάσα",
+ "pant": "Λαχάνιασμα",
+ "snort": "Ρουθούνισμα",
+ "cough": "Βήχας",
+ "throat_clearing": "Ξεροβήξιμο"
}
diff --git a/web/public/locales/el/common.json b/web/public/locales/el/common.json
index 5978d6cff6..256fcfb8aa 100644
--- a/web/public/locales/el/common.json
+++ b/web/public/locales/el/common.json
@@ -109,7 +109,9 @@
"no": "Όχι",
"download": "Κατέβασμα",
"info": "Πληροφορίες",
- "history": "Ιστορία"
+ "history": "Ιστορία",
+ "on": "Ενεργοποίηση",
+ "off": "Απενεργοποίηση"
},
"unit": {
"speed": {
@@ -117,7 +119,8 @@
"kph": "χλμ/ώρα"
},
"length": {
- "meters": "μέτρα"
+ "meters": "μέτρα",
+ "feet": "πόδια"
},
"data": {
"kbps": "kB/s",
@@ -134,7 +137,8 @@
"show": "Εμφάνιση {{item}}",
"ID": "ID",
"none": "Κανένα",
- "all": "Όλα"
+ "all": "Όλα",
+ "other": "άλλο"
},
"toast": {
"save": {
diff --git a/web/public/locales/el/config/cameras.json b/web/public/locales/el/config/cameras.json
index 0967ef424b..b9d465ec92 100644
--- a/web/public/locales/el/config/cameras.json
+++ b/web/public/locales/el/config/cameras.json
@@ -1 +1,3 @@
-{}
+{
+ "label": "Ρύθμιση της κάμερας"
+}
diff --git a/web/public/locales/el/config/global.json b/web/public/locales/el/config/global.json
index 0967ef424b..5b60515cb1 100644
--- a/web/public/locales/el/config/global.json
+++ b/web/public/locales/el/config/global.json
@@ -1 +1,14 @@
-{}
+{
+ "version": {
+ "label": "Τρέχουσα έκδοση διαμόρφωσης"
+ },
+ "safe_mode": {
+ "label": "Ασφαλής λειτουργία"
+ },
+ "auth": {
+ "reset_admin_password": {
+ "label": "Επανέφερε κωδικού πρόσβασης για τον διαχειριστή admin",
+ "description": "Άμα είναι αλήθεια, επαναφέρει τον κωδικό πρόσβασης του χρήστη διαχειριστή(admin) κατά την εκκίνηση και εκτύπωση του νέου κωδικού πρόσβασης στα αρχείο καταγραφής(logs)"
+ }
+ }
+}
diff --git a/web/public/locales/el/config/groups.json b/web/public/locales/el/config/groups.json
index 0967ef424b..23a118f019 100644
--- a/web/public/locales/el/config/groups.json
+++ b/web/public/locales/el/config/groups.json
@@ -1 +1,7 @@
-{}
+{
+ "audio": {
+ "global": {
+ "detection": "Παγκόσμια Ανίχνευση"
+ }
+ }
+}
diff --git a/web/public/locales/el/config/validation.json b/web/public/locales/el/config/validation.json
index 0967ef424b..d99e52717b 100644
--- a/web/public/locales/el/config/validation.json
+++ b/web/public/locales/el/config/validation.json
@@ -1 +1,3 @@
-{}
+{
+ "minimum": "Πρέπει να είναι τουλάχιστον {{limit}}"
+}
diff --git a/web/public/locales/el/views/chat.json b/web/public/locales/el/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/el/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/el/views/motionSearch.json b/web/public/locales/el/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/el/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/el/views/replay.json b/web/public/locales/el/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/el/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/en/common.json b/web/public/locales/en/common.json
index de17f444b5..272e0f2795 100644
--- a/web/public/locales/en/common.json
+++ b/web/public/locales/en/common.json
@@ -177,6 +177,7 @@
"en": "English (English)",
"es": "Español (Spanish)",
"zhCN": "简体中文 (Simplified Chinese)",
+ "zhHant": "繁體中文 (Traditional Chinese)",
"hi": "हिन्दी (Hindi)",
"fr": "Français (French)",
"ar": "العربية (Arabic)",
@@ -207,6 +208,7 @@
"th": "ไทย (Thai)",
"ca": "Català (Catalan)",
"hr": "Hrvatski (Croatian)",
+ "bs": "Bosanski (Bosnian)",
"sr": "Српски (Serbian)",
"sl": "Slovenščina (Slovenian)",
"lt": "Lietuvių (Lithuanian)",
@@ -257,6 +259,7 @@
"export": "Export",
"actions": "Actions",
"uiPlayground": "UI Playground",
+ "features": "Features",
"faceLibrary": "Face Library",
"classification": "Classification",
"chat": "Chat",
@@ -314,5 +317,8 @@
"pixels": "{{area}}px"
},
"no_items": "No items",
- "validation_errors": "Validation Errors"
+ "validation_errors": "Validation Errors",
+ "credentialField": {
+ "savedPlaceholder": "Saved — leave blank to keep current"
+ }
}
diff --git a/web/public/locales/en/components/camera.json b/web/public/locales/en/components/camera.json
index ed37d1771f..3f6afc6420 100644
--- a/web/public/locales/en/components/camera.json
+++ b/web/public/locales/en/components/camera.json
@@ -2,7 +2,10 @@
"group": {
"label": "Camera Groups",
"add": "Add Camera Group",
+ "showAll": "Show all camera groups",
+ "showLess": "Show less",
"edit": "Edit Camera Group",
+ "editGroups": "Edit Camera Groups",
"delete": {
"label": "Delete Camera Group",
"confirm": {
diff --git a/web/public/locales/en/components/dialog.json b/web/public/locales/en/components/dialog.json
index f4e54aa885..dcb4a23db4 100644
--- a/web/public/locales/en/components/dialog.json
+++ b/web/public/locales/en/components/dialog.json
@@ -24,6 +24,9 @@
},
"state": {
"submitted": "Submitted"
+ },
+ "toast": {
+ "error": "Failed to submit to Frigate+. Please check your network connection and try again."
}
}
},
@@ -70,6 +73,13 @@
"selectFromTimeline": "Select from Timeline",
"cameraSelection": "Cameras",
"cameraSelectionHelp": "Cameras with tracked objects in this time range are pre-selected",
+ "searchOrSelectGroup": "Search, or select a camera group...",
+ "selectAll": "Select all cameras",
+ "clearSelection": "Clear selection",
+ "selectWithActivity": "Cameras with tracked objects",
+ "selectGroup": "Select group",
+ "noMatchingCameras": "No cameras match your search",
+ "selectedCount": "{{selected}} / {{total}} selected",
"checkingActivity": "Checking camera activity...",
"noCameras": "No cameras available",
"detectionCount_one": "1 tracked object",
@@ -113,7 +123,7 @@
"error": {
"failed": "Failed to queue export: {{error}}",
"endTimeMustAfterStartTime": "End time must be after start time",
- "noVaildTimeSelected": "No valid time range selected"
+ "noValidTimeSelected": "No valid time range selected"
}
},
"fromTimeline": {
@@ -152,6 +162,14 @@
}
},
"recording": {
+ "shareTimestamp": {
+ "label": "Share Timestamp",
+ "title": "Share Timestamp",
+ "description": "Share a timestamped URL of current player position or choose a custom timestamp. Note that this is not a public share URL and is only accessible to users with access to Frigate and this camera.",
+ "custom": "Custom Timestamp",
+ "button": "Share Timestamp URL",
+ "shareTitle": "Frigate Review Timestamp: {{camera}}"
+ },
"confirmDelete": {
"title": "Confirm Delete",
"desc": {
diff --git a/web/public/locales/en/components/player.json b/web/public/locales/en/components/player.json
index 3b50ff5ed5..011baaa1bf 100644
--- a/web/public/locales/en/components/player.json
+++ b/web/public/locales/en/components/player.json
@@ -4,14 +4,15 @@
"noPreviewFoundFor": "No Preview Found for {{cameraName}}",
"submitFrigatePlus": {
"title": "Submit this frame to Frigate+?",
- "submit": "Submit"
+ "submit": "Submit",
+ "previewError": "Could not load snapshot preview. The recording may not be available at this time."
},
"livePlayerRequiredIOSVersion": "iOS 17.1 or greater is required for this live stream type.",
"streamOffline": {
"title": "Stream Offline",
"desc": "No frames have been received on the {{cameraName}} detect stream, check error logs"
},
- "cameraDisabled": "Camera is disabled",
+ "cameraOff": "Camera is off",
"stats": {
"streamType": {
"title": "Stream Type:",
diff --git a/web/public/locales/en/config/cameras.json b/web/public/locales/en/config/cameras.json
index 1b524c347d..2ddc30b6b8 100644
--- a/web/public/locales/en/config/cameras.json
+++ b/web/public/locales/en/config/cameras.json
@@ -13,7 +13,7 @@
"description": "Enabled"
},
"audio": {
- "label": "Audio events",
+ "label": "Audio detection",
"description": "Settings for audio-based event detection for this camera.",
"enabled": {
"label": "Enable audio detection",
@@ -29,11 +29,15 @@
},
"listen": {
"label": "Listen types",
- "description": "List of audio event types to detect (for example: bark, fire_alarm, scream, speech, yell)."
+ "description": "List of audio event types to detect (for example: bark, fire_alarm, speech, yell)."
},
"filters": {
"label": "Audio filters",
- "description": "Per-audio-type filter settings such as confidence thresholds used to reduce false positives."
+ "description": "Per-audio-type filter settings such as confidence thresholds used to reduce false positives.",
+ "threshold": {
+ "label": "Minimum audio confidence",
+ "description": "Minimum confidence threshold for the audio event to be counted."
+ }
},
"enabled_in_config": {
"label": "Original audio state",
@@ -148,11 +152,11 @@
}
},
"ffmpeg": {
- "label": "FFmpeg",
- "description": "FFmpeg settings including binary path, args, hwaccel options, and per-role output args.",
+ "label": "Streams (FFmpeg)",
+ "description": "Camera stream inputs and FFmpeg options, including binary path, args, hwaccel, and per-role output args.",
"path": {
"label": "FFmpeg path",
- "description": "Path to the FFmpeg binary to use or a version alias (\"5.0\" or \"7.0\")."
+ "description": "Path to the FFmpeg binary to use or a version alias (\"7.0\" or \"8.0\")."
},
"global_args": {
"label": "FFmpeg global arguments",
@@ -485,6 +489,13 @@
"hwaccel_args": {
"label": "Export hwaccel args",
"description": "Hardware acceleration args to use for export/transcode operations."
+ },
+ "max_concurrent": {
+ "label": "Maximum concurrent exports",
+ "description": "Maximum number of export jobs to process at the same time."
+ },
+ "chapters": {
+ "label": "Chapter metadata to embed in exported recordings"
}
},
"preview": {
@@ -658,10 +669,6 @@
"label": "Default retention",
"description": "Default number of days to retain snapshots."
},
- "mode": {
- "label": "Retention mode",
- "description": "Mode for retention: all (save all segments), motion (save segments with motion), or active_objects (save segments with active objects)."
- },
"objects": {
"label": "Object retention",
"description": "Per-object overrides for snapshot retention days."
@@ -674,7 +681,7 @@
},
"timestamp_style": {
"label": "Timestamp style",
- "description": "Styling options for in-feed timestamps applied to recordings and snapshots.",
+ "description": "Styling options for timestamps applied to snapshots and Debug view.",
"position": {
"label": "Timestamp position",
"description": "Position of the timestamp on the image (tl/tr/bl/br)."
@@ -854,6 +861,10 @@
"dashboard": {
"label": "Show in UI",
"description": "Toggle whether this camera is visible everywhere in the Frigate UI. Disabling this will require manually editing the config to view this camera in the UI again."
+ },
+ "review": {
+ "label": "Show in review",
+ "description": "Toggle whether this camera is visible in review (the review page and its camera filter, motion review, and the history view)."
}
},
"webui_url": {
diff --git a/web/public/locales/en/config/global.json b/web/public/locales/en/config/global.json
index 69c77fad11..e3b7c65c68 100644
--- a/web/public/locales/en/config/global.json
+++ b/web/public/locales/en/config/global.json
@@ -212,7 +212,7 @@
},
"default_role": {
"label": "Default role",
- "description": "Default role assigned to proxy-authenticated users when no role mapping applies (admin or viewer)."
+ "description": "Default role assigned to proxy-authenticated users when no role mapping applies."
},
"separator": {
"label": "Separator character",
@@ -242,8 +242,8 @@
"description": "Enable per-process network bandwidth monitoring for camera ffmpeg processes and detectors (requires capabilities)."
},
"intel_gpu_device": {
- "label": "SR-IOV device",
- "description": "Device identifier used when treating Intel GPUs as SR-IOV to fix GPU stats."
+ "label": "Intel GPU device",
+ "description": "PCI bus address or DRM device path (e.g. /dev/dri/card1) used to pin Intel GPU stats to a specific device when multiple are present."
}
},
"version_check": {
@@ -270,14 +270,6 @@
"label": "Time format",
"description": "Time format to use in the UI (browser, 12hour, or 24hour)."
},
- "date_style": {
- "label": "Date style",
- "description": "Date style to use in the UI (full, long, medium, short)."
- },
- "time_style": {
- "label": "Time style",
- "description": "Time style to use in the UI (full, long, medium, short)."
- },
"unit_system": {
"label": "Unit system",
"description": "Unit system for display (metric or imperial) used in the UI and MQTT."
@@ -539,7 +531,7 @@
}
},
"audio": {
- "label": "Audio events",
+ "label": "Audio detection",
"description": "Settings for audio-based event detection for all cameras; can be overridden per-camera.",
"enabled": {
"label": "Enable audio detection",
@@ -555,11 +547,15 @@
},
"listen": {
"label": "Listen types",
- "description": "List of audio event types to detect (for example: bark, fire_alarm, scream, speech, yell)."
+ "description": "List of audio event types to detect (for example: bark, fire_alarm, speech, yell)."
},
"filters": {
"label": "Audio filters",
- "description": "Per-audio-type filter settings such as confidence thresholds used to reduce false positives."
+ "description": "Per-audio-type filter settings such as confidence thresholds used to reduce false positives.",
+ "threshold": {
+ "label": "Minimum audio confidence",
+ "description": "Minimum confidence threshold for the audio event to be counted."
+ }
},
"enabled_in_config": {
"label": "Original audio state",
@@ -687,7 +683,7 @@
"description": "FFmpeg settings including binary path, args, hwaccel options, and per-role output args.",
"path": {
"label": "FFmpeg path",
- "description": "Path to the FFmpeg binary to use or a version alias (\"5.0\" or \"7.0\")."
+ "description": "Path to the FFmpeg binary to use or a version alias (\"7.0\" or \"8.0\")."
},
"global_args": {
"label": "FFmpeg global arguments",
@@ -917,6 +913,41 @@
"label": "Original GenAI state",
"description": "Indicates whether GenAI was enabled in the original static config."
}
+ },
+ "filters_attribute": {
+ "label": "Attribute filters",
+ "description": "Filters applied to detected attributes to reduce false positives (area, ratio, confidence).",
+ "min_area": {
+ "label": "Minimum attribute area",
+ "description": "Minimum bounding box area (pixels or percentage) required for this attribute. Can be pixels (int) or percentage (float between 0.000001 and 0.99)."
+ },
+ "max_area": {
+ "label": "Maximum attribute area",
+ "description": "Maximum bounding box area (pixels or percentage) allowed for this attribute. Can be pixels (int) or percentage (float between 0.000001 and 0.99)."
+ },
+ "min_ratio": {
+ "label": "Minimum aspect ratio",
+ "description": "Minimum width/height ratio required for the bounding box to qualify."
+ },
+ "max_ratio": {
+ "label": "Maximum aspect ratio",
+ "description": "Maximum width/height ratio allowed for the bounding box to qualify."
+ },
+ "threshold": {
+ "label": "Confidence threshold",
+ "description": "Average detection confidence threshold required for the attribute to be considered a true positive."
+ },
+ "min_score": {
+ "label": "Minimum confidence",
+ "description": "Minimum single-frame detection confidence required to associate this attribute with its parent object."
+ },
+ "mask": {
+ "label": "Filter mask",
+ "description": "Polygon coordinates defining where this filter applies within the frame."
+ },
+ "raw_mask": {
+ "label": "Raw Mask"
+ }
}
},
"record": {
@@ -1000,6 +1031,13 @@
"hwaccel_args": {
"label": "Export hwaccel args",
"description": "Hardware acceleration args to use for export/transcode operations."
+ },
+ "max_concurrent": {
+ "label": "Maximum concurrent exports",
+ "description": "Maximum number of export jobs to process at the same time."
+ },
+ "chapters": {
+ "label": "Chapter metadata to embed in exported recordings"
}
},
"preview": {
@@ -1141,10 +1179,6 @@
"label": "Default retention",
"description": "Default number of days to retain snapshots."
},
- "mode": {
- "label": "Retention mode",
- "description": "Mode for retention: all (save all segments), motion (save segments with motion), or active_objects (save segments with active objects)."
- },
"objects": {
"label": "Object retention",
"description": "Per-object overrides for snapshot retention days."
@@ -1511,6 +1545,10 @@
"dashboard": {
"label": "Show in UI",
"description": "Toggle whether this camera is visible everywhere in the Frigate UI. Disabling this will require manually editing the config to view this camera in the UI again."
+ },
+ "review": {
+ "label": "Show in review",
+ "description": "Toggle whether this camera is visible in review (the review page and its camera filter, motion review, and the history view)."
}
},
"onvif": {
diff --git a/web/public/locales/en/config/validation.json b/web/public/locales/en/config/validation.json
index 6f3b5f6864..f3d98a65e9 100644
--- a/web/public/locales/en/config/validation.json
+++ b/web/public/locales/en/config/validation.json
@@ -28,5 +28,8 @@
"detectRequired": "At least one input stream must be assigned the 'detect' role.",
"hwaccelDetectOnly": "Only the input stream with the detect role can define hardware acceleration arguments."
}
+ },
+ "detect": {
+ "dimensionMustBeEven": "Must be an even number."
}
}
diff --git a/web/public/locales/en/objects.json b/web/public/locales/en/objects.json
index 1315104be4..d48f609a13 100644
--- a/web/public/locales/en/objects.json
+++ b/web/public/locales/en/objects.json
@@ -121,5 +121,9 @@
"royal_mail": "Royal Mail",
"school_bus": "School Bus",
"skunk": "Skunk",
- "kangaroo": "Kangaroo"
+ "kangaroo": "Kangaroo",
+ "baby": "Baby",
+ "baby_stroller": "Baby Stroller",
+ "rickshaw": "Rickshaw",
+ "rodent": "Rodent"
}
diff --git a/web/public/locales/en/views/chat.json b/web/public/locales/en/views/chat.json
index 6d78dc71f8..363b0e68e4 100644
--- a/web/public/locales/en/views/chat.json
+++ b/web/public/locales/en/views/chat.json
@@ -42,5 +42,31 @@
"show_camera_status": "What is the current status of my cameras?",
"recap": "What happened while I was away?",
"watch_camera": "Watch the front door and let me know if anyone shows up"
+ },
+ "new_chat": "New chat",
+ "settings": {
+ "title": "Chat settings",
+ "show_stats": {
+ "title": "Show stats",
+ "desc": "Show generation rate and context size for chat responses.",
+ "while_generating": "While generating",
+ "always": "Always"
+ },
+ "auto_scroll": {
+ "title": "Auto-scroll",
+ "desc": "Follow new messages as they arrive."
+ }
+ },
+ "stats": {
+ "context": "{{tokens}} tokens",
+ "tokens_per_second": "{{rate}} t/s"
+ },
+ "reasoning": {
+ "active": "Reasoning…",
+ "show": "Show reasoning",
+ "hide": "Hide reasoning"
+ },
+ "thinking": {
+ "toggle": "Toggle thinking"
}
}
diff --git a/web/public/locales/en/views/classificationModel.json b/web/public/locales/en/views/classificationModel.json
index 3206ad0339..3e881ae345 100644
--- a/web/public/locales/en/views/classificationModel.json
+++ b/web/public/locales/en/views/classificationModel.json
@@ -1,5 +1,6 @@
{
"documentTitle": "Classification Models - Frigate",
+ "disabled": "Disabled",
"details": {
"scoreInfo": "Score represents the average classification confidence across all detections of this object.",
"none": "None",
@@ -64,7 +65,20 @@
"title": "Edit Classification Model",
"descriptionState": "Edit the classes for this state classification model. Changes will require retraining the model.",
"descriptionObject": "Edit the object type and classification type for this object classification model.",
- "stateClassesInfo": "Note: Changing state classes requires retraining the model with the updated classes."
+ "enabled": "Enabled",
+ "enabledDesc": "Run this model. When disabled, it stops running and no longer classifies.",
+ "saveAttempts": "Save Attempts",
+ "saveAttemptsDesc": "Number of classification attempt images to keep for the recent classifications UI.",
+ "motion": "Run on Motion",
+ "motionDesc": "Run classification when motion is detected within the configured crop.",
+ "interval": "Interval",
+ "intervalDesc": "Seconds between periodic classification runs. Leave empty to run only on motion.",
+ "intervalPlaceholder": "No interval",
+ "stateClassesInfo": "Model updated. Retrain the model for the class changes to take effect.",
+ "errors": {
+ "saveAttemptsInvalid": "Save attempts must be a whole number of 0 or greater",
+ "intervalInvalid": "Interval must be a whole number greater than 0"
+ }
},
"deleteDatasetImages": {
"title": "Delete Dataset Images",
diff --git a/web/public/locales/en/views/events.json b/web/public/locales/en/views/events.json
index a829d3687a..f25e3022c5 100644
--- a/web/public/locales/en/views/events.json
+++ b/web/public/locales/en/views/events.json
@@ -45,7 +45,9 @@
},
"documentTitle": "Review - Frigate",
"recordings": {
- "documentTitle": "Recordings - Frigate"
+ "documentTitle": "Recordings - Frigate",
+ "invalidSharedLink": "Unable to open timestamped recording link due to parsing error.",
+ "invalidSharedCamera": "Unable to open timestamped recording link due to an unknown or unauthorized camera."
},
"calendarFilter": {
"last24Hours": "Last 24 Hours"
@@ -65,7 +67,7 @@
"needsReview": "Needs review",
"securityConcern": "Security concern",
"motionSearch": {
- "menuItem": "Motion search",
+ "menuItem": "Motion Search",
"openMenu": "Camera options"
},
"motionPreviews": {
diff --git a/web/public/locales/en/views/explore.json b/web/public/locales/en/views/explore.json
index 43db9bda48..c85ee90d53 100644
--- a/web/public/locales/en/views/explore.json
+++ b/web/public/locales/en/views/explore.json
@@ -76,7 +76,7 @@
},
"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. You can use this setting to offset the annotations forward or backward in time to better align them with the recorded footage.",
+ "desc": "This data comes from your camera's detect feed but is overlaid on images from the record feed. It is unlikely that the two streams are perfectly in sync. As a result, the bounding box and the footage will not line up perfectly. You can use this setting to offset the annotations forward or backward in time to better align them with the recorded footage.",
"millisecondsToOffset": "Milliseconds to offset detect annotations by. Default: 0 ",
"tips": "Lower the value if the video playback is ahead of the boxes and path points, and increase the value if the video playback is behind them. This value can be negative.",
"toast": {
@@ -222,7 +222,7 @@
"label": "Hide object path"
},
"debugReplay": {
- "label": "Debug replay",
+ "label": "Debug Replay",
"aria": "View this tracked object in the debug replay view"
},
"more": {
diff --git a/web/public/locales/en/views/faceLibrary.json b/web/public/locales/en/views/faceLibrary.json
index 4f8a9cf2da..27e5454601 100644
--- a/web/public/locales/en/views/faceLibrary.json
+++ b/web/public/locales/en/views/faceLibrary.json
@@ -32,7 +32,11 @@
"title": "Recent Recognitions",
"titleShort": "Recent",
"aria": "Select recent recognitions",
- "empty": "There are no recent face recognition attempts"
+ "empty": "There are no recent face recognition attempts",
+ "emptyNoLibrary": {
+ "title": "Upload a face",
+ "description": "You must add at least one face to the library for face recognition to function."
+ }
},
"deleteFaceLibrary": {
"title": "Delete Name",
diff --git a/web/public/locales/en/views/live.json b/web/public/locales/en/views/live.json
index 37e6b15dbb..f2ea2721f1 100644
--- a/web/public/locales/en/views/live.json
+++ b/web/public/locales/en/views/live.json
@@ -57,8 +57,8 @@
"presets": "PTZ camera presets"
},
"camera": {
- "enable": "Enable Camera",
- "disable": "Disable Camera"
+ "turnOn": "Turn Camera On",
+ "turnOff": "Turn Camera Off"
},
"muteCameras": {
"enable": "Mute All Cameras",
@@ -70,7 +70,8 @@
},
"recording": {
"enable": "Enable Recording",
- "disable": "Disable Recording"
+ "disable": "Disable Recording",
+ "disabledInConfig": "Recording must first be enabled in Settings for this camera."
},
"snapshots": {
"enable": "Enable Snapshots",
@@ -152,7 +153,7 @@
},
"cameraSettings": {
"title": "{{camera}} Settings",
- "cameraEnabled": "Camera Enabled",
+ "camera": "Camera",
"objectDetection": "Object Detection",
"recording": "Recording",
"snapshots": "Snapshots",
diff --git a/web/public/locales/en/views/motionSearch.json b/web/public/locales/en/views/motionSearch.json
index 6e22c32035..8358bfb75d 100644
--- a/web/public/locales/en/views/motionSearch.json
+++ b/web/public/locales/en/views/motionSearch.json
@@ -8,6 +8,7 @@
"searchCancelled": "Search cancelled",
"cancelSearch": "Cancel",
"searching": "Search in progress.",
+ "scanning": "Scanning {{time}}",
"searchComplete": "Search complete",
"noResultsYet": "Run a search to find motion changes in the selected region",
"noChangesFound": "No pixel changes detected in the selected region",
@@ -24,7 +25,9 @@
"points_one": "{{count}} point",
"points_other": "{{count}} points",
"undo": "Undo last point",
- "reset": "Reset polygon"
+ "reset": "Reset polygon",
+ "drawMode": "Draw",
+ "moveMode": "Move"
},
"motionHeatmapLabel": "Motion Heatmap",
"dialog": {
@@ -40,13 +43,11 @@
"settings": {
"title": "Search Settings",
"parallelMode": "Parallel mode",
- "parallelModeDesc": "Scan multiple recording segments at the same time (faster, but significantly more CPU intensive)",
+ "parallelModeDesc": "Scan multiple recording ranges at the same time (faster; uses more decoding resources)",
"threshold": "Sensitivity Threshold",
"thresholdDesc": "Lower values detect smaller changes (1-255)",
"minArea": "Minimum Change Area",
- "minAreaDesc": "Minimum percentage of the region of interest that must change to be considered significant",
- "frameSkip": "Frame Skip",
- "frameSkipDesc": "Process every Nth frame. Set this to your camera's frame rate to process one frame per second (e.g. 5 for a 5 FPS camera, 30 for a 30 FPS camera). Higher values will be faster, but may miss short motion events.",
+ "minAreaDesc": "Minimum size of a single moving region, as a percentage of the region of interest",
"maxResults": "Maximum Results",
"maxResultsDesc": "Stop after this many matching timestamps"
},
@@ -70,6 +71,8 @@
"framesDecoded": "Frames decoded",
"wallTime": "Search time",
"segmentErrors": "Segment errors",
- "seconds": "{{seconds}}s"
+ "seconds": "{{seconds}}s",
+ "minutesSeconds": "{{minutes}}m {{seconds}}s",
+ "scanSummary": "{{segments}} segments · {{time}}"
}
}
diff --git a/web/public/locales/en/views/replay.json b/web/public/locales/en/views/replay.json
index a966626f5f..e8f50d7b7e 100644
--- a/web/public/locales/en/views/replay.json
+++ b/web/public/locales/en/views/replay.json
@@ -19,26 +19,31 @@
"startLabel": "Start",
"endLabel": "End",
"toast": {
- "success": "Debug replay started successfully",
"error": "Failed to start debug replay: {{error}}",
"alreadyActive": "A replay session is already active",
- "stopped": "Debug replay stopped",
"stopError": "Failed to stop debug replay: {{error}}",
"goToReplay": "Go to Replay"
}
},
"page": {
- "noSession": "No Active Replay Session",
- "noSessionDesc": "Start a debug replay from the History view by clicking the Debug Replay button in the toolbar.",
+ "noSession": "No Active Debug Replay Session",
+ "noSessionDesc": "Start a Debug Replay from History view by clicking the Actions button in the toolbar and choosing Debug Replay.",
"goToRecordings": "Go to History",
+ "preparingClip": "Preparing clip…",
+ "preparingClipDesc": "Frigate is stitching together recordings for the selected time range. This can take a minute for longer ranges.",
+ "startingCamera": "Starting Debug Replay…",
+ "startError": {
+ "title": "Failed to start Debug Replay",
+ "back": "Back to History"
+ },
"sourceCamera": "Source Camera",
"replayCamera": "Replay Camera",
- "initializingReplay": "Initializing replay...",
- "stoppingReplay": "Stopping replay...",
+ "initializingReplay": "Initializing Debug Replay...",
+ "stoppingReplay": "Stopping Debug Replay...",
"stopReplay": "Stop Replay",
"confirmStop": {
"title": "Stop Debug Replay?",
- "description": "This will stop the replay session and clean up all temporary data. Are you sure?",
+ "description": "This will stop the session and clean up all temporary data. Are you sure?",
"confirm": "Stop Replay",
"cancel": "Cancel"
},
@@ -49,6 +54,6 @@
"activeTracking": "Active tracking",
"noActiveTracking": "No active tracking",
"configuration": "Configuration",
- "configurationDesc": "Fine tune motion detection and object tracking settings for the debug replay camera. No changes are saved to your Frigate configuration file."
+ "configurationDesc": "Fine tune motion detection and object tracking settings for the Debug Replay camera. No changes are saved to your Frigate configuration file."
}
}
diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json
index a1e14452e5..68248241ce 100644
--- a/web/public/locales/en/views/settings.json
+++ b/web/public/locales/en/views/settings.json
@@ -12,6 +12,7 @@
"globalConfig": "Global Configuration - Frigate",
"cameraConfig": "Camera Configuration - Frigate",
"frigatePlus": "Frigate+ Settings - Frigate",
+ "detectorsAndModel": "Detectors and model - Frigate",
"notifications": "Notification Settings - Frigate",
"maintenance": "Maintenance - Frigate",
"profiles": "Profiles - Frigate"
@@ -19,8 +20,30 @@
"button": {
"overriddenGlobal": "Overridden (Global)",
"overriddenGlobalTooltip": "This camera overrides global configuration settings in this section",
+ "overriddenGlobalHeading_one": "This camera overrides {{count}} field from the global config:",
+ "overriddenGlobalHeading_other": "This camera overrides {{count}} fields from the global config:",
+ "overriddenGlobalNoDeltas": "This camera overrides the global config, but no field values differ.",
"overriddenBaseConfig": "Overridden (Base Config)",
- "overriddenBaseConfigTooltip": "The {{profile}} profile overrides configuration settings in this section"
+ "overriddenBaseConfigTooltip": "The {{profile}} profile overrides configuration settings in this section",
+ "overriddenBaseConfigHeading_one": "The {{profile}} profile overrides {{count}} field from the base config:",
+ "overriddenBaseConfigHeading_other": "The {{profile}} profile overrides {{count}} fields from the base config:",
+ "overriddenBaseConfigNoDeltas": "The {{profile}} profile overrides this section, but no field values differ from the base config.",
+ "overriddenInCameras": {
+ "label_one": "Overridden in {{count}} camera",
+ "label_other": "Overridden in {{count}} cameras",
+ "tooltip_one": "{{count}} camera overrides values in this section. Click to see details.",
+ "tooltip_other": "{{count}} cameras override values in this section. Click to see details.",
+ "heading_one": "This global section has fields that are overridden in {{count}} camera.",
+ "heading_other": "This global section has fields that are overridden in {{count}} cameras.",
+ "othersField_one": "{{count}} other",
+ "othersField_other": "{{count}} others",
+ "profilePrefix": "{{profile}} profile: {{fields}}"
+ }
+ },
+ "menuDot": {
+ "overrideGlobal": "This section overrides the global configuration",
+ "overrideProfile": "This section is overridden by the {{profile}} profile",
+ "unsaved": "This section has unsaved changes"
},
"menu": {
"general": "General",
@@ -38,7 +61,7 @@
"globalMotion": "Motion detection",
"globalObjects": "Objects",
"globalReview": "Review",
- "globalAudioEvents": "Audio events",
+ "globalAudioEvents": "Audio detection",
"globalLivePlayback": "Live playback",
"globalTimestampStyle": "Timestamp style",
"systemDatabase": "Database",
@@ -52,8 +75,7 @@
"systemTelemetry": "Telemetry",
"systemBirdseye": "Birdseye",
"systemFfmpeg": "FFmpeg",
- "systemDetectorHardware": "Detector hardware",
- "systemDetectionModel": "Detection model",
+ "systemDetectorsAndModel": "Detectors and model",
"systemMqtt": "MQTT",
"systemGo2rtcStreams": "go2rtc streams",
"integrationSemanticSearch": "Semantic search",
@@ -63,13 +85,13 @@
"integrationObjectClassification": "Object classification",
"integrationAudioTranscription": "Audio transcription",
"cameraDetect": "Object detection",
- "cameraFfmpeg": "FFmpeg",
+ "cameraFfmpeg": "Streams (FFmpeg)",
"cameraRecording": "Recording",
"cameraSnapshots": "Snapshots",
"cameraMotion": "Motion detection",
"cameraObjects": "Objects",
"cameraConfigReview": "Review",
- "cameraAudioEvents": "Audio events",
+ "cameraAudioEvents": "Audio detection",
"cameraAudioTranscription": "Audio transcription",
"cameraNotifications": "Notifications",
"cameraLivePlayback": "Live playback",
@@ -81,7 +103,7 @@
"cameraUi": "Camera UI",
"cameraTimestampStyle": "Timestamp style",
"cameraMqtt": "Camera MQTT",
- "cameraManagement": "Management",
+ "cameraManagement": "Camera management",
"cameraReview": "Review",
"masksAndZones": "Masks / Zones",
"motionTuner": "Motion tuner",
@@ -298,7 +320,7 @@
"nameLength": "Camera name must be 64 characters or less",
"invalidCharacters": "Camera name contains invalid characters",
"nameExists": "Camera name already exists",
- "customUrlRtspRequired": "Custom URLs must begin with \"rtsp://\". Manual configuration is required for non-RTSP camera streams."
+ "customUrlRtspRequired": "Custom URLs must begin with \"rtsp://\" or \"rtsps://\". Manual configuration is required for non-RTSP camera streams."
}
},
"step2": {
@@ -415,6 +437,7 @@
"audioCodecGood": "Audio codec is {{codec}}.",
"resolutionHigh": "A resolution of {{resolution}} may cause increased resource usage.",
"resolutionLow": "A resolution of {{resolution}} may be too low for reliable detection of small objects.",
+ "resolutionUnknown": "The resolution of this stream could not be probed. You should manually set the detect resolution in Settings or your config.",
"noAudioWarning": "No audio detected for this stream, recordings will not have audio.",
"audioCodecRecordError": "The AAC audio codec is required to support audio in recordings.",
"audioCodecRequired": "An audio stream is required to support audio detection.",
@@ -434,6 +457,7 @@
},
"cameraManagement": {
"title": "Manage Cameras",
+ "description": "Add, edit, and delete cameras, control the state of each camera, and configure per-profile and camera type overrides. To configure streams, detection, motion, and other camera-specific settings, choose the specific section under Camera Configuration.",
"addCamera": "Add New Camera",
"deleteCamera": "Delete Camera",
"deleteCameraDialog": {
@@ -451,12 +475,34 @@
"selectCamera": "Select a Camera",
"backToSettings": "Back to Camera Settings",
"streams": {
- "title": "Enable / Disable Cameras",
- "enableLabel": "Enabled cameras",
- "enableDesc": "Temporarily disable an enabled camera until Frigate restarts. Disabling a camera completely stops Frigate's processing of this camera's streams. Detection, recording, and debugging will be unavailable. Note: This does not disable go2rtc restreams. ",
- "disableLabel": "Disabled cameras",
- "disableDesc": "Enable a camera that is currently not visible in the UI and disabled in the configuration. A restart of Frigate is required after enabling.",
- "enableSuccess": "Enabled {{cameraName}} in configuration. Restart Frigate to apply the changes."
+ "title": "Camera State and Details",
+ "label": "Camera state",
+ "description": "Set the operating state for each camera.On : streams are processed normally.Off : temporarily pauses processing. Does not persist across Frigate restarts.Disabled : stops processing and saves the change to your configuration. A restart is required to re-enable a disabled camera.Note: Disabling does not affect go2rtc restreams. Drag the handle to reorder active cameras as they appear throughout the UI, including the Live dashboard and camera selection dropdowns.",
+ "disabledSubheading": "Disabled in configuration",
+ "status": {
+ "on": "On",
+ "off": "Off",
+ "disabled": "Disabled"
+ },
+ "enableSuccess": "Enabled {{cameraName}}. Restart Frigate to apply.",
+ "disableSuccess": "Disabled {{cameraName}} and saved to configuration.",
+ "reorderHandle": "Drag to reorder",
+ "saving": "Saving…",
+ "saved": "Saved",
+ "details": {
+ "edit": "Edit camera details",
+ "title": "Edit Camera Details",
+ "description": "Update the display name, external URL, and visibility used for this camera throughout the Frigate UI.",
+ "friendlyNameLabel": "Display Name",
+ "friendlyNameHelp": "Friendly name shown for this camera throughout the Frigate UI. Leave blank to use the camera ID.",
+ "webuiUrlLabel": "Camera Web UI URL",
+ "webuiUrlHelp": "URL to visit the camera's web UI directly from the Debug view. Leave blank to disable the link.",
+ "webuiUrlInvalid": "Must be a valid URL (e.g., https://example.com).",
+ "dashboardLabel": "Show on Live dashboard",
+ "dashboardHelp": "Show this camera on the Live dashboard.",
+ "reviewLabel": "Show in Review",
+ "reviewHelp": "Show this camera in Review, including the camera filter, motion review, and the history view."
+ }
},
"cameraConfig": {
"add": "Add Camera",
@@ -490,10 +536,104 @@
"profiles": {
"title": "Profile Camera Overrides",
"selectLabel": "Select profile",
- "description": "Configure which cameras are enabled or disabled when a profile is activated. Cameras set to \"Inherit\" keep their base enabled state.",
+ "description": "Configure which cameras are turned on or off when a profile is activated. Cameras set to \"Inherit\" keep their default state.",
"inherit": "Inherit",
- "enabled": "Enabled",
- "disabled": "Disabled"
+ "on": "On",
+ "off": "Off"
+ },
+ "cameraType": {
+ "title": "Camera Type",
+ "label": "Camera type",
+ "description": "Set the type for each camera. Dedicated LPR cameras are single-purpose cameras with powerful optical zoom to capture license plates on distant vehicles. Most cameras should use the normal camera type unless the camera is specifically for LPR and has a tightly focused view on license plates.",
+ "normal": "Normal",
+ "dedicatedLpr": "Dedicated LPR",
+ "saveSuccess": "Updated camera type for {{cameraName}}. Restart Frigate to apply the changes."
+ },
+ "clone": {
+ "sectionTitle": "Clone settings",
+ "sectionDescription": "Copy configuration from one camera to another camera or a new one.",
+ "button": "Clone settings",
+ "title": "Clone camera settings",
+ "description": "Copy a camera's configuration to one or more other cameras or a new camera. Identity (name, friendly name, web UI URL, display order) is never copied.",
+ "source": {
+ "label": "Source camera",
+ "placeholder": "Select a source camera",
+ "required": "Select a source camera"
+ },
+ "target": {
+ "legend": "Target",
+ "newRadio": "New camera",
+ "newNameLabel": "Camera name",
+ "newNamePlaceholder": "e.g., back_door or Back Door",
+ "newNameRequired": "Camera name is required",
+ "newNameInvalid": "Invalid camera name",
+ "newNameCollision": "A camera with this name already exists",
+ "newStreamsForced": "Streams are always copied for a new camera.",
+ "existingCamerasRadio": "Existing cameras",
+ "allCameras": "All cameras",
+ "existingPlaceholder": "Select at least one camera",
+ "existingDisabled": "No other cameras to copy to"
+ },
+ "categories": {
+ "legend": "Settings to clone",
+ "description": "Choose which settings to copy from the source camera.",
+ "selectAll": "Select all",
+ "selectNone": "Select none",
+ "resetDefaults": "Reset to defaults",
+ "general": "General",
+ "spatial": "Spatial settings",
+ "streams": "Streams",
+ "spatialWarningTitle": "Resolution mismatch",
+ "spatialWarning": "Source camera {{srcCamera}} detect resolution ({{srcWidth}}×{{srcHeight}}) differs from: {{cameras}}. Polygons may not align on those cameras. These defaults are off; enable to copy as-is.",
+ "restartHint": "Restart required",
+ "items": {
+ "record": "Recording",
+ "snapshots": "Snapshots",
+ "review": "Review",
+ "motion": "Motion detection",
+ "objects": "Objects",
+ "audio": "Audio detection",
+ "audio_transcription": "Audio transcription",
+ "notifications": "Notifications",
+ "birdseye": "Birdseye",
+ "mqtt": "MQTT",
+ "timestamp_style": "Timestamp style",
+ "onvif": "ONVIF",
+ "lpr": "License plate recognition",
+ "face_recognition": "Face recognition",
+ "semantic_search": "Semantic search",
+ "genai": "Generative AI",
+ "type": "Camera type (normal / dedicated LPR)",
+ "profiles": "Profiles",
+ "detect": "Detect dimensions",
+ "zones": "Zones",
+ "motion_mask": "Motion masks",
+ "object_masks": "Object masks",
+ "ffmpeg_live": "Stream URLs and roles"
+ }
+ },
+ "footer": {
+ "changeCount_zero": "No changes selected",
+ "changeCount_one": "{{count}} change will be applied",
+ "changeCount_other": "{{count}} changes will be applied",
+ "restartNeeded": "Restart will be required for some changes.",
+ "liveOnly": "All changes will apply live without a restart.",
+ "submit": "Clone",
+ "submitting": "Cloning…"
+ },
+ "toast": {
+ "success": "Settings copied to {{cameraName}}",
+ "successWithRestart": "Settings copied to {{cameraName}}. Restart Frigate to apply all changes.",
+ "successMulti_one": "Settings copied to {{count}} camera",
+ "successMulti_other": "Settings copied to {{count}} cameras",
+ "successMultiWithRestart_one": "Settings copied to {{count}} camera. Restart Frigate to apply all changes.",
+ "successMultiWithRestart_other": "Settings copied to {{count}} cameras. Restart Frigate to apply all changes.",
+ "partialFailure": "{{successCount}} sections applied; '{{failedSection}}' failed: {{errorMessage}}",
+ "partialFailureMulti": "Copied to {{successCount}} camera(s); failed for {{failed}}: {{errorMessage}}",
+ "newCameraPartialFailure": "Camera {{cameraName}} was created but some settings failed to copy: {{errorMessage}}",
+ "sourceMissing": "Source camera no longer exists",
+ "submitError": "Failed to clone camera: {{errorMessage}}"
+ }
}
},
"cameraReview": {
@@ -913,7 +1053,7 @@
},
"createUser": {
"title": "Create New User",
- "desc": "Add a new user account and specify an role for access to areas of the Frigate UI.",
+ "desc": "Add a new user account and specify a role for access to areas of the Frigate UI.",
"usernameOnlyInclude": "Username may only include letters, numbers, . or _",
"confirmPassword": "Please confirm your password"
},
@@ -1018,7 +1158,8 @@
},
"notificationUnavailable": {
"title": "Notifications Unavailable",
- "desc": "Web push notifications require a secure context (https://…). This is a browser limitation. Access Frigate securely to use notifications."
+ "desc": "Web push notifications require a secure context (https://…). This is a browser limitation. Access Frigate securely to use notifications.",
+ "descPwa": "On iOS, web push notifications are only available when Frigate is installed to your Home Screen. Open the Share menu, choose Add to Home Screen , then open Frigate from the new icon to register this device for notifications."
},
"globalSettings": {
"title": "Global Settings",
@@ -1101,10 +1242,19 @@
"cameras": "Cameras",
"loading": "Loading model information…",
"error": "Failed to load model information",
- "availableModels": "Available Models",
+ "noModelLoaded": "No Frigate+ model is currently loaded.",
+ "availableModels": "Available Frigate+ models",
"loadingAvailableModels": "Loading available models…",
+ "selectModel": "Select a model",
+ "noModelsAvailable": "No models available",
+ "filter": {
+ "ariaLabel": "Filter models by type",
+ "baseModels": "Base Models",
+ "fineTunedModels": "Fine-tuned Models"
+ },
"modelSelect": "Your available models on Frigate+ can be selected here. Note that only models compatible with your current detector configuration can be selected."
},
+ "changeInDetectorsAndModel": "Change model",
"unsavedChanges": "Unsaved Frigate+ settings changes",
"restart_required": "Restart required (Frigate+ model changed)",
"toast": {
@@ -1112,14 +1262,30 @@
"error": "Failed to save config changes: {{errorMessage}}"
}
},
- "detectionModel": {
- "plusActive": {
- "title": "Frigate+ model management",
- "label": "Current model source",
- "description": "This instance is running a Frigate+ model. Select or change your model in Frigate+ settings.",
- "goToFrigatePlus": "Go to Frigate+ settings",
- "showModelForm": "Manually configure a model"
- }
+ "detectorsAndModel": {
+ "title": "Detectors and model",
+ "description": "Configure the detector backend that runs object detection and the model it uses. Changes are saved together so the detector and model stay in sync.",
+ "cardTitles": {
+ "detector": "Detector Hardware",
+ "model": "Detection Model"
+ },
+ "tabs": {
+ "plus": "Frigate+",
+ "custom": "Custom Model"
+ },
+ "mismatch": {
+ "warning": "The current Frigate+ model \"{{model}}\" requires the {{required}} detector. Pick a compatible model below or switch to Custom Model before saving."
+ },
+ "plusModel": {
+ "requiresDetector": "Requires: {{detector}}",
+ "noModelSelected": "Select a Frigate+ model"
+ },
+ "toast": {
+ "saveSuccess": "Detectors and model settings saved. Restart Frigate to apply changes.",
+ "saveError": "Failed to save detector and model settings"
+ },
+ "unsavedChanges": "Unsaved detector and model changes",
+ "restartRequired": "Restart required (detector or model changed)"
},
"triggers": {
"documentTitle": "Triggers",
@@ -1324,12 +1490,30 @@
"keyLabel": "Key",
"valueLabel": "Value",
"keyPlaceholder": "New key",
- "remove": "Remove"
+ "remove": "Remove",
+ "providerNameLabel": "Provider name",
+ "providerNamePlaceholder": "e.g., openai",
+ "variableNameLabel": "Variable name",
+ "variableNamePlaceholder": "e.g., MY_VARIABLE",
+ "loggerNameLabel": "Logger name",
+ "loggerNamePlaceholder": "e.g., frigate.record",
+ "keyPatternError": "Use only letters, numbers, hyphens, and underscores (no spaces)"
},
"knownPlates": {
"namePlaceholder": "e.g., Wife's Car",
"platePlaceholder": "Plate number or regex"
},
+ "liveStreams": {
+ "streamNameLabel": "Stream name",
+ "streamNamePlaceholder": "e.g., Main HD Stream",
+ "go2rtcStreamLabel": "go2rtc stream",
+ "go2rtcStreamPlaceholder": "Select a go2rtc stream",
+ "go2rtcStreamSearch": "Search or enter a stream name…",
+ "noGo2rtcStreams": "No go2rtc streams configured",
+ "availableStreams": "Available streams",
+ "useCustom": "Use \"{{value}}\"",
+ "addStream": "Add stream"
+ },
"timezone": {
"defaultOption": "Use browser timezone"
},
@@ -1376,7 +1560,17 @@
}
},
"cameraInputs": {
- "itemTitle": "Stream {{index}}"
+ "itemTitle": "Stream {{index}}",
+ "sourceMode": {
+ "restream": "Restream (go2rtc)",
+ "manual": "Manual input path",
+ "go2rtcStreamLabel": "go2rtc stream",
+ "go2rtcStreamPlaceholder": "Select a go2rtc stream",
+ "noGo2rtcStreams": "No go2rtc streams configured",
+ "go2rtcStreamSearch": "Search streams...",
+ "availableStreams": "Available streams",
+ "noMatchingStreams": "No matching streams"
+ }
},
"restartRequiredField": "Restart required",
"restartRequiredFooter": "Configuration changed - Restart required",
@@ -1464,8 +1658,8 @@
"genaiRoles": {
"options": {
"embeddings": "Embedding",
- "vision": "Vision",
- "tools": "Tools"
+ "descriptions": "Descriptions",
+ "chat": "Chat"
}
},
"semanticSearchModel": {
@@ -1473,6 +1667,9 @@
"builtIn": "Built-in Models",
"genaiProviders": "GenAI Providers"
},
+ "semanticSearchModelSize": {
+ "notApplicable": "Not applicable for GenAI providers"
+ },
"review": {
"title": "Review Settings"
},
@@ -1491,9 +1688,25 @@
"searchPlaceholder": "Search...",
"addCustomLabel": "Add custom label...",
"genaiModel": {
- "placeholder": "Select model…",
- "search": "Search models…",
- "noModels": "No models available"
+ "placeholder": "Select or enter a model…",
+ "search": "Search or enter a model…",
+ "noModels": "No models available",
+ "available": "Available models",
+ "useCustom": "Use \"{{value}}\"",
+ "refresh": "Refresh models",
+ "probeFailed": "Failed to probe models",
+ "fetchedModels": "Successfully fetched model list"
+ },
+ "ptzPresets": {
+ "placeholder": "Select or enter a preset...",
+ "search": "Search or enter a preset...",
+ "noPresets": "No presets available",
+ "available": "Camera presets",
+ "useCustom": "Use \"{{value}}\""
+ },
+ "defaultRole": {
+ "admin": "Admin",
+ "viewer": "Viewer"
}
},
"globalConfig": {
@@ -1525,6 +1738,8 @@
"resetError": "Failed to reset settings",
"saveAllSuccess_one": "Saved {{count}} section successfully.",
"saveAllSuccess_other": "All {{count}} sections saved successfully.",
+ "saveAllSuccessRestartRequired_one": "Saved {{count}} section successfully. Restart Frigate to apply your changes.",
+ "saveAllSuccessRestartRequired_other": "All {{count}} sections saved successfully. Restart Frigate to apply your changes.",
"saveAllPartial_one": "{{successCount}} of {{totalCount}} section saved. {{failCount}} failed.",
"saveAllPartial_other": "{{successCount}} of {{totalCount}} sections saved. {{failCount}} failed.",
"saveAllFailure": "Failed to save all sections."
@@ -1581,6 +1796,7 @@
"addStream": "Add stream",
"addStreamDesc": "Enter a name for the new stream. This name will be used to reference the stream in your camera configuration.",
"addUrl": "Add URL",
+ "sourceNumber": "Source {{index}}",
"streamName": "Stream name",
"streamNamePlaceholder": "e.g., front_door",
"streamUrlPlaceholder": "e.g., rtsp://user:pass@192.168.1.100/stream",
@@ -1614,18 +1830,90 @@
"audioMp3": "Transcode to MP3",
"audioExclude": "Exclude",
"hardwareNone": "No hardware acceleration",
- "hardwareAuto": "Automatic hardware acceleration"
+ "hardwareAuto": "Automatic (recommended)",
+ "hardwareVaapi": "VAAPI",
+ "hardwareCuda": "CUDA",
+ "hardwareV4l2m2m": "V4L2 M2M",
+ "hardwareDxva2": "DXVA2",
+ "hardwareVideotoolbox": "VideoToolbox",
+ "addVideoCodec": "Add video codec",
+ "addAudioCodec": "Add audio codec",
+ "removeCodec": "Remove codec"
+ }
+ },
+ "birdseye": {
+ "trackingMode": {
+ "objects": "Objects",
+ "motion": "Motion",
+ "continuous": "Continuous"
+ },
+ "cameraOrder": {
+ "label": "Camera order",
+ "description": "Drag cameras to set their order in the Birdseye layout.",
+ "reorderHandle": "Drag to reorder",
+ "saving": "Saving…",
+ "saved": "Saved"
+ }
+ },
+ "retainMode": {
+ "all": "All",
+ "motion": "Motion",
+ "active_objects": "Active Objects"
+ },
+ "previewQuality": {
+ "very_high": "Very High",
+ "high": "High",
+ "medium": "Medium",
+ "low": "Low",
+ "very_low": "Very Low"
+ },
+ "ui": {
+ "timeFormat": {
+ "browser": "Browser",
+ "12hour": "12 hour",
+ "24hour": "24 hour"
+ },
+ "unitSystem": {
+ "metric": "Metric",
+ "imperial": "Imperial"
+ }
+ },
+ "review": {
+ "imageSource": {
+ "recordings": "Recordings",
+ "previews": "Previews"
+ }
+ },
+ "logger": {
+ "logLevel": {
+ "debug": "Debug",
+ "info": "Info",
+ "warning": "Warning",
+ "error": "Error",
+ "critical": "Critical"
}
},
"onvif": {
"profileAuto": "Auto",
- "profileLoading": "Loading profiles..."
+ "profileLoading": "Loading profiles...",
+ "autotracking": {
+ "zooming": {
+ "disabled": "Disabled",
+ "absolute": "Absolute",
+ "relative": "Relative"
+ }
+ }
+ },
+ "modelSize": {
+ "small": "Small",
+ "large": "Large"
},
"configMessages": {
"review": {
"recordDisabled": "Recording is disabled, review items will not be generated.",
"detectDisabled": "Object detection is disabled. Review items require detected objects to categorize alerts and detections.",
- "allNonAlertDetections": "All non-alert activity will be included as detections."
+ "allNonAlertDetections": "All non-alert activity will be included as detections.",
+ "genaiImageSourceRecordingsRecordDisabled": "Image source is set to 'recordings', but recording is disabled. Frigate will fall back to preview images."
},
"audio": {
"noAudioRole": "No streams have the audio role defined. You must enable the audio role for audio detection to function."
@@ -1634,15 +1922,34 @@
"audioDetectionDisabled": "Audio detection is not enabled for this camera. Audio transcription requires audio detection to be active."
},
"detect": {
- "fpsGreaterThanFive": "Setting the detect FPS higher than 5 is not recommended."
+ "fpsGreaterThanFive": "Setting the detect FPS higher than 5 is not recommended. Higher values may cause performance issues and will not provide any benefit.",
+ "disabled": "Object detection is disabled. Snapshots, review items, and enrichments such as face recognition, license plate recognition, and Generative AI will not function.",
+ "resolutionShouldBeMultipleOfFour": "For best results, detect width and height should be multiples of 4. Other even values may produce visual artifacts or slight distortion in the detect stream.",
+ "aspectRatioMismatch": "The width and height you've entered don't match the aspect ratio of your current detect resolution. This may produce a stretched or distorted image.",
+ "maxFramesSet": "Setting max frames overrides default behavior and disables stationary object tracking. There are very few situations where this is needed, use with caution.",
+ "squareResolution": "A square detect resolution is unusual. The detect width and height should match your camera's aspect ratio (for example, 16:9), not the dimensions of the object detection model. A mismatched aspect ratio can stretch the image and reduce detection accuracy.",
+ "resolutionHigh": "This detect resolution is higher than recommended and may cause increased resource usage without improving detection accuracy. A detect resolution at or below 1080p is recommended for most cameras.",
+ "globalResolutionMultipleCameras": "A global detect resolution is set while multiple cameras are configured. Unless all cameras share the same resolution and aspect ratio, the detect width and height should be defined per camera to match each camera's native aspect ratio."
+ },
+ "model": {
+ "optimizedFor320": "Frigate is optimized for a 320x320 model, which is the best choice for most setups. A 640x640 model is slower and only helps in specific scenarios.",
+ "inputDimensionsNotDetectResolution": "Model input width and height are the input dimensions of the object detection model, not your camera's detect resolution. They should match the dimensions of the model you're using — typically a square size like 320x320 or 640x640."
+ },
+ "ffmpeg": {
+ "hwaccelManualNotRecommended": "Manual hardware acceleration arguments are not recommended. Unless a specific requirement exists, select the preset that matches your hardware."
+ },
+ "objects": {
+ "genaiNoDescriptionsProvider": "You must configure a GenAI provider with the 'descriptions' role for descriptions to be generated."
},
"faceRecognition": {
- "globalDisabled": "Face recognition is not enabled at the global level. Enable it in global settings for camera-level face recognition to function.",
- "personNotTracked": "Face recognition requires the 'person' object to be tracked. Ensure 'person' is in the object tracking list."
+ "globalDisabled": "The face recognition enrichment must be enabled for face recognition features to function on this camera.",
+ "personNotTracked": "Face recognition requires the 'person' object to be tracked. Enable 'person' in Objects for this camera.",
+ "modelSizeLarge": "The 'large' model requires a GPU or NPU for reasonable performance. Use 'small' on CPU-only systems."
},
"lpr": {
- "globalDisabled": "License plate recognition is not enabled at the global level. Enable it in global settings for camera-level LPR to function.",
- "vehicleNotTracked": "License plate recognition requires 'car' or 'motorcycle' to be tracked."
+ "globalDisabled": "The license plate recognition enrichment must be enabled for LPR features to function on this camera.",
+ "vehicleNotTracked": "License plate recognition requires 'car' or 'motorcycle' to be tracked. Enable 'car' or 'motorcycle' in Objects for this camera.",
+ "modelSizeLarge": "The 'large' model is optimized for multi-line license plates. The 'small' model provides better performance over 'large' and should be used unless your region uses multi-line plate formats."
},
"record": {
"noRecordRole": "No streams have the record role defined. Recording will not function."
@@ -1656,6 +1963,12 @@
"detectors": {
"mixedTypes": "All detectors must use the same type. Remove existing detectors to use a different type.",
"mixedTypesSuggestion": "All detectors must use the same type. Remove existing detectors or select {{type}}."
+ },
+ "semanticSearch": {
+ "jinav2SmallModelSize": "The 'small' size with the Jina V2 model has high RAM and inference cost. The 'large' model with a discrete GPU is recommended."
+ },
+ "onvif": {
+ "autotrackingNoZones": "Autotracking requires at least one zone. Define a zone for this camera in Masks / Zones, then set it as a required zone below."
}
}
}
diff --git a/web/public/locales/en/views/system.json b/web/public/locales/en/views/system.json
index 6c3f37f71a..9f387a7f30 100644
--- a/web/public/locales/en/views/system.json
+++ b/web/public/locales/en/views/system.json
@@ -174,9 +174,27 @@
"error": "Error: {{error}}",
"tips": {
"title": "Camera Probe Info"
+ },
+ "keyframes": {
+ "title": "Keyframe analysis",
+ "analyzing": "Analyzing keyframes... {{seconds}} seconds remaining",
+ "stillAnalyzing": "Still analyzing keyframes...",
+ "recordStream": "Record stream:",
+ "keyframeCount": "Keyframes observed:",
+ "observedDuration": "Observed duration:",
+ "gap": "Keyframe gap (min / avg / max):",
+ "segmentLength": "Recording segment length:",
+ "ok": "Keyframes every ~{{seconds}}s, good for recording and playback.",
+ "warning": "Sparse or variable keyframes (longest gap ~{{seconds}}s), likely a smart codec (H.264+/H.265+), this is not recommended.",
+ "error": "Keyframe gap (~{{seconds}}s) exceeds the recording segment length ({{segmentTime}}s). Some segments may have no keyframe, which breaks playback. Disable the smart/+ codec on the camera or shorten its keyframe interval.",
+ "unknown": "Couldn't determine keyframe spacing.",
+ "recordDisabled": "Recording is disabled for this camera."
}
},
"framesAndDetections": "Frames / Detections",
+ "noCameras": {
+ "title": "No Cameras Found"
+ },
"label": {
"camera": "camera",
"detect": "detect",
diff --git a/web/public/locales/es/common.json b/web/public/locales/es/common.json
index 49e06c508b..a1957c06bb 100644
--- a/web/public/locales/es/common.json
+++ b/web/public/locales/es/common.json
@@ -154,7 +154,9 @@
"gl": "Galego (Gallego)",
"id": "Bahasa Indonesia (Indonesio)",
"ur": "اردو (Urdu)",
- "hr": "Hrvatski (Croata)"
+ "hr": "Hrvatski (Croata)",
+ "bs": "Bosanski (Bosnio)",
+ "zhHant": "繁體中文 (Chino Tradicional)"
},
"appearance": "Apariencia",
"darkMode": {
@@ -195,7 +197,11 @@
"explore": "Explorar",
"uiPlayground": "Zona de pruebas de la interfaz de usuario",
"faceLibrary": "Biblioteca de rostros",
- "classification": "Clasificación"
+ "classification": "Clasificación",
+ "profiles": "Perfiles",
+ "actions": "Acciones",
+ "features": "Funciones",
+ "chat": "Chat"
},
"unit": {
"speed": {
@@ -251,7 +257,19 @@
"saving": "Guardando…",
"exitFullscreen": "Salir de pantalla completa",
"on": "ENCENDIDO",
- "continue": "Continuar"
+ "continue": "Continuar",
+ "add": "Añadir",
+ "applying": "Aplicando…",
+ "undo": "Deshacer",
+ "copiedToClipboard": "Copiado al portapapeles",
+ "modified": "Modificado",
+ "overridden": "Sobrescrito",
+ "resetToGlobal": "Restablecer a global",
+ "resetToDefault": "Restablecer valores predeterminados",
+ "saveAll": "Guardar todo",
+ "savingAll": "Guardando todo…",
+ "undoAll": "Deshacer todo",
+ "retry": "Reintentar"
},
"toast": {
"save": {
@@ -259,7 +277,8 @@
"noMessage": "No se pudieron guardar los cambios de configuración",
"title": "No se pudieron guardar los cambios de configuración: {{errorMessage}}"
},
- "title": "Guardar"
+ "title": "Guardar",
+ "success": "Cambios de configuración guardados correctamente."
},
"copyUrlToClipboard": "URL copiada al portapapeles."
},
@@ -313,5 +332,10 @@
"field": {
"optional": "Opcional",
"internalID": "La ID interna que usa Frigate en la configuración y en la base de datos"
+ },
+ "no_items": "No hay elementos",
+ "validation_errors": "Errores de validación",
+ "credentialField": {
+ "savedPlaceholder": "Guardado — déjalo en blanco para mantener el actual"
}
}
diff --git a/web/public/locales/es/components/camera.json b/web/public/locales/es/components/camera.json
index 05bca27427..1ce25fa472 100644
--- a/web/public/locales/es/components/camera.json
+++ b/web/public/locales/es/components/camera.json
@@ -68,7 +68,10 @@
"stream": "Transmitir"
},
"birdseye": "Vista Aérea"
- }
+ },
+ "showAll": "Mostrar todos los grupos de cámaras",
+ "showLess": "Mostrar menos",
+ "editGroups": "Editar grupos de cámaras"
},
"debug": {
"options": {
diff --git a/web/public/locales/es/components/dialog.json b/web/public/locales/es/components/dialog.json
index e8f59f05a0..d86eb5036a 100644
--- a/web/public/locales/es/components/dialog.json
+++ b/web/public/locales/es/components/dialog.json
@@ -71,13 +71,85 @@
"endTimeMustAfterStartTime": "La hora de finalización debe ser posterior a la hora de inicio"
},
"success": "Exportación iniciada con éxito. Ver el archivo en la página exportaciones.",
- "view": "Ver"
+ "view": "Vista",
+ "queued": "Exportación en cola. Consulta el progreso en la página de exportaciones.",
+ "batchSuccess_one": "Se inició 1 exportación. Abriendo el caso ahora.",
+ "batchSuccess_many": "Se iniciaron {{count}} exportaciones. Abriendo el caso ahora.",
+ "batchSuccess_other": "Se iniciaron {{count}} exportaciones. Abriendo el caso ahora.",
+ "batchPartial": "Se iniciaron {{successful}} de {{total}} exportaciones. Cámaras fallidas: {{failedCameras}}",
+ "batchFailed": "No se pudieron iniciar {{total}} exportaciones. Cámaras fallidas: {{failedCameras}}",
+ "batchQueuedSuccess_one": "1 exportación en cola. Abriendo el caso ahora.",
+ "batchQueuedSuccess_many": "{{count}} exportaciones en cola. Abriendo el caso ahora.",
+ "batchQueuedSuccess_other": "{{count}} exportaciones en cola. Abriendo el caso ahora.",
+ "batchQueuedPartial": "{{successful}} de {{total}} exportaciones en cola. Cámaras fallidas: {{failedCameras}}",
+ "batchQueueFailed": "No se pudieron poner en cola {{total}} exportaciones. Cámaras fallidas: {{failedCameras}}"
},
"fromTimeline": {
"saveExport": "Guardar exportación",
- "previewExport": "Vista previa de la exportación"
+ "previewExport": "Vista previa de la exportación",
+ "queueingExport": "Poniendo exportación en cola...",
+ "useThisRange": "Usar este intervalo"
},
- "selectOrExport": "Seleccionar o exportar"
+ "selectOrExport": "Seleccionar o exportar",
+ "case": {
+ "label": "Caso",
+ "newCaseDescriptionPlaceholder": "Descripción de caso",
+ "newCaseOption": "Crear nuevo caso",
+ "newCaseNamePlaceholder": "Nombre del nuevo caso",
+ "nonAdminHelp": "Se creará un nuevo caso para estas exportaciones.",
+ "placeholder": "Selecciona un caso"
+ },
+ "queueing": "Poniendo la exportación en cola…",
+ "tabs": {
+ "export": "Una Cámara",
+ "multiCamera": "Multicámara"
+ },
+ "multiCamera": {
+ "timeRange": "Intervalo de tiempo",
+ "selectFromTimeline": "Seleccionar desde la línea de tiempo",
+ "cameraSelection": "Cámaras",
+ "cameraSelectionHelp": "Las cámaras con objetos detectados en este intervalo de tiempo están preseleccionadas",
+ "checkingActivity": "Comprobando actividad de las cámaras...",
+ "noCameras": "No hay cámaras disponibles",
+ "detectionCount_one": "1 objeto detectado",
+ "detectionCount_many": "{{count}} objetos detectados",
+ "detectionCount_other": "{{count}} objetos detectados",
+ "nameLabel": "Nombre de la exportación",
+ "namePlaceholder": "Nombre base opcional para estas exportaciones",
+ "queueingButton": "Poniendo exportaciones en cola...",
+ "exportButton_one": "Exportar 1 cámara",
+ "exportButton_many": "Exportar {{count}} cámaras",
+ "exportButton_other": "Exportar {{count}} cámaras",
+ "searchOrSelectGroup": "Busca o selecciona un grupo de cámaras…",
+ "selectAll": "Seleccionar todas las cámaras",
+ "clearSelection": "Borrar selección",
+ "selectWithActivity": "Cámaras con objetos rastreados",
+ "selectGroup": "Seleccionar grupo",
+ "noMatchingCameras": "Ninguna cámara coincide con tu búsqueda",
+ "selectedCount": "{{selected}} / {{total}} seleccionadas"
+ },
+ "multi": {
+ "title_one": "Exportar 1 revisión",
+ "title_many": "Exportar {{count}} revisiones",
+ "title_other": "Exportar {{count}} revisiones",
+ "description": "Exportar cada revisión seleccionada. Todas las exportaciones se agruparán en un único caso.",
+ "descriptionNoCase": "Exportar cada revisión seleccionada.",
+ "caseNamePlaceholder": "Exportación de revisión - {{date}}",
+ "exportButton_one": "Exportar 1 revisión",
+ "exportButton_many": "Exportar {{count}} revisiones",
+ "exportButton_other": "Exportar {{count}} revisiones",
+ "exportingButton": "Exportando...",
+ "toast": {
+ "started_one": "Se inició 1 exportación. Abriendo el caso ahora.",
+ "started_many": "Se iniciaron {{count}} exportaciones. Abriendo el caso ahora.",
+ "started_other": "Se iniciaron {{count}} exportaciones. Abriendo el caso ahora.",
+ "startedNoCase_one": "Se inició 1 exportación.",
+ "startedNoCase_many": "Se iniciaron {{count}} exportaciones.",
+ "startedNoCase_other": "Se iniciaron {{count}} exportaciones.",
+ "partial": "Se iniciaron {{successful}} de {{total}} exportaciones. Fallidas: {{failedItems}}",
+ "failed": "No se pudieron iniciar {{total}} exportaciones. Fallidas: {{failedItems}}"
+ }
+ }
},
"streaming": {
"restreaming": {
@@ -124,6 +196,14 @@
"markAsReviewed": "Marcar como revisado",
"deleteNow": "Eliminar ahora",
"markAsUnreviewed": "Marcar como no revisado"
+ },
+ "shareTimestamp": {
+ "description": "Comparta una URL con marca de tiempo de la posición actual del reproductor o elija una marca de tiempo personalizada. Tenga en cuenta que esta no es una URL pública para compartir y solo es accesible para los usuarios que tienen acceso a Frigate y a esta cámara.",
+ "label": "Compartir marca de tiempo",
+ "title": "Compartir marca de tiempo",
+ "custom": "Marca de tiempo personalizada",
+ "button": "Compartir URL de la marca de tiempo",
+ "shareTitle": "Marca de tiempo de revisión de Frigate: {{camera}}"
}
},
"imagePicker": {
diff --git a/web/public/locales/es/components/player.json b/web/public/locales/es/components/player.json
index 2a3e4deb1f..e650046466 100644
--- a/web/public/locales/es/components/player.json
+++ b/web/public/locales/es/components/player.json
@@ -3,7 +3,8 @@
"noPreviewFoundFor": "No se encontró vista previa para {{cameraName}}",
"submitFrigatePlus": {
"submit": "Enviar",
- "title": "¿Enviar este fotograma a Frigate+?"
+ "title": "¿Enviar este fotograma a Frigate+?",
+ "previewError": "No se pudo cargar la vista previa de la instantánea. Es posible que la grabación no esté disponible en este momento."
},
"streamOffline": {
"desc": "No se han recibido fotogramas en la transmisión detect de {{cameraName}}, revisa los registros de errores",
@@ -47,5 +48,6 @@
}
},
"livePlayerRequiredIOSVersion": "Se requiere iOS 17.1 o superior para este tipo de transmisión en vivo.",
- "noRecordingsFoundForThisTime": "No se encontraron grabaciones para este momento"
+ "noRecordingsFoundForThisTime": "No se encontraron grabaciones para este momento",
+ "cameraOff": "La cámara está apagada"
}
diff --git a/web/public/locales/es/config/cameras.json b/web/public/locales/es/config/cameras.json
index aeb6083714..95e412ff4e 100644
--- a/web/public/locales/es/config/cameras.json
+++ b/web/public/locales/es/config/cameras.json
@@ -1,6 +1,6 @@
{
"name": {
- "label": "Nombre de cámara",
+ "label": "Nombre de Cámara",
"description": "El nombre de la cámara es necesario"
},
"enabled": {
@@ -8,7 +8,7 @@
"description": "Habilitado"
},
"audio": {
- "label": "Eventos de audio",
+ "label": "Detección de audio",
"description": "Configuración para la detección de eventos basada en audio para esta cámara.",
"enabled": {
"label": "Habilitar la detección de audio",
@@ -24,33 +24,110 @@
},
"listen": {
"label": "Tipos de escucha",
- "description": "Lista de tipos de eventos de audio a detectar (por ejemplo: ladrido, alarma de incendios, grito, voz, alarido)."
+ "description": "Lista de tipos de eventos de audio a detectar (por ejemplo: ladrar, alarma de incendio, habla, gritar)."
},
"filters": {
"label": "Filtros de audio",
- "description": "Ajustes de filtrado por tipo de audio, como umbrales de confianza utilizados para reducir los falsos positivos."
+ "description": "Ajustes de filtro por tipo de audio, como umbrales de confianza utilizados para reducir los falsos positivos.",
+ "threshold": {
+ "label": "Confianza mínima de audio",
+ "description": "Umbral mínimo de confianza para que se cuente el evento de audio."
+ }
},
"enabled_in_config": {
- "description": "Indica si la detección de audio estaba habilitada originalmente en el archivo de configuración estática.",
- "label": "Estado original del audio"
+ "description": "Indica si la detección de audio estaba habilitada originalmente en el archivo de configuración estático.",
+ "label": "Estado de audio original"
},
"num_threads": {
- "label": "Hilos de detección"
+ "label": "Hilos de detección",
+ "description": "Número de hilos a usar para procesamiento de detección de audio."
}
},
"friendly_name": {
"label": "Nombre descriptivo",
"description": "Nombre descriptivo de la cámara utilizado en la interfaz de usuario de Frigate"
},
- "label": "Configuración de Cámara",
+ "label": "Configuración de cámara",
"onvif": {
"profile": {
- "label": "Perfil ONVIF"
+ "label": "Perfil ONVIF",
+ "description": "Perfil multimedia ONVIF específico que se utilizará para el control PTZ, identificado por token o nombre. Si no se especifica, se selecciona automáticamente el primer perfil con una configuración PTZ válida."
+ },
+ "autotracking": {
+ "zoom_factor": {
+ "description": "Controla el nivel de zoom en los objetos rastreados. Los valores más bajos mantienen una mayor parte de la escena a la vista; los valores más altos acercan la imagen, pero pueden provocar la pérdida del rastreo. Valores entre 0.1 y 0.75.",
+ "label": "Factor de zoom"
+ },
+ "calibrate_on_startup": {
+ "description": "Mida la velocidad de los motores PTZ al encenderlos para mejorar la precisión del seguimiento. Frigate actualizará la configuración con los `movement_weights` tras la calibración.",
+ "label": "Calibrar al iniciar"
+ },
+ "description": "Realice un seguimiento automático de objetos en movimiento y manténgalos centrados en el encuadre mediante movimientos de cámara PTZ.",
+ "zooming": {
+ "description": "Control del comportamiento del zoom: deshabilitado (solo panorámica/inclinación), absoluto (mayor compatibilidad) o relativo (panorámica/inclinación/zoom simultáneos).",
+ "label": "Modo de zoom"
+ },
+ "return_preset": {
+ "description": "Nombre del preajuste ONVIF configurado en el firmware de la cámara al que regresar una vez finalizado el seguimiento.",
+ "label": "Preajuste de retorno"
+ },
+ "timeout": {
+ "description": "Espere esta cantidad de segundos después de perder el seguimiento antes de devolver la cámara a la posición preestablecida.",
+ "label": "Tiempo de espera de retorno"
+ },
+ "label": "Seguimiento automático",
+ "enabled": {
+ "label": "Habilitar seguimiento automático",
+ "description": "Habilita o deshabilita el seguimiento automático con cámara PTZ de objetos detectados."
+ },
+ "track": {
+ "label": "Objetos rastreados",
+ "description": "Lista de tipos de objetos que deben activar el seguimiento automático."
+ },
+ "required_zones": {
+ "label": "Zonas requeridas",
+ "description": "Los objetos deben entrar en una de estas zonas antes de que comience el seguimiento automático."
+ },
+ "movement_weights": {
+ "label": "Pesos de movimiento",
+ "description": "Valores de calibración generados automáticamente por la calibración de la cámara. No los modifiques manualmente."
+ },
+ "enabled_in_config": {
+ "label": "Estado original de autoseguimiento",
+ "description": "Campo interno para rastrear si el seguimiento automático estaba habilitado en la configuración."
+ }
+ },
+ "tls_insecure": {
+ "description": "Omitir la verificación TLS y deshabilitar la autenticación digest para ONVIF (no seguro; usar solo en redes seguras).",
+ "label": "Deshabilitar verificación TLS"
+ },
+ "label": "ONVIF",
+ "description": "Ajustes de conexión ONVIF y seguimiento automático PTZ para esta cámara.",
+ "host": {
+ "label": "Host ONVIF",
+ "description": "Host (y esquema opcional) para el servicio ONVIF de esta cámara."
+ },
+ "port": {
+ "label": "Puerto ONVIF",
+ "description": "Número de puerto del servicio ONVIF."
+ },
+ "user": {
+ "label": "Nombre de usuario ONVIF",
+ "description": "Nombre de usuario para la autenticación ONVIF; algunos dispositivos requieren un usuario administrador para ONVIF."
+ },
+ "password": {
+ "label": "Contraseña ONVIF",
+ "description": "Contraseña para la autenticación ONVIF."
+ },
+ "ignore_time_mismatch": {
+ "label": "Ignorar discrepancia horaria",
+ "description": "Ignora las diferencias de sincronización horaria entre la cámara y el servidor Frigate para la comunicación ONVIF."
}
},
"zones": {
"distances": {
- "label": "Distancias reales"
+ "label": "Distancias reales",
+ "description": "Distancias reales opcionales para cada lado del cuadrilátero de la zona, usadas para cálculos de velocidad o distancia. Debe tener exactamente 4 valores si se establece."
},
"coordinates": {
"description": "Coordenadas del polígono que definen el área de la zona. Puede ser una cadena separada por comas o una lista de cadenas de coordenadas. Las coordenadas deben ser relativas (0-1) o absolutas (heredadas).",
@@ -83,7 +160,42 @@
"max_area": {
"description": "Área máxima del cuadro delimitador (píxeles o porcentaje) permitida para este tipo de objeto. Puede expresarse en píxeles (entero) o como porcentaje (decimal entre 0,000001 y 0,99).",
"label": "Área máxima del objeto"
+ },
+ "description": "Filtros para aplicar a los objetos dentro de esta zona. Se utilizan para reducir los falsos positivos o restringir qué objetos se consideran presentes en la zona.",
+ "label": "Filtros de zona",
+ "min_area": {
+ "label": "Área mínima de objeto",
+ "description": "Área mínima del cuadro delimitador (píxeles o porcentaje) necesaria para este tipo de objeto. Puede ser píxeles (int) o porcentaje (float entre 0.000001 y 0.99)."
}
+ },
+ "objects": {
+ "description": "Lista de tipos de objetos (del mapa de etiquetas) que pueden activar esta zona. Puede ser una cadena de texto o una lista de cadenas. Si está vacío, se consideran todos los objetos.",
+ "label": "Objetos activadores"
+ },
+ "description": "Las zonas le permiten definir un área específica del fotograma, de modo que pueda determinar si un objeto se encuentra o no dentro de un área determinada.",
+ "speed_threshold": {
+ "description": "Velocidad mínima (en unidades del mundo real, si se han configurado distancias) requerida para que un objeto se considere presente en la zona. Se utiliza para los disparadores de zona basados en la velocidad.",
+ "label": "Velocidad mínima"
+ },
+ "friendly_name": {
+ "description": "Un nombre fácil de usar para la zona, que se muestra en la interfaz de usuario de Frigate. Si no se especifica, se utilizará una versión formateada del nombre de la zona.",
+ "label": "Nombre de zona"
+ },
+ "inertia": {
+ "description": "Número de fotogramas consecutivos en los que se debe detectar un objeto dentro de la zona antes de considerarlo presente. Ayuda a filtrar las detecciones transitorias.",
+ "label": "Fotogramas de inercia"
+ },
+ "loitering_time": {
+ "description": "Número de segundos que un objeto debe permanecer en la zona para ser considerado como merodeo. Establezca en 0 para desactivar la detección de merodeo.",
+ "label": "Segundos de permanencia"
+ },
+ "label": "Zonas",
+ "enabled": {
+ "label": "Habilitado",
+ "description": "Habilita o deshabilita esta zona. Las zonas deshabilitadas se ignoran en tiempo de ejecución."
+ },
+ "enabled_in_config": {
+ "label": "Mantiene el registro del estado original de la zona."
}
},
"objects": {
@@ -100,7 +212,746 @@
"use_snapshot": {
"label": "Usar instantáneas",
"description": "Usar instantáneas de objetos en lugar de miniaturas para la generación de descripciones de GenAI."
+ },
+ "send_triggers": {
+ "after_significant_updates": {
+ "description": "Envía una solicitud a GenAI tras un número especificado de actualizaciones significativas del objeto rastreado.",
+ "label": "Activador temprano de GenAI"
+ },
+ "description": "Define cuándo se deben enviar los fotogramas a GenAI (al finalizar, después de las actualizaciones, etc.).",
+ "label": "Activadores de GenAI",
+ "tracked_object_end": {
+ "label": "Enviar al finalizar",
+ "description": "Envía una solicitud a GenAI cuando finaliza el objeto rastreado."
+ }
+ },
+ "required_zones": {
+ "description": "Zonas en las que deben ubicarse los objetos para ser elegibles para la generación de descripciones con GenAI.",
+ "label": "Zonas requeridas"
+ },
+ "prompt": {
+ "label": "Prompt de descripción",
+ "description": "Plantilla de prompt predeterminada usada al generar descripciones con GenAI."
+ },
+ "object_prompts": {
+ "label": "Prompts de objetos",
+ "description": "Prompts por objeto para personalizar las salidas de GenAI para etiquetas concretas."
+ },
+ "objects": {
+ "label": "Objetos de GenAI",
+ "description": "Lista de etiquetas de objetos que se enviarán a GenAI de forma predeterminada."
+ },
+ "debug_save_thumbnails": {
+ "label": "Guardar miniaturas",
+ "description": "Guarda las miniaturas enviadas a GenAI para depuración y revisión."
+ },
+ "enabled_in_config": {
+ "label": "Estado original de GenAI",
+ "description": "Indica si GenAI estaba habilitado en la configuración estática original."
+ }
+ },
+ "label": "Objetos",
+ "description": "Valores predeterminados de seguimiento de objetos, incluidas las etiquetas que se rastrean y los filtros por objeto.",
+ "track": {
+ "label": "Objetos a rastrear",
+ "description": "Lista de etiquetas de objetos a rastrear para esta cámara."
+ },
+ "filters": {
+ "label": "Filtros de objetos",
+ "description": "Filtros aplicados a los objetos detectados para reducir falsos positivos (área, relación, confianza).",
+ "min_area": {
+ "label": "Área mínima de objeto",
+ "description": "Área mínima del cuadro delimitador (píxeles o porcentaje) necesaria para este tipo de objeto. Puede ser píxeles (int) o porcentaje (float entre 0.000001 y 0.99)."
+ },
+ "max_area": {
+ "label": "Área máxima de objeto",
+ "description": "Área máxima del cuadro delimitador (píxeles o porcentaje) permitida para este tipo de objeto. Puede ser píxeles (int) o porcentaje (float entre 0.000001 y 0.99)."
+ },
+ "min_ratio": {
+ "label": "Relación de aspecto mínima",
+ "description": "Relación mínima anchura/altura necesaria para que el cuadro delimitador sea válido."
+ },
+ "max_ratio": {
+ "label": "Relación de aspecto máxima",
+ "description": "Relación máxima anchura/altura permitida para que el cuadro delimitador sea válido."
+ },
+ "threshold": {
+ "label": "Umbral de confianza",
+ "description": "Umbral medio de confianza de detección necesario para que el objeto se considere un positivo verdadero."
+ },
+ "min_score": {
+ "label": "Confianza mínima",
+ "description": "Confianza mínima de detección en un único fotograma necesaria para que el objeto se contabilice."
+ },
+ "mask": {
+ "label": "Máscara de filtro",
+ "description": "Coordenadas del polígono que definen dónde se aplica este filtro dentro del fotograma."
+ },
+ "raw_mask": {
+ "label": "Máscara sin procesar"
+ }
+ },
+ "mask": {
+ "label": "Máscara de objeto",
+ "description": "Polígono de máscara usado para evitar la detección de objetos en áreas especificadas."
+ }
+ },
+ "mqtt": {
+ "label": "MQTT",
+ "required_zones": {
+ "description": "Zonas en las que debe entrar un objeto para que se publique una imagen MQTT.",
+ "label": "Zonas requeridas"
+ },
+ "description": "Ajustes de publicación de imágenes MQTT.",
+ "enabled": {
+ "label": "Enviar imagen",
+ "description": "Habilita la publicación de instantáneas de objetos en temas MQTT para esta cámara."
+ },
+ "timestamp": {
+ "label": "Añadir marca de tiempo",
+ "description": "Superpone una marca de tiempo en las imágenes publicadas en MQTT."
+ },
+ "bounding_box": {
+ "label": "Añadir cuadro delimitador",
+ "description": "Dibuja cuadros delimitadores en las imágenes publicadas mediante MQTT."
+ },
+ "crop": {
+ "label": "Recortar imagen",
+ "description": "Recorta las imágenes publicadas en MQTT al cuadro delimitador del objeto detectado."
+ },
+ "height": {
+ "label": "Altura de imagen",
+ "description": "Altura (píxeles) a la que redimensionar las imágenes publicadas mediante MQTT."
+ },
+ "quality": {
+ "label": "Calidad JPEG",
+ "description": "Calidad JPEG de las imágenes publicadas en MQTT (0-100)."
+ }
+ },
+ "notifications": {
+ "email": {
+ "label": "Email de notificacion",
+ "description": "Dirección de correo electrónico usada para notificaciones push o requerida por ciertos proveedores de notificaciones."
+ },
+ "label": "Notificaciones",
+ "description": "Ajustes para habilitar y controlar las notificaciones de esta cámara.",
+ "enabled": {
+ "label": "Habilitar notificaciones",
+ "description": "Habilita o deshabilita las notificaciones para esta cámara."
+ },
+ "cooldown": {
+ "label": "Periodo de enfriamiento",
+ "description": "Periodo de enfriamiento (segundos) entre notificaciones para evitar saturar a los destinatarios."
+ },
+ "enabled_in_config": {
+ "label": "Estado original de notificaciones",
+ "description": "Indica si las notificaciones estaban habilitadas en la configuración estática original."
+ }
+ },
+ "audio_transcription": {
+ "description": "Configuración para la transcripción de audio en directo y de voz utilizada para eventos y subtítulos en directo.",
+ "enabled": {
+ "label": "Habilitar transcripción",
+ "description": "Habilitar o deshabilitar la transcripción de eventos de audio activados manualmente."
+ },
+ "label": "Transcripción de audio",
+ "enabled_in_config": {
+ "label": "Estado original de la transcripción"
+ },
+ "live_enabled": {
+ "label": "Transcripción en directo",
+ "description": "Habilitar la transcripción en directo del audio a medida que se recibe."
+ }
+ },
+ "motion": {
+ "skip_motion_threshold": {
+ "description": "Si se establece en un valor entre 0,0 y 1,0, y más de esta fracción de la imagen cambia en un solo fotograma, el detector no devolverá cuadros de movimiento y se recalibrará inmediatamente. Esto puede ahorrar recursos de CPU y reducir los falsos positivos durante tormentas eléctricas, tempestades, etc., aunque podría pasar por alto eventos reales, como el seguimiento automático de un objeto por parte de una cámara PTZ. La disyuntiva está entre descartar unos cuantos megabytes de grabaciones o revisar un par de clips cortos. Deje este parámetro sin establecer (None) para desactivar esta función.",
+ "label": "Omitir umbral de movimiento"
+ },
+ "lightning_threshold": {
+ "description": "Umbral para detectar e ignorar breves picos de luz (un valor menor indica mayor sensibilidad; valores entre 0,3 y 1,0). Esto no impide por completo la detección de movimiento; Simplemente provoca que el detector deje de analizar fotogramas adicionales una vez que se supera el umbral. Durante estos eventos aún se realizan grabaciones basadas en el movimiento.",
+ "label": "Umbral de iluminación"
+ },
+ "threshold": {
+ "description": "Umbral de diferencia de píxeles utilizado por el detector de movimiento; los valores más altos reducen la sensibilidad (rango 1-255).",
+ "label": "Umbral de movimiento"
+ },
+ "label": "Detección de movimiento",
+ "description": "Ajustes predeterminados de detección de movimiento para esta cámara.",
+ "enabled": {
+ "label": "Habilitar detección de movimiento",
+ "description": "Habilita o deshabilita la detección de movimiento para esta cámara."
+ },
+ "improve_contrast": {
+ "label": "Mejorar contraste",
+ "description": "Aplica una mejora de contraste a los fotogramas antes del análisis de movimiento para ayudar a la detección."
+ },
+ "contour_area": {
+ "label": "Área de contorno",
+ "description": "Área mínima de contorno en píxeles necesaria para que se cuente un contorno de movimiento."
+ },
+ "delta_alpha": {
+ "label": "Delta alfa",
+ "description": "Factor de mezcla alfa usado en la diferencia entre fotogramas para calcular el movimiento."
+ },
+ "frame_alpha": {
+ "label": "Alfa del fotograma",
+ "description": "Valor alfa usado al mezclar fotogramas para el preprocesamiento de movimiento."
+ },
+ "frame_height": {
+ "label": "Altura del fotograma",
+ "description": "Altura en píxeles a la que escalar los fotogramas al calcular el movimiento."
+ },
+ "mask": {
+ "label": "Coordenadas de máscara",
+ "description": "Coordenadas x,y ordenadas que definen el polígono de máscara de movimiento usado para incluir/excluir áreas."
+ },
+ "mqtt_off_delay": {
+ "label": "Retraso de apagado MQTT",
+ "description": "Segundos a esperar tras el último movimiento antes de publicar un estado MQTT 'off'."
+ },
+ "enabled_in_config": {
+ "label": "Estado de movimiento original",
+ "description": "Indica si la detección de movimiento estaba habilitada en la configuración estática original."
+ },
+ "raw_mask": {
+ "label": "Máscara sin procesar"
+ }
+ },
+ "lpr": {
+ "enhancement": {
+ "description": "Nivel de mejora (0-10) que se aplicará a los recortes de matrículas antes del OCR; los valores más altos no siempre mejoran los resultados, y los niveles superiores a 5 podrían funcionar únicamente con matrículas capturadas de noche, por lo que deben utilizarse con precaución.",
+ "label": "Nivel de mejora"
+ },
+ "expire_time": {
+ "description": "Tiempo en segundos tras el cual una matrícula no detectada caduca en el sistema de seguimiento (solo para cámaras LPR dedicadas).",
+ "label": "Segundos hasta caducar"
+ },
+ "label": "Reconocimiento de matrículas",
+ "description": "Ajustes de reconocimiento de matrículas, incluidos umbrales de detección, formato y matrículas conocidas.",
+ "enabled": {
+ "label": "Habilitar LPR",
+ "description": "Habilita o deshabilita LPR en esta cámara."
+ },
+ "min_area": {
+ "label": "Área mínima de matrícula",
+ "description": "Área mínima de matrícula (píxeles) necesaria para intentar el reconocimiento."
+ }
+ },
+ "detect": {
+ "fps": {
+ "description": "Fotogramas por segundo deseados para ejecutar la detección; los valores más bajos reducen el uso de la CPU (el valor recomendado es 5; establezca un valor superior —como máximo de 10— únicamente si realiza el seguimiento de objetos que se mueven con extrema rapidez).",
+ "label": "FPS de detección"
+ },
+ "min_initialized": {
+ "description": "Número de detecciones consecutivas requeridas antes de crear un objeto rastreado. Auméntelo para reducir las inicializaciones falsas. El valor predeterminado es los FPS divididos por 2.",
+ "label": "Fotogramas mínimos de inicialización"
+ },
+ "height": {
+ "description": "Altura (en píxeles) de los fotogramas utilizados para la transmisión de detección; déjelo vacío para utilizar la resolución nativa de la transmisión.",
+ "label": "Altura de detección"
+ },
+ "width": {
+ "description": "Ancho (en píxeles) de los fotogramas utilizados para la transmisión de detección; déjelo vacío para utilizar la resolución nativa de la transmisión.",
+ "label": "Anchura de detección"
+ },
+ "stationary": {
+ "description": "Configuración para detectar y gestionar objetos que permanecen inmóviles durante un periodo de tiempo.",
+ "label": "Configuración de objetos estacionarios",
+ "interval": {
+ "label": "Intervalo estacionario",
+ "description": "Frecuencia (en fotogramas) con la que se ejecuta una comprobación de detección para confirmar un objeto estacionario."
+ },
+ "threshold": {
+ "label": "Umbral estacionario",
+ "description": "Número de fotogramas sin cambio de posición necesarios para marcar un objeto como estacionario."
+ },
+ "max_frames": {
+ "label": "Fotogramas máximos",
+ "description": "Limita durante cuánto tiempo se rastrean los objetos estacionarios antes de descartarlos.",
+ "default": {
+ "label": "Fotogramas máximos predeterminados",
+ "description": "Número máximo predeterminado de fotogramas para rastrear un objeto estacionario antes de detenerse."
+ },
+ "objects": {
+ "label": "Fotogramas máximos por objeto",
+ "description": "Sobrescrituras por objeto para el número máximo de fotogramas en los que rastrear objetos estacionarios."
+ }
+ },
+ "classifier": {
+ "label": "Habilitar clasificador visual",
+ "description": "Usa un clasificador visual para detectar objetos realmente estacionarios incluso cuando los cuadros delimitadores oscilan."
+ }
+ },
+ "label": "Detección de objetos",
+ "description": "Ajustes del rol de detección/detect usado para ejecutar la detección de objetos e inicializar los rastreadores.",
+ "enabled": {
+ "label": "Habilitar detección de objetos",
+ "description": "Habilita o deshabilita la detección de objetos para esta cámara."
+ },
+ "max_disappeared": {
+ "label": "Fotogramas máximos desaparecido",
+ "description": "Número de fotogramas sin detección antes de que un objeto rastreado se considere desaparecido."
+ },
+ "annotation_offset": {
+ "label": "Desplazamiento de anotaciones",
+ "description": "Milisegundos para desplazar las anotaciones de detección y alinear mejor los cuadros delimitadores de la línea de tiempo con las grabaciones; puede ser positivo o negativo."
+ }
+ },
+ "record": {
+ "motion": {
+ "description": "Número de días para conservar las grabaciones activadas por movimiento, independientemente de los objetos rastreados. Establézcalo en 0 si solo desea conservar las grabaciones de alertas y detecciones.",
+ "label": "Retención de movimiento",
+ "days": {
+ "label": "Días de retención",
+ "description": "Días durante los que conservar las grabaciones."
+ }
+ },
+ "continuous": {
+ "description": "Número de días para conservar las grabaciones, independientemente de los objetos rastreados o del movimiento. Establézcalo en 0 si solo desea conservar las grabaciones de alertas y detecciones.",
+ "label": "Retención continua",
+ "days": {
+ "label": "Días de retención",
+ "description": "Días durante los que conservar las grabaciones."
+ }
+ },
+ "detections": {
+ "pre_capture": {
+ "description": "Número de segundos antes del evento de detección que se incluirán en la grabación.",
+ "label": "Segundos de captura previa"
+ },
+ "post_capture": {
+ "description": "Número de segundos después del evento de detección que se incluirán en la grabación.",
+ "label": "Segundos de captura posterior"
+ },
+ "label": "Retención de detección",
+ "description": "Ajustes de retención de grabaciones para eventos de detección, incluidas las duraciones de captura previa/posterior.",
+ "retain": {
+ "label": "Retención de eventos",
+ "description": "Ajustes de retención para grabaciones de eventos de detección.",
+ "days": {
+ "label": "Días de retención",
+ "description": "Número de días durante los que conservar grabaciones de eventos de detección."
+ },
+ "mode": {
+ "label": "Modo de retención",
+ "description": "Modo de retención: all (guarda todos los segmentos), motion (guarda segmentos con movimiento) o active_objects (guarda segmentos con objetos activos)."
+ }
+ }
+ },
+ "alerts": {
+ "pre_capture": {
+ "description": "Número de segundos antes del evento de detección que se incluirán en la grabación.",
+ "label": "Segundos de captura previa"
+ },
+ "post_capture": {
+ "description": "Número de segundos después del evento de detección que se incluirán en la grabación.",
+ "label": "Segundos de captura posterior"
+ },
+ "label": "Retención de alertas",
+ "description": "Ajustes de retención de grabaciones para eventos de alerta, incluidas las duraciones de captura previa/posterior.",
+ "retain": {
+ "label": "Retención de eventos",
+ "description": "Ajustes de retención para grabaciones de eventos de detección.",
+ "days": {
+ "label": "Días de retención",
+ "description": "Número de días durante los que conservar grabaciones de eventos de detección."
+ },
+ "mode": {
+ "label": "Modo de retención",
+ "description": "Modo de retención: all (guarda todos los segmentos), motion (guarda segmentos con movimiento) o active_objects (guarda segmentos con objetos activos)."
+ }
+ }
+ },
+ "label": "Grabación",
+ "description": "Ajustes de grabación y retención para esta cámara.",
+ "enabled": {
+ "label": "Habilitar grabación",
+ "description": "Habilita o deshabilita la grabación para esta cámara."
+ },
+ "expire_interval": {
+ "label": "Intervalo de limpieza de grabaciones",
+ "description": "Minutos entre pasadas de limpieza que eliminan segmentos de grabación caducados."
+ },
+ "export": {
+ "label": "Configuración de exportación",
+ "description": "Ajustes usados al exportar grabaciones, como timelapse y aceleración por hardware.",
+ "hwaccel_args": {
+ "label": "Argumentos hwaccel de exportación",
+ "description": "Argumentos de aceleración por hardware que se usarán en operaciones de exportación/transcodificación."
+ },
+ "max_concurrent": {
+ "label": "Exportaciones simultáneas máximas",
+ "description": "Número máximo de trabajos de exportación que se procesarán al mismo tiempo."
+ }
+ },
+ "preview": {
+ "label": "Configuración de vista previa",
+ "description": "Ajustes que controlan la calidad de las vistas previas de grabaciones mostradas en la interfaz.",
+ "quality": {
+ "label": "Calidad de vista previa",
+ "description": "Nivel de calidad de vista previa (very_low, low, medium, high, very_high)."
+ }
+ },
+ "enabled_in_config": {
+ "label": "Estado de grabación original",
+ "description": "Indica si la grabación estaba habilitada en la configuración estática original."
+ }
+ },
+ "ui": {
+ "dashboard": {
+ "description": "Alterna si esta cámara es visible en toda la interfaz de usuario de Frigate. Desactivar esta opción requerirá editar manualmente la configuración para volver a visualizar esta cámara en la interfaz.",
+ "label": "Mostrar en la interfaz"
+ },
+ "label": "Interfaz de cámara",
+ "description": "Orden de visualización y visibilidad de esta cámara en la interfaz. El orden afecta al panel predeterminado. Para un control más granular, usa grupos de cámaras.",
+ "order": {
+ "label": "Orden en la interfaz",
+ "description": "Orden numérico usado para ordenar la cámara en la interfaz (panel predeterminado y listas); los números más altos aparecen más tarde."
+ },
+ "review": {
+ "label": "Mostrar en Revisión",
+ "description": "Activa o desactiva si esta cámara es visible en Revisión (la página de Revisión y su filtro de cámaras, la revisión de movimiento y la vista de historial)."
+ }
+ },
+ "live": {
+ "height": {
+ "description": "Altura (en píxeles) para renderizar la transmisión en vivo de jsmpeg en la interfaz web; debe ser <= a la altura de la transmisión de detección.",
+ "label": "Altura en directo"
+ },
+ "description": "Configuraciones utilizadas por la interfaz web para controlar la selección, la resolución y la calidad de transmisiónes en vivo.",
+ "label": "Reproducción en directo",
+ "streams": {
+ "label": "Nombres de flujos en directo",
+ "description": "Asignación de nombres de flujos configurados a nombres de restream/go2rtc usados para la reproducción en directo."
+ },
+ "quality": {
+ "label": "Calidad en directo",
+ "description": "Calidad de codificación para el flujo jsmpeg (1 la más alta, 31 la más baja)."
+ }
+ },
+ "review": {
+ "description": "Configuraciones que controlan las alertas, las detecciones y los resúmenes de revisión de GenAI utilizados por la interfaz de usuario y el almacenamiento de esta cámara.",
+ "alerts": {
+ "required_zones": {
+ "description": "Zonas en las que debe entrar un objeto para ser considerado una alerta; dejar vacío para permitir cualquier zona.",
+ "label": "Zonas requeridas"
+ },
+ "labels": {
+ "description": "Lista de etiquetas de objetos que califican como alertas (por ejemplo: car, person).",
+ "label": "Etiquetas de alerta"
+ },
+ "label": "Configuración de alertas",
+ "description": "Ajustes sobre qué objetos rastreados generan alertas y cómo se conservan las alertas.",
+ "enabled": {
+ "label": "Habilitar alertas",
+ "description": "Habilita o deshabilita la generación de alertas para esta cámara."
+ },
+ "enabled_in_config": {
+ "label": "Estado original de alertas",
+ "description": "Rastrea si las alertas estaban habilitadas originalmente en la configuración estática."
+ },
+ "cutoff_time": {
+ "label": "Tiempo de corte de alertas",
+ "description": "Segundos que se esperarán tras dejar de haber actividad causante de alerta antes de cortar una alerta."
+ }
+ },
+ "detections": {
+ "required_zones": {
+ "description": "Zonas en las que debe entrar un objeto para ser considerado detectado; dejar vacío para permitir cualquier zona.",
+ "label": "Zonas requeridas"
+ },
+ "description": "Configuración para determinar qué objetos rastreados generan detecciones (no alertas) y cómo se retienen dichas detecciones.",
+ "label": "Configuración de detecciones",
+ "enabled": {
+ "label": "Habilitar detecciones",
+ "description": "Habilita o deshabilita los eventos de detección para esta cámara."
+ },
+ "labels": {
+ "label": "Etiquetas de detección",
+ "description": "Lista de etiquetas de objetos que cuentan como eventos de detección."
+ },
+ "cutoff_time": {
+ "label": "Tiempo de corte de detecciones",
+ "description": "Segundos que se esperarán tras dejar de haber actividad causante de detección antes de cortar una detección."
+ },
+ "enabled_in_config": {
+ "label": "Estado original de detecciones",
+ "description": "Rastrea si las detecciones estaban habilitadas originalmente en la configuración estática."
+ }
+ },
+ "genai": {
+ "image_source": {
+ "description": "Fuente de las imágenes enviadas a GenAI ('preview' o 'recordings'); La opción 'recordings' utiliza fotogramas de mayor calidad, pero requiere más tokens.",
+ "label": "Origen de imagen de revisión"
+ },
+ "additional_concerns": {
+ "description": "Una lista de preocupaciones o notas adicionales que GenAI debería tener en cuenta al evaluar la actividad en esta cámara.",
+ "label": "Consideraciones adicionales"
+ },
+ "activity_context_prompt": {
+ "description": "Instrucción personalizada que describe qué constituye y qué no una actividad sospechosa, con el fin de proporcionar contexto para los resúmenes generados por GenAI.",
+ "label": "Prompt de contexto de actividad"
+ },
+ "description": "Controla el uso de IA generativa (GenAI) para la elaboración de descripciones y resúmenes de elementos de revisión.",
+ "debug_save_thumbnails": {
+ "description": "Guarde las miniaturas que se envían al proveedor de GenAI para su depuración y revisión.",
+ "label": "Guardar miniaturas"
+ },
+ "label": "Configuración de GenAI",
+ "enabled": {
+ "label": "Habilitar descripciones de GenAI",
+ "description": "Habilita o deshabilita las descripciones y resúmenes generados por GenAI para los elementos de revisión."
+ },
+ "alerts": {
+ "label": "Habilitar GenAI para alertas",
+ "description": "Usa GenAI para generar descripciones de elementos de alerta."
+ },
+ "detections": {
+ "label": "Habilitar GenAI para detecciones",
+ "description": "Usa GenAI para generar descripciones de elementos de detección."
+ },
+ "enabled_in_config": {
+ "label": "Estado original de GenAI",
+ "description": "Rastrea si la revisión de GenAI estaba habilitada originalmente en la configuración estática."
+ },
+ "preferred_language": {
+ "label": "Idioma preferido",
+ "description": "Idioma preferido que se solicitará al proveedor de GenAI para las respuestas generadas."
+ }
+ },
+ "label": "Revisión"
+ },
+ "birdseye": {
+ "description": "Configuración para la vista compuesta Birdseye, que combina las transmisiones de múltiples cámaras en una sola vista.",
+ "label": "Birdseye",
+ "enabled": {
+ "label": "Habilitar Birdseye",
+ "description": "Habilita o deshabilita la función de vista Birdseye."
+ },
+ "mode": {
+ "label": "Modo de seguimiento",
+ "description": "Modo para incluir cámaras en Birdseye: 'objects', 'motion' o 'continuous'."
+ },
+ "order": {
+ "label": "Posición",
+ "description": "Posición numérica que controla el orden de la cámara en el diseño de Birdseye."
+ }
+ },
+ "ffmpeg": {
+ "retry_interval": {
+ "description": "Segundos de espera antes de intentar reconectar la transmisión de una cámara tras un fallo. El valor predeterminado es 10.",
+ "label": "Tiempo de reintento de FFmpeg"
+ },
+ "path": {
+ "description": "Ruta al binario de FFmpeg que se va a utilizar o un alias de versión (\"7.0\" o \"8.0\").",
+ "label": "Ruta de FFmpeg"
+ },
+ "output_args": {
+ "description": "Argumentos de salida predeterminados utilizados para diferentes roles de FFmpeg, tales como detección y grabación.",
+ "label": "Argumentos de salida",
+ "detect": {
+ "label": "Argumentos de salida de detección",
+ "description": "Argumentos de salida predeterminados para los flujos con rol de detección."
+ },
+ "record": {
+ "label": "Argumentos de salida de grabación",
+ "description": "Argumentos de salida predeterminados para los flujos con rol de grabación."
+ }
+ },
+ "description": "Entradas de flujo de la cámara y opciones de FFmpeg, incluida la ruta del binario, argumentos, aceleración por hardware y argumentos de salida por rol.",
+ "label": "Flujos (FFmpeg)",
+ "global_args": {
+ "label": "Argumentos globales de FFmpeg",
+ "description": "Argumentos globales pasados a los procesos de FFmpeg."
+ },
+ "hwaccel_args": {
+ "label": "Argumentos de aceleración por hardware",
+ "description": "Argumentos de aceleración por hardware para FFmpeg. Se recomiendan preajustes específicos del proveedor."
+ },
+ "input_args": {
+ "label": "Argumentos de entrada",
+ "description": "Argumentos de entrada aplicados a los flujos de entrada de FFmpeg."
+ },
+ "apple_compatibility": {
+ "label": "Compatibilidad con Apple",
+ "description": "Habilita el etiquetado HEVC para mejorar la compatibilidad con reproductores de Apple al grabar H.265."
+ },
+ "gpu": {
+ "label": "Índice de GPU",
+ "description": "Índice de GPU predeterminado usado para la aceleración por hardware si está disponible."
+ },
+ "inputs": {
+ "label": "Entradas de cámara",
+ "description": "Lista de definiciones de flujos de entrada (rutas y roles) para esta cámara.",
+ "path": {
+ "label": "Ruta de entrada",
+ "description": "URL o ruta del flujo de entrada de la cámara."
+ },
+ "roles": {
+ "label": "Roles de entrada",
+ "description": "Roles para este flujo de entrada."
+ },
+ "global_args": {
+ "label": "Argumentos globales de FFmpeg",
+ "description": "Argumentos globales de FFmpeg para este flujo de entrada."
+ },
+ "hwaccel_args": {
+ "label": "Argumentos de aceleración por hardware",
+ "description": "Argumentos de aceleración por hardware para este flujo de entrada."
+ },
+ "input_args": {
+ "label": "Argumentos de entrada",
+ "description": "Argumentos de entrada específicos para este flujo."
}
}
+ },
+ "face_recognition": {
+ "label": "Reconocimiento facial",
+ "description": "Ajustes de detección y reconocimiento facial para esta cámara.",
+ "enabled": {
+ "label": "Habilitar reconocimiento facial",
+ "description": "Habilita o deshabilita el reconocimiento facial."
+ },
+ "min_area": {
+ "label": "Área mínima de rostro",
+ "description": "Área mínima (píxeles) del cuadro de un rostro detectado necesaria para intentar el reconocimiento."
+ }
+ },
+ "semantic_search": {
+ "label": "Búsqueda semántica",
+ "description": "Ajustes de búsqueda semántica, que crea y consulta embeddings de objetos para encontrar elementos similares.",
+ "triggers": {
+ "label": "Activadores",
+ "description": "Acciones y criterios de coincidencia para activadores de búsqueda semántica específicos de la cámara.",
+ "friendly_name": {
+ "label": "Nombre descriptivo",
+ "description": "Nombre descriptivo opcional mostrado en la interfaz para este activador."
+ },
+ "enabled": {
+ "label": "Habilitar este activador",
+ "description": "Habilita o deshabilita este activador de búsqueda semántica."
+ },
+ "type": {
+ "label": "Tipo de activador",
+ "description": "Tipo de activador: 'thumbnail' (coincidir con imagen) o 'description' (coincidir con texto)."
+ },
+ "data": {
+ "label": "Contenido del activador",
+ "description": "Frase de texto o ID de miniatura que se comparará con objetos rastreados."
+ },
+ "threshold": {
+ "label": "Umbral del activador",
+ "description": "Puntuación mínima de similitud (0-1) necesaria para activar este activador."
+ },
+ "actions": {
+ "label": "Acciones del activador",
+ "description": "Lista de acciones que se ejecutarán cuando el activador coincida (notification, sub_label, attribute)."
+ }
+ }
+ },
+ "snapshots": {
+ "label": "Instantáneas",
+ "description": "Ajustes de instantáneas generadas por la API de objetos rastreados para esta cámara.",
+ "enabled": {
+ "label": "Habilitar instantáneas",
+ "description": "Habilita o deshabilita el guardado de instantáneas para esta cámara."
+ },
+ "timestamp": {
+ "label": "Superposición de marca de tiempo",
+ "description": "Superpone una marca de tiempo en las instantáneas de la API."
+ },
+ "bounding_box": {
+ "label": "Superposición de cuadro delimitador",
+ "description": "Dibuja cuadros delimitadores para los objetos rastreados en las instantáneas de la API."
+ },
+ "crop": {
+ "label": "Recortar instantánea",
+ "description": "Recorta las instantáneas de la API al cuadro delimitador del objeto detectado."
+ },
+ "required_zones": {
+ "label": "Zonas requeridas",
+ "description": "Zonas en las que debe entrar un objeto para que se guarde una instantánea."
+ },
+ "height": {
+ "label": "Altura de instantánea",
+ "description": "Altura (píxeles) a la que redimensionar las instantáneas de la API; déjalo vacío para conservar el tamaño original."
+ },
+ "retain": {
+ "label": "Retención de instantáneas",
+ "description": "Ajustes de retención de instantáneas, incluidos días predeterminados y sobrescrituras por objeto.",
+ "default": {
+ "label": "Retención predeterminada",
+ "description": "Número predeterminado de días durante los que conservar instantáneas."
+ },
+ "mode": {
+ "label": "Modo de retención",
+ "description": "Modo de retención: all (guarda todos los segmentos), motion (guarda segmentos con movimiento) o active_objects (guarda segmentos con objetos activos)."
+ },
+ "objects": {
+ "label": "Retención por objeto",
+ "description": "Sobrescrituras por objeto para los días de retención de instantáneas."
+ }
+ },
+ "quality": {
+ "label": "Calidad de instantánea",
+ "description": "Calidad de codificación de las instantáneas guardadas (0-100)."
+ }
+ },
+ "timestamp_style": {
+ "label": "Estilo de marca de tiempo",
+ "description": "Opciones de estilo para las marcas de tiempo aplicadas a las instantáneas y a la vista de depuración.",
+ "position": {
+ "label": "Posición de marca de tiempo",
+ "description": "Posición de la marca de tiempo en la imagen (tl/tr/bl/br)."
+ },
+ "format": {
+ "label": "Formato de marca de tiempo",
+ "description": "Cadena de formato de fecha y hora usada para las marcas de tiempo (códigos de formato datetime de Python)."
+ },
+ "color": {
+ "label": "Color de marca de tiempo",
+ "description": "Valores de color RGB para el texto de la marca de tiempo (todos los valores 0-255).",
+ "red": {
+ "label": "Rojo",
+ "description": "Componente rojo (0-255) para el color de la marca de tiempo."
+ },
+ "green": {
+ "label": "Verde",
+ "description": "Componente verde (0-255) para el color de la marca de tiempo."
+ },
+ "blue": {
+ "label": "Azul",
+ "description": "Componente azul (0-255) para el color de la marca de tiempo."
+ }
+ },
+ "thickness": {
+ "label": "Grosor de marca de tiempo",
+ "description": "Grosor de línea del texto de la marca de tiempo."
+ },
+ "effect": {
+ "label": "Efecto de marca de tiempo",
+ "description": "Efecto visual para el texto de la marca de tiempo (none, solid, shadow)."
+ }
+ },
+ "best_image_timeout": {
+ "label": "Tiempo de espera de mejor imagen",
+ "description": "Tiempo que se esperará la imagen con la puntuación de confianza más alta."
+ },
+ "type": {
+ "label": "Tipo de cámara",
+ "description": "Tipo de cámara"
+ },
+ "webui_url": {
+ "label": "URL de la cámara",
+ "description": "URL para visitar la cámara directamente desde la página del sistema"
+ },
+ "profiles": {
+ "label": "Perfiles",
+ "description": "Perfiles de configuración con nombre y sobrescrituras parciales que pueden activarse en tiempo de ejecución."
+ },
+ "enabled_in_config": {
+ "label": "Estado original de cámara",
+ "description": "Mantiene el registro del estado original de la cámara."
}
}
diff --git a/web/public/locales/es/config/global.json b/web/public/locales/es/config/global.json
index 53cdd0aa6e..7869ce4ba2 100644
--- a/web/public/locales/es/config/global.json
+++ b/web/public/locales/es/config/global.json
@@ -24,9 +24,10 @@
}
},
"audio": {
- "label": "Eventos de audio",
+ "label": "Detección de audio",
"enabled": {
- "label": "Habilitar la detección de audio"
+ "label": "Habilitar la detección de audio",
+ "description": "Habilita o deshabilita la detección de eventos de audio para todas las cámaras; se puede sobrescribir por cámara."
},
"max_not_heard": {
"label": "Finalizar el tiempo de espera",
@@ -38,19 +39,25 @@
},
"listen": {
"label": "Tipos de escucha",
- "description": "Lista de tipos de eventos de audio a detectar (por ejemplo: ladrido, alarma de incendios, grito, voz, alarido)."
+ "description": "Lista de tipos de eventos de audio a detectar (por ejemplo: ladrar, alarma de incendio, habla, gritar)."
},
"filters": {
"label": "Filtros de audio",
- "description": "Ajustes de filtrado por tipo de audio, como umbrales de confianza utilizados para reducir los falsos positivos."
+ "description": "Ajustes de filtro por tipo de audio, como umbrales de confianza utilizados para reducir los falsos positivos.",
+ "threshold": {
+ "label": "Confianza mínima de audio",
+ "description": "Umbral mínimo de confianza para que se cuente el evento de audio."
+ }
},
"enabled_in_config": {
- "description": "Indica si la detección de audio estaba habilitada originalmente en el archivo de configuración estática.",
- "label": "Estado original del audio"
+ "description": "Indica si la detección de audio estaba habilitada originalmente en el archivo de configuración estático.",
+ "label": "Estado de audio original"
},
"num_threads": {
- "label": "Hilos de detección"
- }
+ "label": "Hilos de detección",
+ "description": "Número de hilos a usar para procesamiento de detección de audio."
+ },
+ "description": "Ajustes para la detección de eventos basada en audio en todas las cámaras; se pueden sobrescribir por cámara."
},
"auth": {
"label": "Autenticación",
@@ -70,11 +77,110 @@
"cookie_secure": {
"label": "Flag de cookie segura",
"description": "Establece el flag de seguridad en la cookie de autenticación; debe ser 'true' cuando se utilice TLS."
+ },
+ "failed_login_rate_limit": {
+ "label": "Limite de intento de acceso fallidos",
+ "description": "Reglas de limitación de intentos de inicio de sesión fallidos para reducir los ataques de fuerza bruta."
+ },
+ "session_length": {
+ "description": "Duración de la sesión en segundos para sesiones de JWT.",
+ "label": "Duración de la sesión"
+ },
+ "admin_first_time_login": {
+ "description": "Cuando se establece en true, la interfaz de usuario puede mostrar un enlace de ayuda en la página de inicio de sesión, informando a los usuarios sobre cómo iniciar sesión tras el restablecimiento de la contraseña de administrador. ",
+ "label": "Marca de administrador inicial"
+ },
+ "refresh_time": {
+ "description": "Cuando a una sesión le queden menos de esta cantidad de segundos para expirar, actualícela para restablecer su duración completa.",
+ "label": "Ventana de actualización de la sesión"
+ },
+ "trusted_proxies": {
+ "label": "Proxies de confianza",
+ "description": "Lista de IPs de proxies de confianza utilizadas para determinar la IP del cliente en la limitación de peticiones."
+ },
+ "hash_iterations": {
+ "label": "Iteraciones de hash",
+ "description": "Número de iteraciones PBKDF2-SHA256 que se utilizarán al generar el hash de las contraseñas de los usuarios."
+ },
+ "roles": {
+ "label": "Asignaciones de roles",
+ "description": "Asigna roles a listas de cámaras. Una lista vacía concede acceso a todas las cámaras para ese rol."
}
},
"onvif": {
"profile": {
- "label": "Perfil ONVIF"
+ "label": "Perfil ONVIF",
+ "description": "Perfil multimedia ONVIF específico que se utilizará para el control PTZ, identificado por token o nombre. Si no se especifica, se selecciona automáticamente el primer perfil con una configuración PTZ válida."
+ },
+ "autotracking": {
+ "zoom_factor": {
+ "description": "Controla el nivel de zoom en los objetos rastreados. Los valores más bajos mantienen una mayor parte de la escena a la vista; los valores más altos acercan la imagen, pero pueden provocar la pérdida del rastreo. Valores entre 0.1 y 0.75.",
+ "label": "Factor de zoom"
+ },
+ "calibrate_on_startup": {
+ "description": "Mida la velocidad de los motores PTZ al encenderlos para mejorar la precisión del seguimiento. Frigate actualizará la configuración con los `movement_weights` tras la calibración.",
+ "label": "Calibrar al iniciar"
+ },
+ "description": "Realice un seguimiento automático de objetos en movimiento y manténgalos centrados en el encuadre mediante movimientos de cámara PTZ.",
+ "zooming": {
+ "description": "Control del comportamiento del zoom: deshabilitado (solo panorámica/inclinación), absoluto (mayor compatibilidad) o relativo (panorámica/inclinación/zoom simultáneos).",
+ "label": "Modo de zoom"
+ },
+ "return_preset": {
+ "description": "Nombre del preajuste ONVIF configurado en el firmware de la cámara al que regresar una vez finalizado el seguimiento.",
+ "label": "Preajuste de retorno"
+ },
+ "timeout": {
+ "description": "Espere esta cantidad de segundos después de perder el seguimiento antes de devolver la cámara a la posición preestablecida.",
+ "label": "Tiempo de espera de retorno"
+ },
+ "label": "Seguimiento automático",
+ "enabled": {
+ "label": "Habilitar seguimiento automático",
+ "description": "Habilita o deshabilita el seguimiento automático con cámara PTZ de objetos detectados."
+ },
+ "track": {
+ "label": "Objetos rastreados",
+ "description": "Lista de tipos de objetos que deben activar el seguimiento automático."
+ },
+ "required_zones": {
+ "label": "Zonas requeridas",
+ "description": "Los objetos deben entrar en una de estas zonas antes de que comience el seguimiento automático."
+ },
+ "movement_weights": {
+ "label": "Pesos de movimiento",
+ "description": "Valores de calibración generados automáticamente por la calibración de la cámara. No los modifiques manualmente."
+ },
+ "enabled_in_config": {
+ "label": "Estado original de autoseguimiento",
+ "description": "Campo interno para rastrear si el seguimiento automático estaba habilitado en la configuración."
+ }
+ },
+ "tls_insecure": {
+ "description": "Omitir la verificación TLS y deshabilitar la autenticación digest para ONVIF (no seguro; usar solo en redes seguras).",
+ "label": "Deshabilitar verificación TLS"
+ },
+ "label": "ONVIF",
+ "description": "Ajustes de conexión ONVIF y seguimiento automático PTZ para esta cámara.",
+ "host": {
+ "label": "Host ONVIF",
+ "description": "Host (y esquema opcional) para el servicio ONVIF de esta cámara."
+ },
+ "port": {
+ "label": "Puerto ONVIF",
+ "description": "Número de puerto del servicio ONVIF."
+ },
+ "user": {
+ "label": "Nombre de usuario ONVIF",
+ "description": "Nombre de usuario para la autenticación ONVIF; algunos dispositivos requieren un usuario administrador para ONVIF."
+ },
+ "password": {
+ "label": "Contraseña ONVIF",
+ "description": "Contraseña para la autenticación ONVIF."
+ },
+ "ignore_time_mismatch": {
+ "label": "Ignorar discrepancia horaria",
+ "description": "Ignora las diferencias de sincronización horaria entre la cámara y el servidor Frigate para la comunicación ONVIF."
}
},
"objects": {
@@ -91,6 +197,122 @@
"use_snapshot": {
"label": "Usar instantáneas",
"description": "Usar instantáneas de objetos en lugar de miniaturas para la generación de descripciones de GenAI."
+ },
+ "send_triggers": {
+ "after_significant_updates": {
+ "description": "Envía una solicitud a GenAI tras un número especificado de actualizaciones significativas del objeto rastreado.",
+ "label": "Activador temprano de GenAI"
+ },
+ "description": "Define cuándo se deben enviar los fotogramas a GenAI (al finalizar, después de las actualizaciones, etc.).",
+ "label": "Activadores de GenAI",
+ "tracked_object_end": {
+ "label": "Enviar al finalizar",
+ "description": "Envía una solicitud a GenAI cuando finaliza el objeto rastreado."
+ }
+ },
+ "required_zones": {
+ "description": "Zonas en las que deben ubicarse los objetos para ser elegibles para la generación de descripciones con GenAI.",
+ "label": "Zonas requeridas"
+ },
+ "prompt": {
+ "label": "Prompt de descripción",
+ "description": "Plantilla de prompt predeterminada usada al generar descripciones con GenAI."
+ },
+ "object_prompts": {
+ "label": "Prompts de objetos",
+ "description": "Prompts por objeto para personalizar las salidas de GenAI para etiquetas concretas."
+ },
+ "objects": {
+ "label": "Objetos de GenAI",
+ "description": "Lista de etiquetas de objetos que se enviarán a GenAI de forma predeterminada."
+ },
+ "debug_save_thumbnails": {
+ "label": "Guardar miniaturas",
+ "description": "Guarda las miniaturas enviadas a GenAI para depuración y revisión."
+ },
+ "enabled_in_config": {
+ "label": "Estado original de GenAI",
+ "description": "Indica si GenAI estaba habilitado en la configuración estática original."
+ }
+ },
+ "track": {
+ "description": "Lista de etiquetas de objetos a rastrear para todas las cámaras; puede anularse por cámara.",
+ "label": "Objetos a rastrear"
+ },
+ "label": "Objetos",
+ "description": "Valores predeterminados de seguimiento de objetos, incluidas las etiquetas que se rastrean y los filtros por objeto.",
+ "filters": {
+ "label": "Filtros de objetos",
+ "description": "Filtros aplicados a los objetos detectados para reducir falsos positivos (área, relación, confianza).",
+ "min_area": {
+ "label": "Área mínima de objeto",
+ "description": "Área mínima del cuadro delimitador (píxeles o porcentaje) necesaria para este tipo de objeto. Puede ser píxeles (int) o porcentaje (float entre 0.000001 y 0.99)."
+ },
+ "max_area": {
+ "label": "Área máxima de objeto",
+ "description": "Área máxima del cuadro delimitador (píxeles o porcentaje) permitida para este tipo de objeto. Puede ser píxeles (int) o porcentaje (float entre 0.000001 y 0.99)."
+ },
+ "min_ratio": {
+ "label": "Relación de aspecto mínima",
+ "description": "Relación mínima anchura/altura necesaria para que el cuadro delimitador sea válido."
+ },
+ "max_ratio": {
+ "label": "Relación de aspecto máxima",
+ "description": "Relación máxima anchura/altura permitida para que el cuadro delimitador sea válido."
+ },
+ "threshold": {
+ "label": "Umbral de confianza",
+ "description": "Umbral medio de confianza de detección necesario para que el objeto se considere un positivo verdadero."
+ },
+ "min_score": {
+ "label": "Confianza mínima",
+ "description": "Confianza mínima de detección en un único fotograma necesaria para que el objeto se contabilice."
+ },
+ "mask": {
+ "label": "Máscara de filtro",
+ "description": "Coordenadas del polígono que definen dónde se aplica este filtro dentro del fotograma."
+ },
+ "raw_mask": {
+ "label": "Máscara sin procesar"
+ }
+ },
+ "mask": {
+ "label": "Máscara de objeto",
+ "description": "Polígono de máscara usado para evitar la detección de objetos en áreas especificadas."
+ },
+ "filters_attribute": {
+ "label": "Filtros de atributos",
+ "description": "Filtros aplicados a los atributos detectados para reducir falsos positivos (área, proporción y confianza).",
+ "min_area": {
+ "label": "Área mínima del atributo",
+ "description": "Área mínima del cuadro delimitador (en píxeles o porcentaje) necesaria para este atributo. Puede expresarse en píxeles (entero) o como porcentaje (valor decimal entre 0.000001 y 0.99)."
+ },
+ "max_area": {
+ "label": "Área máxima del atributo",
+ "description": "Área máxima del cuadro delimitador (en píxeles o porcentaje) permitida para este atributo. Puede expresarse en píxeles (entero) o como porcentaje (valor decimal entre 0.000001 y 0.99)."
+ },
+ "min_ratio": {
+ "label": "Relación de aspecto mínima",
+ "description": "Relación mínima entre anchura y altura necesaria para que el cuadro delimitador se considere válido."
+ },
+ "max_ratio": {
+ "label": "Relación de aspecto máxima",
+ "description": "Relación máxima entre anchura y altura permitida para que el cuadro delimitador se considere válido."
+ },
+ "threshold": {
+ "label": "Umbral de confianza",
+ "description": "Umbral medio de confianza de detección necesario para que el atributo se considere un verdadero positivo."
+ },
+ "min_score": {
+ "label": "Confianza mínima",
+ "description": "Confianza mínima de detección en un único fotograma necesaria para asociar este atributo con su objeto principal."
+ },
+ "mask": {
+ "label": "Máscara de filtro",
+ "description": "Coordenadas del polígono que definen dónde se aplica este filtro dentro del fotograma."
+ },
+ "raw_mask": {
+ "label": "Máscara sin procesar"
}
}
},
@@ -98,15 +320,1320 @@
"deepstack": {
"description": "Detector DeepStack/CodeProject.AI que envía imágenes a una API HTTP remota de DeepStack para la inferencia. No recomendado.",
"api_url": {
- "description": "La URL de la API de DeepStack."
+ "description": "La URL de la API de DeepStack.",
+ "label": "URL de la API de DeepStack"
},
"api_timeout": {
"label": "Tiempo de espera de la API de DeepStack (en segundos)",
"description": "Tiempo máximo permitido para una solicitud a la API de DeepStack."
},
"api_key": {
- "label": "Clave de API de DeepStack (si es necesaria)"
+ "label": "Clave de API de DeepStack (si es necesaria)",
+ "description": "Clave API opcional para servicios autenticados de DeepStack."
+ },
+ "label": "DeepStack"
+ },
+ "type": {
+ "label": "Tipo"
+ },
+ "label": "Detector de hardware",
+ "cpu": {
+ "label": "CPU",
+ "num_threads": {
+ "label": "Número de hilos para detección",
+ "description": "Número de hilos usados para inferencia basada en CPU."
+ },
+ "description": "Detector TFLite de CPU que ejecuta modelos de TensorFlow Lite en la CPU del host sin aceleración por hardware. No recomendado."
+ },
+ "axengine": {
+ "label": "Motor AX NPU",
+ "description": "Detector NPU AXERA AX650N/AX8850N que ejecuta archivos .axmodel compilados mediante el runtime AXEngine."
+ },
+ "teflon_tfl": {
+ "description": "Detector de delegados Teflon para TFLite, que utiliza la biblioteca de delegados Mesa Teflon para acelerar la inferencia en las GPU compatibles.",
+ "label": "Teflon"
+ },
+ "synaptics": {
+ "description": "Detector NPU de Synaptics para modelos en formato .synap, utilizando el Synap SDK en hardware de Synaptics.",
+ "label": "Synaptics"
+ },
+ "zmq": {
+ "description": "Detector ZMQ IPC que descarga la inferencia a un proceso externo a través de un punto de conexión IPC de ZeroMQ.",
+ "label": "IPC de ZMQ",
+ "endpoint": {
+ "label": "Endpoint IPC de ZMQ",
+ "description": "Endpoint ZMQ al que conectarse."
+ },
+ "request_timeout_ms": {
+ "label": "Tiempo de espera de solicitud ZMQ en milisegundos",
+ "description": "Tiempo de espera para solicitudes ZMQ en milisegundos."
+ },
+ "linger_ms": {
+ "label": "Persistencia del socket ZMQ en milisegundos",
+ "description": "Periodo de persistencia del socket en milisegundos."
+ }
+ },
+ "hailo8l": {
+ "description": "Detector Hailo-8/Hailo-8L que utiliza modelos HEF y el SDK HailoRT para la inferencia en hardware Hailo.",
+ "label": "Hailo-8/Hailo-8L",
+ "device": {
+ "label": "Tipo de dispositivo",
+ "description": "Dispositivo que se usará para la inferencia Hailo (p. ej., 'PCIe', 'M.2')."
+ }
+ },
+ "onnx": {
+ "description": "Detector ONNX para ejecutar modelos ONNX; utilizará los backends de aceleración disponibles (CUDA/ROCm/OpenVINO) cuando estén disponibles.",
+ "label": "ONNX",
+ "device": {
+ "label": "Tipo de dispositivo",
+ "description": "Dispositivo que se usará para la inferencia ONNX (p. ej., 'AUTO', 'CPU', 'GPU')."
+ }
+ },
+ "description": "Configuración para detectores de objetos (backends de CPU, GPU y ONNX) y cualquier ajuste del modelo específico del detector.",
+ "openvino": {
+ "description": "Detector OpenVINO para CPU AMD e Intel, GPU Intel y hardware VPU Intel.",
+ "label": "OpenVINO",
+ "device": {
+ "label": "Tipo de dispositivo",
+ "description": "Dispositivo que se usará para la inferencia OpenVINO (p. ej., 'CPU', 'GPU', 'NPU')."
+ }
+ },
+ "tensorrt": {
+ "description": "Detector TensorRT para dispositivos Nvidia Jetson que utiliza motores TensorRT serializados para una inferencia acelerada.",
+ "label": "TensorRT",
+ "device": {
+ "label": "Índice de dispositivo GPU",
+ "description": "Índice del dispositivo GPU que se usará."
+ }
+ },
+ "degirum": {
+ "description": "Detector DeGirum para ejecutar modelos a través de la nube de DeGirum o servicios de inferencia local.",
+ "label": "DeGirum",
+ "location": {
+ "label": "Ubicación de inferencia",
+ "description": "Ubicación del motor de inferencia DeGirum (p. ej., '@cloud', '127.0.0.1')."
+ },
+ "zoo": {
+ "label": "Repositorio de modelos",
+ "description": "Ruta o URL al repositorio de modelos de DeGirum."
+ },
+ "token": {
+ "label": "Token de DeGirum Cloud",
+ "description": "Token para acceder a DeGirum Cloud."
+ }
+ },
+ "rknn": {
+ "description": "Detector RKNN para NPUs de Rockchip; ejecuta modelos compilados para RKNN en hardware de Rockchip.",
+ "label": "RKNN",
+ "num_cores": {
+ "label": "Número de núcleos NPU que se usarán.",
+ "description": "Número de núcleos NPU que se usarán (0 para automático)."
+ }
+ },
+ "model": {
+ "label": "Configuración de modelo específica del detector",
+ "description": "Opciones de configuración de modelo específicas del detector (ruta, tamaño de entrada, etc.).",
+ "path": {
+ "label": "Ruta del modelo de detector de objetos personalizado",
+ "description": "Ruta a un archivo de modelo de detección personalizado (o plus:// para modelos de Frigate+)."
+ },
+ "labelmap_path": {
+ "label": "Mapa de etiquetas para detector de objetos personalizado",
+ "description": "Ruta a un archivo labelmap que asigna clases numéricas a etiquetas de texto para el detector."
+ },
+ "width": {
+ "label": "Anchura de entrada del modelo de detección de objetos",
+ "description": "Anchura del tensor de entrada del modelo en píxeles."
+ },
+ "height": {
+ "label": "Altura de entrada del modelo de detección de objetos",
+ "description": "Altura del tensor de entrada del modelo en píxeles."
+ },
+ "labelmap": {
+ "label": "Personalización del mapa de etiquetas",
+ "description": "Sobrescrituras o entradas de reasignación que se fusionarán con el mapa de etiquetas estándar."
+ },
+ "attributes_map": {
+ "label": "Mapa de etiquetas de objetos a sus etiquetas de atributos",
+ "description": "Asignación de etiquetas de objetos a etiquetas de atributos usada para adjuntar metadatos (por ejemplo, 'car' -> ['license_plate'])."
+ },
+ "input_tensor": {
+ "label": "Forma del tensor de entrada del modelo",
+ "description": "Formato de tensor esperado por el modelo: 'nhwc' o 'nchw'."
+ },
+ "input_pixel_format": {
+ "label": "Formato de color de píxeles de entrada del modelo",
+ "description": "Espacio de color de píxeles esperado por el modelo: 'rgb', 'bgr' o 'yuv'."
+ },
+ "input_dtype": {
+ "label": "Tipo D de entrada del modelo",
+ "description": "Tipo de datos del tensor de entrada del modelo (por ejemplo, 'float32')."
+ },
+ "model_type": {
+ "label": "Tipo de modelo de detección de objetos",
+ "description": "Tipo de arquitectura del modelo detector (ssd, yolox, yolonas) usado por algunos detectores para optimización."
+ }
+ },
+ "model_path": {
+ "label": "Ruta de modelo específica del detector",
+ "description": "Ruta del archivo binario del modelo detector si lo requiere el detector elegido."
+ },
+ "edgetpu": {
+ "label": "EdgeTPU",
+ "description": "Detector EdgeTPU que ejecuta modelos TensorFlow Lite compilados para Coral EdgeTPU mediante el delegado EdgeTPU.",
+ "device": {
+ "label": "Tipo de dispositivo",
+ "description": "Dispositivo que se usará para la inferencia EdgeTPU (p. ej., 'usb', 'pci')."
+ }
+ },
+ "memryx": {
+ "label": "MemryX",
+ "description": "Detector MemryX MX3 que ejecuta modelos DFP compilados en aceleradores MemryX.",
+ "device": {
+ "label": "Ruta del dispositivo",
+ "description": "Dispositivo que se usará para la inferencia MemryX (p. ej., 'PCIe')."
}
}
+ },
+ "database": {
+ "label": "Base de datos",
+ "description": "Configuración de la base de datos SQLite utilizada por Frigate para almacenar los metadatos de los objetos rastreados y las grabaciones.",
+ "path": {
+ "label": "Ruta de la base de datos",
+ "description": "Ruta del sistema de archivos donde se almacenará el archivo de base de datos SQLite de Frigate."
+ }
+ },
+ "mqtt": {
+ "label": "MQTT",
+ "port": {
+ "label": "Puerto MQTT",
+ "description": "Puerto del broker MQTT (normalmente 1883 para MQTT sin cifrar)."
+ },
+ "tls_client_cert": {
+ "label": "Certificado cliente",
+ "description": "Ruta del certificado de cliente para autenticación TLS mutua; no configures usuario/contraseña al usar certificados de cliente."
+ },
+ "description": "Configuración para conectar y publicar telemetría, instantáneas y detalles de eventos en un broker MQTT.",
+ "topic_prefix": {
+ "description": "Prefijo del tema MQTT para todos los temas de Frigate; debe ser único si se ejecutan múltiples instancias.",
+ "label": "Prefijo de tema"
+ },
+ "client_id": {
+ "description": "Identificador de cliente utilizado al conectarse al broker MQTT; debe ser único para cada instancia.",
+ "label": "ID de cliente"
+ },
+ "enabled": {
+ "label": "Habilitar MQTT",
+ "description": "Habilita o deshabilita la integración MQTT para estados, eventos e instantáneas."
+ },
+ "host": {
+ "label": "Host MQTT",
+ "description": "Nombre de host o dirección IP del broker MQTT."
+ },
+ "stats_interval": {
+ "label": "Intervalo de estadísticas",
+ "description": "Intervalo en segundos para publicar estadísticas del sistema y de las cámaras en MQTT."
+ },
+ "user": {
+ "label": "Nombre de usuario MQTT",
+ "description": "Nombre de usuario MQTT opcional; puede proporcionarse mediante variables de entorno o secretos."
+ },
+ "password": {
+ "label": "Contraseña MQTT",
+ "description": "Contraseña MQTT opcional; puede proporcionarse mediante variables de entorno o secretos."
+ },
+ "tls_ca_certs": {
+ "label": "Certificados CA TLS",
+ "description": "Ruta al certificado CA para conexiones TLS con el broker (para certificados autofirmados)."
+ },
+ "tls_client_key": {
+ "label": "Clave de cliente",
+ "description": "Ruta de la clave privada del certificado de cliente."
+ },
+ "tls_insecure": {
+ "label": "TLS inseguro",
+ "description": "Permite conexiones TLS inseguras omitiendo la verificación del nombre de host (no recomendado)."
+ },
+ "qos": {
+ "label": "QoS de MQTT",
+ "description": "Nivel de calidad de servicio para publicaciones/suscripciones MQTT (0, 1 o 2)."
+ }
+ },
+ "notifications": {
+ "email": {
+ "label": "Email de notificacion",
+ "description": "Dirección de correo electrónico usada para notificaciones push o requerida por ciertos proveedores de notificaciones."
+ },
+ "label": "Notificaciones",
+ "enabled": {
+ "label": "Habilitar notificaciones",
+ "description": "Habilita o deshabilita las notificaciones para todas las cámaras; se puede sobrescribir por cámara."
+ },
+ "cooldown": {
+ "label": "Periodo de enfriamiento",
+ "description": "Periodo de enfriamiento (segundos) entre notificaciones para evitar saturar a los destinatarios."
+ },
+ "enabled_in_config": {
+ "label": "Estado original de notificaciones",
+ "description": "Indica si las notificaciones estaban habilitadas en la configuración estática original."
+ },
+ "description": "Ajustes para habilitar y controlar las notificaciones de todas las cámaras; se pueden sobrescribir por cámara."
+ },
+ "networking": {
+ "ipv6": {
+ "label": "Configuración IPV6",
+ "description": "Ajustes específicos de IPv6 para los servicios de red de Frigate.",
+ "enabled": {
+ "label": "Habilitar IPv6",
+ "description": "Habilita la compatibilidad con IPv6 para los servicios de Frigate (API e interfaz) cuando corresponda."
+ }
+ },
+ "listen": {
+ "internal": {
+ "label": "Puerto interno",
+ "description": "Puerto de escucha interno de Frigate (predeterminado 5000)."
+ },
+ "external": {
+ "label": "Puerto externo",
+ "description": "Puerto externo de escucha para Frigate (por defecto 8791)."
+ },
+ "description": "Configuración de los puertos de escucha internos y externos. Esto es para usuarios avanzados. Para la mayoría de los casos de uso, se recomienda modificar la sección de puertos de su configuración de Docker Compose.",
+ "label": "Configuración de puertos de escucha"
+ },
+ "label": "Red",
+ "description": "Ajustes relacionados con la red, como la habilitación de IPv6 para los endpoints de Frigate."
+ },
+ "proxy": {
+ "label": "Proxy",
+ "separator": {
+ "label": "Carácter de separación",
+ "description": "Carácter usado para separar varios valores proporcionados en las cabeceras del proxy."
+ },
+ "default_role": {
+ "description": "Rol predeterminado asignado a los usuarios autenticados mediante proxy cuando no se aplica ninguna asignación de roles.",
+ "label": "Rol predeterminado"
+ },
+ "description": "Configuración para integrar Frigate detrás de un proxy inverso que transmite encabezados de usuario autenticados.",
+ "header_map": {
+ "description": "Mapear los encabezados de proxy entrantes a los campos de usuario y rol de Frigate para la autenticación basada en proxy.",
+ "role": {
+ "description": "Encabezado que contiene el rol o los grupos del usuario autenticado provenientes del proxy ascendente.",
+ "label": "Cabecera de rol"
+ },
+ "label": "Asignación de cabeceras",
+ "user": {
+ "label": "Cabecera de usuario",
+ "description": "Cabecera que contiene el nombre de usuario autenticado proporcionado por el proxy ascendente."
+ },
+ "role_map": {
+ "label": "Asignación de roles",
+ "description": "Asigna valores de grupos ascendentes a roles de Frigate (por ejemplo, asignar grupos de administradores al rol de administrador)."
+ }
+ },
+ "logout_url": {
+ "label": "URL de cierre de sesión",
+ "description": "URL a la que redirigir a los usuarios al cerrar sesión mediante el proxy."
+ },
+ "auth_secret": {
+ "label": "Secreto del proxy",
+ "description": "Secreto opcional que se comprueba con la cabecera X-Proxy-Secret para verificar proxies de confianza."
+ }
+ },
+ "telemetry": {
+ "label": "Telemetria",
+ "stats": {
+ "intel_gpu_stats": {
+ "label": "Estadísticas GPU Intel",
+ "description": "Habilitar la recopilación de estadísticas de la GPU Intel si hay una GPU Intel presente."
+ },
+ "network_bandwidth": {
+ "label": "Ancho de banda",
+ "description": "Habilita la monitorización del ancho de banda de red por proceso para procesos ffmpeg de cámaras y detectores (requiere capacidades)."
+ },
+ "amd_gpu_stats": {
+ "label": "Estadísticas GPU Amd",
+ "description": "Habilitar la recopilación de estadísticas de la GPU AMD si hay una GPU AMD presente."
+ },
+ "intel_gpu_device": {
+ "description": "Dirección del bus PCI o ruta del dispositivo DRM (p. ej., /dev/dri/card1) usada para fijar las estadísticas de la GPU Intel a un dispositivo concreto cuando hay varios presentes.",
+ "label": "Dispositivo GPU Intel"
+ },
+ "label": "Estadísticas del sistema",
+ "description": "Opciones para habilitar/deshabilitar la recopilación de distintas estadísticas del sistema y de la GPU."
+ },
+ "version_check": {
+ "description": "Habilite una verificación saliente para detectar si hay disponible una versión más reciente de Frigate.",
+ "label": "Comprobación de versión"
+ },
+ "description": "Opciones de telemetría y estadísticas del sistema, incluida la monitorización de GPU y ancho de banda de red.",
+ "network_interfaces": {
+ "label": "Interfaces de red",
+ "description": "Lista de prefijos de nombres de interfaces de red que se monitorizarán para estadísticas de ancho de banda."
+ }
+ },
+ "ui": {
+ "timezone": {
+ "label": "Uso horario",
+ "description": "Zona horaria opcional que se mostrará en la interfaz de usuario (si no se especifica, se utilizará la hora local del navegador)."
+ },
+ "unit_system": {
+ "label": "Unidad de sistema",
+ "description": "Sistema de unidades para la visualización (métrico o imperial) utilizado en la interfaz de usuario y en MQTT."
+ },
+ "label": "Interfaz",
+ "description": "Preferencias de la interfaz de usuario, como zona horaria, formato de fecha/hora y unidades.",
+ "time_format": {
+ "label": "Formato de hora",
+ "description": "Formato de hora que se usará en la interfaz (browser, 12hour o 24hour)."
+ },
+ "date_style": {
+ "label": "Estilo de fecha",
+ "description": "Estilo de fecha que se usará en la interfaz (full, long, medium, short)."
+ },
+ "time_style": {
+ "label": "Estilo de hora",
+ "description": "Estilo de hora que se usará en la interfaz (full, long, medium, short)."
+ }
+ },
+ "audio_transcription": {
+ "description": "Configuración para la transcripción de audio en directo y de voz utilizada para eventos y subtítulos en directo.",
+ "language": {
+ "description": "Código de idioma utilizado para la transcripción/traducción (por ejemplo, 'es' para Español). Consulte https://whisper-api.com/docs/languages/ para ver los códigos de idioma compatibles.",
+ "label": "Idioma de transcripción"
+ },
+ "enabled": {
+ "description": "Habilitar o deshabilitar la transcripción automática de audio para todas las cámaras; puede anularse por cámara.",
+ "label": "Habilitar transcripción de audio"
+ },
+ "label": "Transcripción de audio",
+ "live_enabled": {
+ "label": "Transcripción en directo",
+ "description": "Habilitar la transcripción en directo del audio a medida que se recibe."
+ },
+ "device": {
+ "label": "Dispositivo de transcripción",
+ "description": "Clave del dispositivo (CPU/GPU) donde ejecutar el modelo de transcripción. Actualmente, solo se admiten GPU NVIDIA CUDA para la transcripción."
+ },
+ "model_size": {
+ "label": "Tamaño del modelo",
+ "description": "Tamaño del modelo que se usará para la transcripción sin conexión de eventos de audio."
+ }
+ },
+ "motion": {
+ "skip_motion_threshold": {
+ "description": "Si se establece en un valor entre 0,0 y 1,0, y más de esta fracción de la imagen cambia en un solo fotograma, el detector no devolverá cuadros de movimiento y se recalibrará inmediatamente. Esto puede ahorrar recursos de CPU y reducir los falsos positivos durante tormentas eléctricas, tempestades, etc., aunque podría pasar por alto eventos reales, como el seguimiento automático de un objeto por parte de una cámara PTZ. La disyuntiva está entre descartar unos cuantos megabytes de grabaciones o revisar un par de clips cortos. Deje este parámetro sin establecer (None) para desactivar esta función.",
+ "label": "Omitir umbral de movimiento"
+ },
+ "lightning_threshold": {
+ "description": "Umbral para detectar e ignorar breves picos de luz (un valor menor indica mayor sensibilidad; valores entre 0,3 y 1,0). Esto no impide por completo la detección de movimiento; Simplemente provoca que el detector deje de analizar fotogramas adicionales una vez que se supera el umbral. Durante estos eventos aún se realizan grabaciones basadas en el movimiento.",
+ "label": "Umbral de iluminación"
+ },
+ "threshold": {
+ "description": "Umbral de diferencia de píxeles utilizado por el detector de movimiento; los valores más altos reducen la sensibilidad (rango 1-255).",
+ "label": "Umbral de movimiento"
+ },
+ "enabled": {
+ "description": "Habilitar o deshabilitar la detección de movimiento para todas las cámaras; puede anularse para cada cámara individualmente.",
+ "label": "Habilitar detección de movimiento"
+ },
+ "label": "Detección de movimiento",
+ "improve_contrast": {
+ "label": "Mejorar contraste",
+ "description": "Aplica una mejora de contraste a los fotogramas antes del análisis de movimiento para ayudar a la detección."
+ },
+ "contour_area": {
+ "label": "Área de contorno",
+ "description": "Área mínima de contorno en píxeles necesaria para que se cuente un contorno de movimiento."
+ },
+ "delta_alpha": {
+ "label": "Delta alfa",
+ "description": "Factor de mezcla alfa usado en la diferencia entre fotogramas para calcular el movimiento."
+ },
+ "frame_alpha": {
+ "label": "Alfa del fotograma",
+ "description": "Valor alfa usado al mezclar fotogramas para el preprocesamiento de movimiento."
+ },
+ "frame_height": {
+ "label": "Altura del fotograma",
+ "description": "Altura en píxeles a la que escalar los fotogramas al calcular el movimiento."
+ },
+ "mask": {
+ "label": "Coordenadas de máscara",
+ "description": "Coordenadas x,y ordenadas que definen el polígono de máscara de movimiento usado para incluir/excluir áreas."
+ },
+ "mqtt_off_delay": {
+ "label": "Retraso de apagado MQTT",
+ "description": "Segundos a esperar tras el último movimiento antes de publicar un estado MQTT 'off'."
+ },
+ "enabled_in_config": {
+ "label": "Estado de movimiento original",
+ "description": "Indica si la detección de movimiento estaba habilitada en la configuración estática original."
+ },
+ "raw_mask": {
+ "label": "Máscara sin procesar"
+ },
+ "description": "Ajustes predeterminados de detección de movimiento aplicados a las cámaras salvo que se sobrescriban por cámara."
+ },
+ "lpr": {
+ "enhancement": {
+ "description": "Nivel de mejora (0-10) que se aplicará a los recortes de matrículas antes del OCR; los valores más altos no siempre mejoran los resultados, y los niveles superiores a 5 podrían funcionar únicamente con matrículas capturadas de noche, por lo que deben utilizarse con precaución.",
+ "label": "Nivel de mejora"
+ },
+ "expire_time": {
+ "description": "Tiempo en segundos tras el cual una matrícula no detectada caduca en el sistema de seguimiento (solo para cámaras LPR dedicadas).",
+ "label": "Segundos hasta caducar"
+ },
+ "enabled": {
+ "description": "Habilitar o deshabilitar el reconocimiento de matrículas para todas las cámaras; puede anularse por cámara.",
+ "label": "Habilitar LPR"
+ },
+ "min_plate_length": {
+ "description": "Número mínimo de caracteres que debe contener una matrícula reconocida para ser considerada válida.",
+ "label": "Longitud mínima de matrícula"
+ },
+ "label": "Reconocimiento de matrículas",
+ "description": "Ajustes de reconocimiento de matrículas, incluidos umbrales de detección, formato y matrículas conocidas.",
+ "min_area": {
+ "label": "Área mínima de matrícula",
+ "description": "Área mínima de matrícula (píxeles) necesaria para intentar el reconocimiento."
+ },
+ "model_size": {
+ "label": "Tamaño del modelo",
+ "description": "Tamaño del modelo usado para detección/reconocimiento de texto. La mayoría de usuarios debería usar 'small'."
+ },
+ "detection_threshold": {
+ "label": "Umbral de detección",
+ "description": "Umbral de confianza de detección para empezar a ejecutar OCR en una matrícula sospechosa."
+ },
+ "recognition_threshold": {
+ "label": "Umbral de reconocimiento",
+ "description": "Umbral de confianza necesario para adjuntar el texto de matrícula reconocido como subetiqueta."
+ },
+ "format": {
+ "label": "Regex de formato de matrícula",
+ "description": "Regex opcional para validar cadenas de matrícula reconocidas frente a un formato esperado."
+ },
+ "match_distance": {
+ "label": "Distancia de coincidencia",
+ "description": "Número de diferencias de caracteres permitidas al comparar matrículas detectadas con matrículas conocidas."
+ },
+ "known_plates": {
+ "label": "Matrículas conocidas",
+ "description": "Lista de matrículas o regexes que se rastrearán especialmente o sobre las que se alertará."
+ },
+ "debug_save_plates": {
+ "label": "Guardar matrículas de depuración",
+ "description": "Guarda imágenes recortadas de matrículas para depurar el rendimiento de LPR."
+ },
+ "device": {
+ "label": "Dispositivo",
+ "description": "Esto es una sobrescritura para apuntar a un dispositivo concreto. Consulta https://onnxruntime.ai/docs/execution-providers/ para obtener más información"
+ },
+ "replace_rules": {
+ "label": "Reglas de sustitución",
+ "description": "Reglas de sustitución regex usadas para normalizar cadenas de matrícula detectadas antes de compararlas.",
+ "pattern": {
+ "label": "Patrón regex"
+ },
+ "replacement": {
+ "label": "Cadena de sustitución"
+ }
+ }
+ },
+ "detect": {
+ "fps": {
+ "description": "Fotogramas por segundo deseados para ejecutar la detección; los valores más bajos reducen el uso de la CPU (el valor recomendado es 5; establezca un valor superior —como máximo de 10— únicamente si realiza el seguimiento de objetos que se mueven con extrema rapidez).",
+ "label": "FPS de detección"
+ },
+ "min_initialized": {
+ "description": "Número de detecciones consecutivas requeridas antes de crear un objeto rastreado. Auméntelo para reducir las inicializaciones falsas. El valor predeterminado es los FPS divididos por 2.",
+ "label": "Fotogramas mínimos de inicialización"
+ },
+ "height": {
+ "description": "Altura (en píxeles) de los fotogramas utilizados para la transmisión de detección; déjelo vacío para utilizar la resolución nativa de la transmisión.",
+ "label": "Altura de detección"
+ },
+ "width": {
+ "description": "Ancho (en píxeles) de los fotogramas utilizados para la transmisión de detección; déjelo vacío para utilizar la resolución nativa de la transmisión.",
+ "label": "Anchura de detección"
+ },
+ "stationary": {
+ "description": "Configuración para detectar y gestionar objetos que permanecen inmóviles durante un periodo de tiempo.",
+ "label": "Configuración de objetos estacionarios",
+ "interval": {
+ "label": "Intervalo estacionario",
+ "description": "Frecuencia (en fotogramas) con la que se ejecuta una comprobación de detección para confirmar un objeto estacionario."
+ },
+ "threshold": {
+ "label": "Umbral estacionario",
+ "description": "Número de fotogramas sin cambio de posición necesarios para marcar un objeto como estacionario."
+ },
+ "max_frames": {
+ "label": "Fotogramas máximos",
+ "description": "Limita durante cuánto tiempo se rastrean los objetos estacionarios antes de descartarlos.",
+ "default": {
+ "label": "Fotogramas máximos predeterminados",
+ "description": "Número máximo predeterminado de fotogramas para rastrear un objeto estacionario antes de detenerse."
+ },
+ "objects": {
+ "label": "Fotogramas máximos por objeto",
+ "description": "Sobrescrituras por objeto para el número máximo de fotogramas en los que rastrear objetos estacionarios."
+ }
+ },
+ "classifier": {
+ "label": "Habilitar clasificador visual",
+ "description": "Usa un clasificador visual para detectar objetos realmente estacionarios incluso cuando los cuadros delimitadores oscilan."
+ }
+ },
+ "enabled": {
+ "description": "Habilitar o deshabilitar la detección de objetos para todas las cámaras; puede anularse para cada cámara individualmente.",
+ "label": "Habilitar detección de objetos"
+ },
+ "label": "Detección de objetos",
+ "description": "Ajustes del rol de detección/detect usado para ejecutar la detección de objetos e inicializar los rastreadores.",
+ "max_disappeared": {
+ "label": "Fotogramas máximos desaparecido",
+ "description": "Número de fotogramas sin detección antes de que un objeto rastreado se considere desaparecido."
+ },
+ "annotation_offset": {
+ "label": "Desplazamiento de anotaciones",
+ "description": "Milisegundos para desplazar las anotaciones de detección y alinear mejor los cuadros delimitadores de la línea de tiempo con las grabaciones; puede ser positivo o negativo."
+ }
+ },
+ "record": {
+ "motion": {
+ "description": "Número de días para conservar las grabaciones activadas por movimiento, independientemente de los objetos rastreados. Establézcalo en 0 si solo desea conservar las grabaciones de alertas y detecciones.",
+ "label": "Retención de movimiento",
+ "days": {
+ "label": "Días de retención",
+ "description": "Días durante los que conservar las grabaciones."
+ }
+ },
+ "continuous": {
+ "description": "Número de días para conservar las grabaciones, independientemente de los objetos rastreados o del movimiento. Establézcalo en 0 si solo desea conservar las grabaciones de alertas y detecciones.",
+ "label": "Retención continua",
+ "days": {
+ "label": "Días de retención",
+ "description": "Días durante los que conservar las grabaciones."
+ }
+ },
+ "detections": {
+ "pre_capture": {
+ "description": "Número de segundos antes del evento de detección que se incluirán en la grabación.",
+ "label": "Segundos de captura previa"
+ },
+ "post_capture": {
+ "description": "Número de segundos después del evento de detección que se incluirán en la grabación.",
+ "label": "Segundos de captura posterior"
+ },
+ "label": "Retención de detección",
+ "description": "Ajustes de retención de grabaciones para eventos de detección, incluidas las duraciones de captura previa/posterior.",
+ "retain": {
+ "label": "Retención de eventos",
+ "description": "Ajustes de retención para grabaciones de eventos de detección.",
+ "days": {
+ "label": "Días de retención",
+ "description": "Número de días durante los que conservar grabaciones de eventos de detección."
+ },
+ "mode": {
+ "label": "Modo de retención",
+ "description": "Modo de retención: all (guarda todos los segmentos), motion (guarda segmentos con movimiento) o active_objects (guarda segmentos con objetos activos)."
+ }
+ }
+ },
+ "alerts": {
+ "pre_capture": {
+ "description": "Número de segundos antes del evento de detección que se incluirán en la grabación.",
+ "label": "Segundos de captura previa"
+ },
+ "post_capture": {
+ "description": "Número de segundos después del evento de detección que se incluirán en la grabación.",
+ "label": "Segundos de captura posterior"
+ },
+ "label": "Retención de alertas",
+ "description": "Ajustes de retención de grabaciones para eventos de alerta, incluidas las duraciones de captura previa/posterior.",
+ "retain": {
+ "label": "Retención de eventos",
+ "description": "Ajustes de retención para grabaciones de eventos de detección.",
+ "days": {
+ "label": "Días de retención",
+ "description": "Número de días durante los que conservar grabaciones de eventos de detección."
+ },
+ "mode": {
+ "label": "Modo de retención",
+ "description": "Modo de retención: all (guarda todos los segmentos), motion (guarda segmentos con movimiento) o active_objects (guarda segmentos con objetos activos)."
+ }
+ }
+ },
+ "label": "Grabación",
+ "enabled": {
+ "label": "Habilitar grabación",
+ "description": "Habilita o deshabilita la grabación para todas las cámaras; se puede sobrescribir por cámara."
+ },
+ "expire_interval": {
+ "label": "Intervalo de limpieza de grabaciones",
+ "description": "Minutos entre pasadas de limpieza que eliminan segmentos de grabación caducados."
+ },
+ "export": {
+ "label": "Configuración de exportación",
+ "description": "Ajustes usados al exportar grabaciones, como timelapse y aceleración por hardware.",
+ "hwaccel_args": {
+ "label": "Argumentos hwaccel de exportación",
+ "description": "Argumentos de aceleración por hardware que se usarán en operaciones de exportación/transcodificación."
+ },
+ "max_concurrent": {
+ "label": "Exportaciones simultáneas máximas",
+ "description": "Número máximo de trabajos de exportación que se procesarán al mismo tiempo."
+ }
+ },
+ "preview": {
+ "label": "Configuración de vista previa",
+ "description": "Ajustes que controlan la calidad de las vistas previas de grabaciones mostradas en la interfaz.",
+ "quality": {
+ "label": "Calidad de vista previa",
+ "description": "Nivel de calidad de vista previa (very_low, low, medium, high, very_high)."
+ }
+ },
+ "enabled_in_config": {
+ "label": "Estado de grabación original",
+ "description": "Indica si la grabación estaba habilitada en la configuración estática original."
+ },
+ "description": "Ajustes de grabación y retención aplicados a las cámaras salvo que se sobrescriban por cámara."
+ },
+ "camera_ui": {
+ "dashboard": {
+ "description": "Alterna si esta cámara es visible en toda la interfaz de usuario de Frigate. Desactivar esta opción requerirá editar manualmente la configuración para volver a visualizar esta cámara en la interfaz.",
+ "label": "Mostrar en la interfaz"
+ },
+ "label": "Interfaz de cámara",
+ "description": "Orden de visualización y visibilidad de esta cámara en la interfaz. El orden afecta al panel predeterminado. Para un control más granular, usa grupos de cámaras.",
+ "order": {
+ "label": "Orden en la interfaz",
+ "description": "Orden numérico usado para ordenar la cámara en la interfaz (panel predeterminado y listas); los números más altos aparecen más tarde."
+ },
+ "review": {
+ "label": "Mostrar en Revisión",
+ "description": "Activa o desactiva si esta cámara se muestra en Revisión (la página de Revisión y su filtro de cámaras, la revisión de movimiento y la vista de historial)."
+ }
+ },
+ "live": {
+ "description": "Configuración para controlar la resolución y la calidad de la transmisión en vivo de jsmpeg. Esto no afecta a las cámaras retransmitidas que utilizan go2rtc para la visualización en vivo.",
+ "height": {
+ "description": "Altura (en píxeles) para renderizar la transmisión en vivo de jsmpeg en la interfaz web; debe ser <= a la altura de la transmisión de detección.",
+ "label": "Altura en directo"
+ },
+ "label": "Reproducción en directo",
+ "streams": {
+ "label": "Nombres de flujos en directo",
+ "description": "Asignación de nombres de flujos configurados a nombres de restream/go2rtc usados para la reproducción en directo."
+ },
+ "quality": {
+ "label": "Calidad en directo",
+ "description": "Calidad de codificación para el flujo jsmpeg (1 la más alta, 31 la más baja)."
+ }
+ },
+ "semantic_search": {
+ "model": {
+ "description": "El modelo de embeddings a utilizar para la búsqueda semántica (por ejemplo, 'jinav1'), o el nombre de un proveedor de GenAI con el rol de embeddings.",
+ "label": "Modelo de búsqueda semántica o nombre del proveedor GenAI"
+ },
+ "label": "Búsqueda semántica",
+ "triggers": {
+ "label": "Activadores",
+ "description": "Acciones y criterios de coincidencia para activadores de búsqueda semántica específicos de la cámara.",
+ "friendly_name": {
+ "label": "Nombre descriptivo",
+ "description": "Nombre descriptivo opcional mostrado en la interfaz para este activador."
+ },
+ "enabled": {
+ "label": "Habilitar este activador",
+ "description": "Habilita o deshabilita este activador de búsqueda semántica."
+ },
+ "type": {
+ "label": "Tipo de activador",
+ "description": "Tipo de activador: 'thumbnail' (coincidir con imagen) o 'description' (coincidir con texto)."
+ },
+ "data": {
+ "label": "Contenido del activador",
+ "description": "Frase de texto o ID de miniatura que se comparará con objetos rastreados."
+ },
+ "threshold": {
+ "label": "Umbral del activador",
+ "description": "Puntuación mínima de similitud (0-1) necesaria para activar este activador."
+ },
+ "actions": {
+ "label": "Acciones del activador",
+ "description": "Lista de acciones que se ejecutarán cuando el activador coincida (notification, sub_label, attribute)."
+ }
+ },
+ "description": "Ajustes de la búsqueda semántica, que crea y consulta embeddings de objetos para encontrar elementos similares.",
+ "enabled": {
+ "label": "Habilitar búsqueda semántica",
+ "description": "Habilita o deshabilita la función de búsqueda semántica."
+ },
+ "reindex": {
+ "label": "Reindexar al iniciar",
+ "description": "Activa una reindexación completa de los objetos rastreados históricos en la base de datos de embeddings."
+ },
+ "model_size": {
+ "label": "Tamaño del modelo",
+ "description": "Selecciona el tamaño del modelo; 'small' se ejecuta en CPU y 'large' normalmente requiere GPU."
+ },
+ "device": {
+ "label": "Dispositivo",
+ "description": "Esto es una sobrescritura para apuntar a un dispositivo concreto. Consulta https://onnxruntime.ai/docs/execution-providers/ para obtener más información"
+ }
+ },
+ "review": {
+ "alerts": {
+ "required_zones": {
+ "description": "Zonas en las que debe entrar un objeto para ser considerado una alerta; dejar vacío para permitir cualquier zona.",
+ "label": "Zonas requeridas"
+ },
+ "labels": {
+ "description": "Lista de etiquetas de objetos que califican como alertas (por ejemplo: car, person).",
+ "label": "Etiquetas de alerta"
+ },
+ "label": "Configuración de alertas",
+ "description": "Ajustes sobre qué objetos rastreados generan alertas y cómo se conservan las alertas.",
+ "enabled": {
+ "label": "Habilitar alertas",
+ "description": "Habilita o deshabilita la generación de alertas para todas las cámaras; se puede sobrescribir por cámara."
+ },
+ "enabled_in_config": {
+ "label": "Estado original de alertas",
+ "description": "Rastrea si las alertas estaban habilitadas originalmente en la configuración estática."
+ },
+ "cutoff_time": {
+ "label": "Tiempo de corte de alertas",
+ "description": "Segundos que se esperarán tras dejar de haber actividad causante de alerta antes de cortar una alerta."
+ }
+ },
+ "detections": {
+ "required_zones": {
+ "description": "Zonas en las que debe entrar un objeto para ser considerado detectado; dejar vacío para permitir cualquier zona.",
+ "label": "Zonas requeridas"
+ },
+ "description": "Configuración para determinar qué objetos rastreados generan detecciones (no alertas) y cómo se retienen dichas detecciones.",
+ "label": "Configuración de detecciones",
+ "enabled": {
+ "label": "Habilitar detecciones",
+ "description": "Habilita o deshabilita los eventos de detección para todas las cámaras; se puede sobrescribir por cámara."
+ },
+ "labels": {
+ "label": "Etiquetas de detección",
+ "description": "Lista de etiquetas de objetos que cuentan como eventos de detección."
+ },
+ "cutoff_time": {
+ "label": "Tiempo de corte de detecciones",
+ "description": "Segundos que se esperarán tras dejar de haber actividad causante de detección antes de cortar una detección."
+ },
+ "enabled_in_config": {
+ "label": "Estado original de detecciones",
+ "description": "Rastrea si las detecciones estaban habilitadas originalmente en la configuración estática."
+ }
+ },
+ "genai": {
+ "image_source": {
+ "description": "Fuente de las imágenes enviadas a GenAI ('preview' o 'recordings'); La opción 'recordings' utiliza fotogramas de mayor calidad, pero requiere más tokens.",
+ "label": "Origen de imagen de revisión"
+ },
+ "additional_concerns": {
+ "description": "Una lista de preocupaciones o notas adicionales que GenAI debería tener en cuenta al evaluar la actividad en esta cámara.",
+ "label": "Consideraciones adicionales"
+ },
+ "activity_context_prompt": {
+ "description": "Instrucción personalizada que describe qué constituye y qué no una actividad sospechosa, con el fin de proporcionar contexto para los resúmenes generados por GenAI.",
+ "label": "Prompt de contexto de actividad"
+ },
+ "description": "Controla el uso de IA generativa (GenAI) para la elaboración de descripciones y resúmenes de elementos de revisión.",
+ "debug_save_thumbnails": {
+ "description": "Guarde las miniaturas que se envían al proveedor de GenAI para su depuración y revisión.",
+ "label": "Guardar miniaturas"
+ },
+ "label": "Configuración de GenAI",
+ "enabled": {
+ "label": "Habilitar descripciones de GenAI",
+ "description": "Habilita o deshabilita las descripciones y resúmenes generados por GenAI para los elementos de revisión."
+ },
+ "alerts": {
+ "label": "Habilitar GenAI para alertas",
+ "description": "Usa GenAI para generar descripciones de elementos de alerta."
+ },
+ "detections": {
+ "label": "Habilitar GenAI para detecciones",
+ "description": "Usa GenAI para generar descripciones de elementos de detección."
+ },
+ "enabled_in_config": {
+ "label": "Estado original de GenAI",
+ "description": "Rastrea si la revisión de GenAI estaba habilitada originalmente en la configuración estática."
+ },
+ "preferred_language": {
+ "label": "Idioma preferido",
+ "description": "Idioma preferido que se solicitará al proveedor de GenAI para las respuestas generadas."
+ }
+ },
+ "label": "Revisión",
+ "description": "Ajustes que controlan alertas, detecciones y resúmenes de revisión de GenAI usados por la interfaz y el almacenamiento."
+ },
+ "birdseye": {
+ "description": "Configuración para la vista compuesta Birdseye, que combina las transmisiones de múltiples cámaras en una sola vista.",
+ "restream": {
+ "description": "Retransmita la salida de video de Birdseye como una transmisión en vivo RTSP; al habilitar esta opción, Birdseye se mantendrá en ejecución de forma continua.",
+ "label": "Retransmisión RTSP"
+ },
+ "layout": {
+ "max_cameras": {
+ "description": "Número máximo de cámaras a mostrar simultáneamente en Birdseye; muestra las cámaras más recientes.",
+ "label": "Cámaras máximas"
+ },
+ "label": "Diseño",
+ "description": "Opciones de diseño para la composición de Birdseye.",
+ "scaling_factor": {
+ "label": "Factor de escala",
+ "description": "Factor de escala usado por el calculador de diseño (rango de 1.0 a 5.0)."
+ }
+ },
+ "label": "Birdseye",
+ "enabled": {
+ "label": "Habilitar Birdseye",
+ "description": "Habilita o deshabilita la función de vista Birdseye."
+ },
+ "mode": {
+ "label": "Modo de seguimiento",
+ "description": "Modo para incluir cámaras en Birdseye: 'objects', 'motion' o 'continuous'."
+ },
+ "order": {
+ "label": "Posición",
+ "description": "Posición numérica que controla el orden de la cámara en el diseño de Birdseye."
+ },
+ "width": {
+ "label": "Anchura",
+ "description": "Anchura de salida (píxeles) del fotograma compuesto de Birdseye."
+ },
+ "height": {
+ "label": "Altura",
+ "description": "Altura de salida (píxeles) del fotograma compuesto de Birdseye."
+ },
+ "quality": {
+ "label": "Calidad de codificación",
+ "description": "Calidad de codificación para el flujo mpeg1 de Birdseye (1 la calidad más alta, 31 la más baja)."
+ },
+ "inactivity_threshold": {
+ "label": "Umbral de inactividad",
+ "description": "Segundos de inactividad tras los cuales una cámara dejará de mostrarse en Birdseye."
+ },
+ "idle_heartbeat_fps": {
+ "label": "FPS de latido en reposo",
+ "description": "Fotogramas por segundo para reenviar el último fotograma compuesto de Birdseye en reposo; establécelo en 0 para deshabilitarlo."
+ }
+ },
+ "ffmpeg": {
+ "retry_interval": {
+ "description": "Segundos de espera antes de intentar reconectar la transmisión de una cámara tras un fallo. El valor predeterminado es 10.",
+ "label": "Tiempo de reintento de FFmpeg"
+ },
+ "path": {
+ "description": "Ruta al binario de FFmpeg que se va a utilizar o un alias de versión (\"7.0\" o \"8.0\").",
+ "label": "Ruta de FFmpeg"
+ },
+ "output_args": {
+ "description": "Argumentos de salida predeterminados utilizados para diferentes roles de FFmpeg, tales como detección y grabación.",
+ "label": "Argumentos de salida",
+ "detect": {
+ "label": "Argumentos de salida de detección",
+ "description": "Argumentos de salida predeterminados para los flujos con rol de detección."
+ },
+ "record": {
+ "label": "Argumentos de salida de grabación",
+ "description": "Argumentos de salida predeterminados para los flujos con rol de grabación."
+ }
+ },
+ "description": "Configuración de FFmpeg, incluyendo la ruta del binario, argumentos, opciones de aceleración por hardware y argumentos de salida por rol.",
+ "label": "FFmpeg",
+ "global_args": {
+ "label": "Argumentos globales de FFmpeg",
+ "description": "Argumentos globales pasados a los procesos de FFmpeg."
+ },
+ "hwaccel_args": {
+ "label": "Argumentos de aceleración por hardware",
+ "description": "Argumentos de aceleración por hardware para FFmpeg. Se recomiendan preajustes específicos del proveedor."
+ },
+ "input_args": {
+ "label": "Argumentos de entrada",
+ "description": "Argumentos de entrada aplicados a los flujos de entrada de FFmpeg."
+ },
+ "apple_compatibility": {
+ "label": "Compatibilidad con Apple",
+ "description": "Habilita el etiquetado HEVC para mejorar la compatibilidad con reproductores de Apple al grabar H.265."
+ },
+ "gpu": {
+ "label": "Índice de GPU",
+ "description": "Índice de GPU predeterminado usado para la aceleración por hardware si está disponible."
+ },
+ "inputs": {
+ "label": "Entradas de cámara",
+ "description": "Lista de definiciones de flujos de entrada (rutas y roles) para esta cámara.",
+ "path": {
+ "label": "Ruta de entrada",
+ "description": "URL o ruta del flujo de entrada de la cámara."
+ },
+ "roles": {
+ "label": "Roles de entrada",
+ "description": "Roles para este flujo de entrada."
+ },
+ "global_args": {
+ "label": "Argumentos globales de FFmpeg",
+ "description": "Argumentos globales de FFmpeg para este flujo de entrada."
+ },
+ "hwaccel_args": {
+ "label": "Argumentos de aceleración por hardware",
+ "description": "Argumentos de aceleración por hardware para este flujo de entrada."
+ },
+ "input_args": {
+ "label": "Argumentos de entrada",
+ "description": "Argumentos de entrada específicos para este flujo."
+ }
+ }
+ },
+ "go2rtc": {
+ "description": "Configuración del servicio integrado de retransmisión go2rtc, utilizado para el relevo y la traducción de transmisiones en vivo.",
+ "label": "go2rtc"
+ },
+ "genai": {
+ "description": "Configuración para los proveedores integrados de IA generativa (GenAI) utilizados para generar descripciones de objetos y resúmenes de reseñas.",
+ "api_key": {
+ "description": "Clave de API requerida por algunos proveedores (también puede configurarse mediante variables de entorno).",
+ "label": "Clave API"
+ },
+ "base_url": {
+ "description": "URL base para proveedores autoalojados o compatibles (por ejemplo, una instancia de Ollama).",
+ "label": "URL base"
+ },
+ "model": {
+ "description": "El modelo del proveedor que se utilizará para generar descripciones o resúmenes.",
+ "label": "Modelo"
+ },
+ "label": "Configuración de IA generativa",
+ "provider": {
+ "label": "Proveedor",
+ "description": "Proveedor de GenAI que se usará (por ejemplo: ollama, gemini, openai)."
+ },
+ "roles": {
+ "label": "Roles",
+ "description": "Roles de GenAI (chat, descriptions, embeddings); un proveedor por rol."
+ },
+ "provider_options": {
+ "label": "Opciones del proveedor",
+ "description": "Opciones adicionales específicas del proveedor que se pasarán al cliente GenAI."
+ },
+ "runtime_options": {
+ "label": "Opciones de ejecución",
+ "description": "Opciones de ejecución pasadas al proveedor para cada llamada de inferencia."
+ }
+ },
+ "face_recognition": {
+ "description": "Configuración para la detección y el reconocimiento facial en todas las cámaras; puede anularse por cámara.",
+ "label": "Reconocimiento facial",
+ "enabled": {
+ "label": "Habilitar reconocimiento facial",
+ "description": "Habilita o deshabilita el reconocimiento facial para todas las cámaras; se puede sobrescribir por cámara."
+ },
+ "min_area": {
+ "label": "Área mínima de rostro",
+ "description": "Área mínima (píxeles) del cuadro de un rostro detectado necesaria para intentar el reconocimiento."
+ },
+ "model_size": {
+ "label": "Tamaño del modelo",
+ "description": "Tamaño del modelo que se usará para embeddings faciales (small/large); el más grande puede requerir GPU."
+ },
+ "unknown_score": {
+ "label": "Umbral de puntuación desconocida",
+ "description": "Umbral de distancia por debajo del cual un rostro se considera una posible coincidencia (más alto = más estricto)."
+ },
+ "detection_threshold": {
+ "label": "Umbral de detección",
+ "description": "Confianza mínima de detección necesaria para considerar válida una detección de rostro."
+ },
+ "recognition_threshold": {
+ "label": "Umbral de reconocimiento",
+ "description": "Umbral de distancia de embedding facial para considerar que dos rostros coinciden."
+ },
+ "min_faces": {
+ "label": "Rostros mínimos",
+ "description": "Número mínimo de reconocimientos faciales necesarios antes de aplicar una subetiqueta reconocida a una persona."
+ },
+ "save_attempts": {
+ "label": "Guardar intentos",
+ "description": "Número de intentos de reconocimiento facial que se conservarán para la interfaz de reconocimientos recientes."
+ },
+ "blur_confidence_filter": {
+ "label": "Filtro de confianza por desenfoque",
+ "description": "Ajusta las puntuaciones de confianza según el desenfoque de la imagen para reducir falsos positivos en rostros de baja calidad."
+ },
+ "device": {
+ "label": "Dispositivo",
+ "description": "Esto es una sobrescritura para apuntar a un dispositivo concreto. Consulta https://onnxruntime.ai/docs/execution-providers/ para obtener más información"
+ }
+ },
+ "camera_mqtt": {
+ "required_zones": {
+ "description": "Zonas en las que debe entrar un objeto para que se publique una imagen MQTT.",
+ "label": "Zonas requeridas"
+ },
+ "label": "MQTT",
+ "description": "Ajustes de publicación de imágenes MQTT.",
+ "enabled": {
+ "label": "Enviar imagen",
+ "description": "Habilita la publicación de instantáneas de objetos en temas MQTT para esta cámara."
+ },
+ "timestamp": {
+ "label": "Añadir marca de tiempo",
+ "description": "Superpone una marca de tiempo en las imágenes publicadas en MQTT."
+ },
+ "bounding_box": {
+ "label": "Añadir cuadro delimitador",
+ "description": "Dibuja cuadros delimitadores en las imágenes publicadas mediante MQTT."
+ },
+ "crop": {
+ "label": "Recortar imagen",
+ "description": "Recorta las imágenes publicadas en MQTT al cuadro delimitador del objeto detectado."
+ },
+ "height": {
+ "label": "Altura de imagen",
+ "description": "Altura (píxeles) a la que redimensionar las imágenes publicadas mediante MQTT."
+ },
+ "quality": {
+ "label": "Calidad JPEG",
+ "description": "Calidad JPEG de las imágenes publicadas en MQTT (0-100)."
+ }
+ },
+ "snapshots": {
+ "label": "Instantáneas",
+ "enabled": {
+ "label": "Habilitar instantáneas",
+ "description": "Habilita o deshabilita el guardado de instantáneas para todas las cámaras; se puede sobrescribir por cámara."
+ },
+ "timestamp": {
+ "label": "Superposición de marca de tiempo",
+ "description": "Superpone una marca de tiempo en las instantáneas de la API."
+ },
+ "bounding_box": {
+ "label": "Superposición de cuadro delimitador",
+ "description": "Dibuja cuadros delimitadores para los objetos rastreados en las instantáneas de la API."
+ },
+ "crop": {
+ "label": "Recortar instantánea",
+ "description": "Recorta las instantáneas de la API al cuadro delimitador del objeto detectado."
+ },
+ "required_zones": {
+ "label": "Zonas requeridas",
+ "description": "Zonas en las que debe entrar un objeto para que se guarde una instantánea."
+ },
+ "height": {
+ "label": "Altura de instantánea",
+ "description": "Altura (píxeles) a la que redimensionar las instantáneas de la API; déjalo vacío para conservar el tamaño original."
+ },
+ "retain": {
+ "label": "Retención de instantáneas",
+ "description": "Ajustes de retención de instantáneas, incluidos días predeterminados y sobrescrituras por objeto.",
+ "default": {
+ "label": "Retención predeterminada",
+ "description": "Número predeterminado de días durante los que conservar instantáneas."
+ },
+ "mode": {
+ "label": "Modo de retención",
+ "description": "Modo de retención: all (guarda todos los segmentos), motion (guarda segmentos con movimiento) o active_objects (guarda segmentos con objetos activos)."
+ },
+ "objects": {
+ "label": "Retención por objeto",
+ "description": "Sobrescrituras por objeto para los días de retención de instantáneas."
+ }
+ },
+ "quality": {
+ "label": "Calidad de instantánea",
+ "description": "Calidad de codificación de las instantáneas guardadas (0-100)."
+ },
+ "description": "Ajustes para instantáneas generadas por la API de objetos rastreados en todas las cámaras; se pueden sobrescribir por cámara."
+ },
+ "timestamp_style": {
+ "label": "Estilo de marca de tiempo",
+ "position": {
+ "label": "Posición de marca de tiempo",
+ "description": "Posición de la marca de tiempo en la imagen (tl/tr/bl/br)."
+ },
+ "format": {
+ "label": "Formato de marca de tiempo",
+ "description": "Cadena de formato de fecha y hora usada para las marcas de tiempo (códigos de formato datetime de Python)."
+ },
+ "color": {
+ "label": "Color de marca de tiempo",
+ "description": "Valores de color RGB para el texto de la marca de tiempo (todos los valores 0-255).",
+ "red": {
+ "label": "Rojo",
+ "description": "Componente rojo (0-255) para el color de la marca de tiempo."
+ },
+ "green": {
+ "label": "Verde",
+ "description": "Componente verde (0-255) para el color de la marca de tiempo."
+ },
+ "blue": {
+ "label": "Azul",
+ "description": "Componente azul (0-255) para el color de la marca de tiempo."
+ }
+ },
+ "thickness": {
+ "label": "Grosor de marca de tiempo",
+ "description": "Grosor de línea del texto de la marca de tiempo."
+ },
+ "effect": {
+ "label": "Efecto de marca de tiempo",
+ "description": "Efecto visual para el texto de la marca de tiempo (none, solid, shadow)."
+ },
+ "description": "Opciones de estilo para marcas de tiempo integradas aplicadas a la vista de depuración y a las instantáneas."
+ },
+ "profiles": {
+ "label": "Perfiles",
+ "description": "Definiciones de perfiles con nombre y nombres descriptivos. Los perfiles de cámara deben hacer referencia a nombres definidos aquí.",
+ "friendly_name": {
+ "label": "Nombre descriptivo",
+ "description": "Nombre mostrado para este perfil en la interfaz."
+ }
+ },
+ "tls": {
+ "label": "TLS",
+ "description": "Ajustes TLS para los endpoints web de Frigate (puerto 8971).",
+ "enabled": {
+ "label": "Habilitar TLS",
+ "description": "Habilita TLS para la interfaz web y la API de Frigate en el puerto TLS configurado."
+ }
+ },
+ "model": {
+ "label": "Modelo de detección",
+ "description": "Ajustes para configurar un modelo de detección de objetos personalizado y su forma de entrada.",
+ "path": {
+ "label": "Ruta del modelo de detector de objetos personalizado",
+ "description": "Ruta a un archivo de modelo de detección personalizado (o plus:// para modelos de Frigate+)."
+ },
+ "labelmap_path": {
+ "label": "Mapa de etiquetas para detector de objetos personalizado",
+ "description": "Ruta a un archivo labelmap que asigna clases numéricas a etiquetas de texto para el detector."
+ },
+ "width": {
+ "label": "Anchura de entrada del modelo de detección de objetos",
+ "description": "Anchura del tensor de entrada del modelo en píxeles."
+ },
+ "height": {
+ "label": "Altura de entrada del modelo de detección de objetos",
+ "description": "Altura del tensor de entrada del modelo en píxeles."
+ },
+ "labelmap": {
+ "label": "Personalización del mapa de etiquetas",
+ "description": "Sobrescrituras o entradas de reasignación que se fusionarán con el mapa de etiquetas estándar."
+ },
+ "attributes_map": {
+ "label": "Mapa de etiquetas de objetos a sus etiquetas de atributos",
+ "description": "Asignación de etiquetas de objetos a etiquetas de atributos usada para adjuntar metadatos (por ejemplo, 'car' -> ['license_plate'])."
+ },
+ "input_tensor": {
+ "label": "Forma del tensor de entrada del modelo",
+ "description": "Formato de tensor esperado por el modelo: 'nhwc' o 'nchw'."
+ },
+ "input_pixel_format": {
+ "label": "Formato de color de píxeles de entrada del modelo",
+ "description": "Espacio de color de píxeles esperado por el modelo: 'rgb', 'bgr' o 'yuv'."
+ },
+ "input_dtype": {
+ "label": "Tipo D de entrada del modelo",
+ "description": "Tipo de datos del tensor de entrada del modelo (por ejemplo, 'float32')."
+ },
+ "model_type": {
+ "label": "Tipo de modelo de detección de objetos",
+ "description": "Tipo de arquitectura del modelo detector (ssd, yolox, yolonas) usado por algunos detectores para optimización."
+ }
+ },
+ "classification": {
+ "label": "Clasificación de objetos",
+ "description": "Ajustes de los modelos de clasificación usados para refinar etiquetas de objetos o clasificación de estado.",
+ "bird": {
+ "label": "Configuración de clasificación de aves",
+ "description": "Ajustes específicos de los modelos de clasificación de aves.",
+ "enabled": {
+ "label": "Clasificación de aves",
+ "description": "Habilita o deshabilita la clasificación de aves."
+ },
+ "threshold": {
+ "label": "Puntuación mínima",
+ "description": "Puntuación mínima de clasificación necesaria para aceptar una clasificación de ave."
+ }
+ },
+ "custom": {
+ "label": "Modelos de clasificación personalizados",
+ "description": "Configuración de modelos de clasificación personalizados usados para objetos o detección de estado.",
+ "enabled": {
+ "label": "Habilitar modelo",
+ "description": "Habilita o deshabilita el modelo de clasificación personalizado."
+ },
+ "name": {
+ "label": "Nombre del modelo",
+ "description": "Identificador del modelo de clasificación personalizado que se usará."
+ },
+ "threshold": {
+ "label": "Umbral de puntuación",
+ "description": "Umbral de puntuación usado para cambiar el estado de clasificación."
+ },
+ "save_attempts": {
+ "label": "Guardar intentos",
+ "description": "Cuántos intentos de clasificación se guardarán para la interfaz de clasificaciones recientes."
+ },
+ "object_config": {
+ "objects": {
+ "label": "Clasificar objetos",
+ "description": "Lista de tipos de objetos sobre los que ejecutar la clasificación de objetos."
+ },
+ "classification_type": {
+ "label": "Tipo de clasificación",
+ "description": "Tipo de clasificación aplicado: 'sub_label' (añade sub_label) u otros tipos compatibles."
+ }
+ },
+ "state_config": {
+ "cameras": {
+ "label": "Cámaras de clasificación",
+ "description": "Recorte y ajustes por cámara para ejecutar la clasificación de estado.",
+ "crop": {
+ "label": "Recorte de clasificación",
+ "description": "Coordenadas de recorte que se usarán para ejecutar la clasificación en esta cámara."
+ }
+ },
+ "motion": {
+ "label": "Ejecutar con movimiento",
+ "description": "Si es true, ejecuta la clasificación cuando se detecte movimiento dentro del recorte especificado."
+ },
+ "interval": {
+ "label": "Intervalo de clasificación",
+ "description": "Intervalo (segundos) entre ejecuciones periódicas de clasificación para la clasificación de estado."
+ }
+ }
+ }
+ },
+ "camera_groups": {
+ "label": "Grupos de cámaras",
+ "description": "Configuración de grupos de cámaras con nombre usados para organizar cámaras en la interfaz.",
+ "cameras": {
+ "label": "Lista de cámaras",
+ "description": "Array de nombres de cámaras incluidos en este grupo."
+ },
+ "icon": {
+ "label": "Icono de grupo",
+ "description": "Icono usado para representar el grupo de cámaras en la interfaz."
+ },
+ "order": {
+ "label": "Orden de clasificación",
+ "description": "Orden numérico usado para ordenar grupos de cámaras en la interfaz; los números más altos aparecen más tarde."
+ }
+ },
+ "active_profile": {
+ "label": "Perfil activo",
+ "description": "Nombre del perfil activo actualmente. Solo en tiempo de ejecución, no se conserva en YAML."
}
}
diff --git a/web/public/locales/es/config/groups.json b/web/public/locales/es/config/groups.json
index d6b2b9d81e..a8cb25b469 100644
--- a/web/public/locales/es/config/groups.json
+++ b/web/public/locales/es/config/groups.json
@@ -59,6 +59,15 @@
"global": {
"retention": "Retención global",
"events": "Eventos globales"
+ },
+ "cameras": {
+ "events": "Evento",
+ "retention": "Retención"
+ }
+ },
+ "ffmpeg": {
+ "cameras": {
+ "cameraFfmpeg": "Argumentos de FFmpeg específicos de la cámara"
}
}
}
diff --git a/web/public/locales/es/config/validation.json b/web/public/locales/es/config/validation.json
index faf7032f87..dc35c3e1bb 100644
--- a/web/public/locales/es/config/validation.json
+++ b/web/public/locales/es/config/validation.json
@@ -19,7 +19,8 @@
"ffmpeg": {
"inputs": {
"rolesUnique": "Cada rol solo puede asignarse a un flujo de entrada.",
- "detectRequired": "Al menos un flujo de entrada debe tener asignado el rol 'detect'."
+ "detectRequired": "Al menos un flujo de entrada debe tener asignado el rol 'detect'.",
+ "hwaccelDetectOnly": "Solo el flujo de entrada con la función \"detect\" puede definir argumentos de aceleración por hardware."
}
},
"anyOf": "Debe coincidir con al menos uno de los esquemas permitidos",
@@ -27,5 +28,8 @@
"header_map": {
"roleHeaderRequired": "Se requiere el encabezado de rol cuando hay mapeos de roles configurados."
}
+ },
+ "detect": {
+ "dimensionMustBeEven": "Debe ser un número par."
}
}
diff --git a/web/public/locales/es/objects.json b/web/public/locales/es/objects.json
index 0fd02208a6..94adda5cb9 100644
--- a/web/public/locales/es/objects.json
+++ b/web/public/locales/es/objects.json
@@ -47,7 +47,7 @@
"carrot": "Zanahoria",
"hot_dog": "Perrito caliente",
"pizza": "Pizza",
- "donut": "Donut",
+ "donut": "Rosquilla",
"chair": "Silla",
"couch": "Sofá",
"potted_plant": "Planta en maceta",
@@ -116,5 +116,15 @@
"animal": "Animal",
"postnord": "PostNord",
"usps": "USPS",
- "gls": "GLS"
+ "gls": "GLS",
+ "canada_post": "Canada Post",
+ "royal_mail": "Royal Mail",
+ "school_bus": "Autobús escolar",
+ "skunk": "Mofeta",
+ "kangaroo": "Canguro",
+ "baby": "Bebé",
+ "baby_stroller": "Cochecito de bebé",
+ "rickshaw": "Rickshaw",
+ "Rodent": "Roedor",
+ "rodent": "Roedor"
}
diff --git a/web/public/locales/es/views/chat.json b/web/public/locales/es/views/chat.json
new file mode 100644
index 0000000000..7fe10c75be
--- /dev/null
+++ b/web/public/locales/es/views/chat.json
@@ -0,0 +1,72 @@
+{
+ "documentTitle": "Chat - Frigate",
+ "title": "Frigate Chat",
+ "subtitle": "Tu asistente de IA para la gestión de cámaras y análisis",
+ "placeholder": "Pregunta cualquier cosa...",
+ "error": "Algo salió mal. Por favor, inténtalo de nuevo.",
+ "processing": "Procesando...",
+ "toolsUsed": "Usado: {{tools}}",
+ "showTools": "Mostrar herramientas ({{count}})",
+ "hideTools": "Ocultar herramientas",
+ "call": "Llamar",
+ "result": "Resultado",
+ "arguments": "Argumentos:",
+ "response": "Respuesta:",
+ "attachment_chip_label": "{{label}} en {{camera}}",
+ "attachment_chip_remove": "Eliminar adjunto",
+ "open_in_explore": "Abrir en Explorar",
+ "attach_event_aria": "Adjuntar evento {{eventId}}",
+ "attachment_picker_paste_label": "O pega el ID del evento",
+ "attachment_picker_attach": "Adjuntar",
+ "attachment_picker_placeholder": "Adjuntar un evento",
+ "quick_reply_find_similar": "Buscar avistamientos similares",
+ "quick_reply_tell_me_more": "Cuéntame más sobre esto",
+ "quick_reply_when_else": "¿Cuándo más se vio?",
+ "quick_reply_find_similar_text": "Buscar avistamientos similares a este.",
+ "quick_reply_tell_me_more_text": "Cuéntame más sobre este.",
+ "quick_reply_when_else_text": "¿Cuándo más se vio esto?",
+ "anchor": "Referencia",
+ "similarity_score": "Similitud",
+ "no_similar_objects_found": "No se encontraron objetos similares.",
+ "semantic_search_required": "La búsqueda semántica debe estar activada para encontrar objetos similares.",
+ "send": "Enviar",
+ "suggested_requests": "Prueba preguntando:",
+ "starting_requests": {
+ "show_recent_events": "Mostrar eventos recientes",
+ "show_camera_status": "Mostrar estado de la cámara",
+ "recap": "¿Qué ha pasado mientras estaba fuera?",
+ "watch_camera": "Vigilar una cámara en busca de actividad"
+ },
+ "starting_requests_prompts": {
+ "show_recent_events": "Muéstrame los eventos recientes de la última hora",
+ "show_camera_status": "¿Cuál es el estado actual de mis cámaras?",
+ "recap": "¿Qué ha pasado mientras estaba fuera?",
+ "watch_camera": "Vigila la puerta principal y avísame si aparece alguien"
+ },
+ "new_chat": "Nuevo chat",
+ "settings": {
+ "title": "Ajustes del chat",
+ "show_stats": {
+ "title": "Mostrar estadísticas",
+ "desc": "Mostrar la velocidad de generación y el tamaño del contexto en las respuestas del chat.",
+ "while_generating": "Durante la generación",
+ "always": "Siempre"
+ },
+ "auto_scroll": {
+ "title": "Desplazamiento automático",
+ "desc": "Seguir los mensajes nuevos a medida que llegan."
+ }
+ },
+ "stats": {
+ "context": "{{tokens}} tokens",
+ "tokens_per_second": "{{rate}} t/s"
+ },
+ "reasoning": {
+ "active": "Razonando…",
+ "show": "Mostrar razonamiento",
+ "hide": "Ocultar razonamiento"
+ },
+ "thinking": {
+ "toggle": "Alternar razonamiento"
+ }
+}
diff --git a/web/public/locales/es/views/classificationModel.json b/web/public/locales/es/views/classificationModel.json
index ee6fc5ed10..93bc070b77 100644
--- a/web/public/locales/es/views/classificationModel.json
+++ b/web/public/locales/es/views/classificationModel.json
@@ -12,12 +12,12 @@
},
"toast": {
"success": {
- "deletedCategory_one": "Clase Borrada",
- "deletedCategory_many": "",
- "deletedCategory_other": "",
- "deletedImage_one": "Imágenes Borradas",
- "deletedImage_many": "",
- "deletedImage_other": "",
+ "deletedCategory_one": "Se eliminó {{count}} clase",
+ "deletedCategory_many": "Se eliminaron {{count}} clases",
+ "deletedCategory_other": "Se eliminaron {{count}} clases",
+ "deletedImage_one": "Se eliminó {{count}} imagen",
+ "deletedImage_many": "Se eliminaron {{count}} imágenes",
+ "deletedImage_other": "Se eliminaron {{count}} imágenes",
"deletedModel_one": "Borrado con éxito {{count}} modelo",
"deletedModel_many": "Borrados con éxito {{count}} modelos",
"deletedModel_other": "Borrados con éxito {{count}} modelos",
@@ -36,14 +36,14 @@
"trainingFailed": "El entrenamiento del modelo ha fallado. Revisa los registros de Frigate para más detalles.",
"updateModelFailed": "Fallo al actualizar modelo: {{errorMessage}}",
"trainingFailedToStart": "No se pudo iniciar el entrenamiento del modelo: {{errorMessage}}",
- "renameCategoryFailed": "Falló el renombrado de la clase: {{errorMessage}}",
+ "renameCategoryFailed": "Fallo al renombrar la clase:{{errorMessage}}",
"reclassifyFailed": "Error al reclasificar la imagen: {{errorMessage}}"
}
},
"deleteCategory": {
"title": "Borrar Clase",
"desc": "¿Esta seguro de que quiere borrar la clase {{name}}? Esto borrará permanentemente todas las imágenes asociadas y requerirá reentrenar el modelo.",
- "minClassesTitle": "No se puede Borrar la Clase",
+ "minClassesTitle": "No se puede borrar la clase",
"minClassesDesc": "Un modelo de clasificación debe tener al menos 2 clases. Añade otra clase antes de borrar esta."
},
"deleteModel": {
@@ -66,9 +66,9 @@
"noNewImages": "No hay imágenes nuevas para entrenar. Clasifica antes más imágenes del conjunto de datos."
},
"details": {
- "scoreInfo": "La puntuación representa la confianza media de clasificación en todas las detecciones de este objeto.",
+ "scoreInfo": "La puntuación representa la confianza promedio de la clasificación en todas las detecciones de este objeto.",
"unknown": "Desconocido",
- "none": "Ninguna"
+ "none": "Ninguno"
},
"categorizeImage": "Clasificar Imagen",
"menu": {
@@ -146,7 +146,7 @@
"generateSuccess": "Imágenes de ejemplo generadas correctamente",
"missingStatesWarning": {
"title": "Faltan Ejemplos de Estado",
- "description": "Se recomienda seleccionar ejemplos para todos los estados para obtener mejores resultados. Puede continuar sin seleccionar todos los estados, pero el modelo no se entrenará hasta que todos los estados tengan imágenes. Después de continuar, use la vista \"Clasificaciones recientes\" para clasificar las imágenes de los estados faltantes y luego entrene el modelo."
+ "description": "No todas las clases tienen ejemplos. Prueba a generar nuevos ejemplos para encontrar la clase que falta, o continúa y usa la vista de Clasificaciones recientes para añadir imágenes más tarde."
},
"allImagesRequired_one": "Por favor clasifique todas las imágenes. Queda {{count}} imagen.",
"allImagesRequired_many": "Por favor clasifique todas las imágenes. Quedan {{count}} imágenes.",
@@ -166,7 +166,7 @@
"desc_other": "¿Está seguro de que quiere eliminar {{count}} imágenes de {{dataset}}? Esta acción no puede ser deshecha y requerirá reentrenar el modelo."
},
"deleteTrainImages": {
- "title": "Borrar Imágenes de Entrenamiento",
+ "title": "Borrar imágenes de entrenamiento",
"desc_one": "¿Está seguro de que quiere eliminar {{count}} imagen? Esta acción no puede ser deshecha.",
"desc_many": "¿Está seguro de que quiere eliminar {{count}} imágenes? Esta acción no puede ser deshecha.",
"desc_other": "¿Está seguro de que quiere eliminar {{count}} imágenes? Esta acción no puede ser deshecha."
diff --git a/web/public/locales/es/views/configEditor.json b/web/public/locales/es/views/configEditor.json
index 265e7ec8e8..8f109c2ce1 100644
--- a/web/public/locales/es/views/configEditor.json
+++ b/web/public/locales/es/views/configEditor.json
@@ -14,5 +14,5 @@
"documentTitle": "Editor de Configuración - Frigate",
"confirm": "¿Salir sin guardar?",
"safeConfigEditor": "Editor de Configuración (Modo Seguro)",
- "safeModeDescription": "Frigate esta en modo seguro debido a un error en la validación de la configuración."
+ "safeModeDescription": "Frigate se encuentra en modo seguro debido a un error de validación en la configuración."
}
diff --git a/web/public/locales/es/views/events.json b/web/public/locales/es/views/events.json
index f2bdab0e99..7c2dd8b362 100644
--- a/web/public/locales/es/views/events.json
+++ b/web/public/locales/es/views/events.json
@@ -32,7 +32,9 @@
},
"camera": "Cámara",
"recordings": {
- "documentTitle": "Grabaciones - Frigate"
+ "documentTitle": "Grabaciones - Frigate",
+ "invalidSharedLink": "No se puede abrir el enlace de la grabación con marca de tiempo debido a un error de análisis.",
+ "invalidSharedCamera": "No se puede abrir el enlace de la grabación con marca de tiempo debido a una cámara desconocida o no autorizada."
},
"calendarFilter": {
"last24Hours": "Últimas 24 horas"
@@ -66,5 +68,28 @@
"select_all": "Todas",
"normalActivity": "Normal",
"needsReview": "Necesita revisión",
- "securityConcern": "Aviso de seguridad"
+ "securityConcern": "Aviso de seguridad",
+ "motionSearch": {
+ "menuItem": "Búsqueda de movimiento",
+ "openMenu": "Opciones de cámara"
+ },
+ "motionPreviews": {
+ "menuItem": "Ver vistas previas de movimiento",
+ "title": "Vistas previas de movimiento: {{camera}}",
+ "mobileSettingsTitle": "Ajustes de vistas previas de movimiento",
+ "mobileSettingsDesc": "Ajusta la velocidad de reproducción y el atenuado, y elige una fecha para revisar clips solo de movimiento.",
+ "dim": "Atenuar",
+ "dimAria": "Ajustar intensidad de atenuado",
+ "dimDesc": "Aumenta el atenuado para mejorar la visibilidad de las áreas con movimiento.",
+ "speed": "Velocidad",
+ "speedAria": "Seleccionar velocidad de reproducción de las vistas previas",
+ "speedDesc": "Elige la velocidad a la que se reproducen los clips de vista previa.",
+ "back": "Atrás",
+ "empty": "No hay vistas previas disponibles",
+ "noPreview": "Vista previa no disponible",
+ "seekAria": "Mover el reproductor de {{camera}} a {{time}}",
+ "filter": "Filtrar",
+ "filterDesc": "Selecciona áreas para mostrar solo clips con movimiento en esas regiones.",
+ "filterClear": "Limpiar"
+ }
}
diff --git a/web/public/locales/es/views/explore.json b/web/public/locales/es/views/explore.json
index ded5ca91fb..2f993e647e 100644
--- a/web/public/locales/es/views/explore.json
+++ b/web/public/locales/es/views/explore.json
@@ -208,7 +208,7 @@
},
"addTrigger": {
"label": "Añadir disparador",
- "aria": "Añadir disparador para el objeto seguido"
+ "aria": "Añadir disparador para este objeto rastreado"
},
"downloadCleanSnapshot": {
"label": "Descargue instantánea limpia",
@@ -226,6 +226,10 @@
},
"more": {
"aria": "Más"
+ },
+ "debugReplay": {
+ "label": "Reproducción de depuración",
+ "aria": "Ver este objeto rastreado en la reproducción de depuración"
}
},
"dialog": {
@@ -269,8 +273,8 @@
"count": "{{first}} de {{second}}",
"lifecycleItemDesc": {
"visible": "{{label}} detectado",
- "active": "{{label}} ha sido activado/a",
- "stationary": "{{label}} se volvió estacionaria",
+ "active": "{{label}} está activo",
+ "stationary": "{{label}} se volvió estacionario",
"attribute": {
"faceOrLicense_plate": "{{attribute}} detectado para {{label}}",
"other": "{{label}} reconocido como {{attribute}}"
@@ -282,7 +286,10 @@
"zones": "Zonas",
"area": "Área",
"score": "Puntuación",
- "ratio": "Ratio(proporción)"
+ "ratio": "Ratio(proporción)",
+ "computedScore": "Puntuación calculada",
+ "topScore": "Puntuación más alta",
+ "toggleAdvancedScores": "Alternar puntuaciones avanzadas"
},
"entered_zone": "{{label}} ha entrado en {{zones}}"
},
diff --git a/web/public/locales/es/views/exports.json b/web/public/locales/es/views/exports.json
index 1099d45c89..0aec0eeca7 100644
--- a/web/public/locales/es/views/exports.json
+++ b/web/public/locales/es/views/exports.json
@@ -13,7 +13,9 @@
"toast": {
"error": {
"renameExportFailed": "No se pudo renombrar la exportación: {{errorMessage}}",
- "assignCaseFailed": "Fallo en la actualización de la asignación de caso: {{errorMessage}}"
+ "assignCaseFailed": "No se pudo actualizar la asignación al caso: {{errorMessage}}",
+ "caseSaveFailed": "No se pudo guardar el caso: {{errorMessage}}",
+ "caseDeleteFailed": "No se pudo eliminar el caso: {{errorMessage}}"
}
},
"deleteExport.desc": "¿Estás seguro de que quieres eliminar {{exportName}}?",
@@ -22,17 +24,105 @@
"downloadVideo": "Descargar video",
"editName": "Editar nombre",
"deleteExport": "Eliminar exportación",
- "assignToCase": "Añadir al caso"
+ "assignToCase": "Añadir al caso",
+ "removeFromCase": "Eliminar del caso"
},
"headings": {
"cases": "Casos",
- "uncategorizedExports": "Exportaciones sin categorizar"
+ "uncategorizedExports": "Exportaciones sin categorización"
},
"caseDialog": {
"title": "Añadir al caso",
"newCaseOption": "Crear nuevo caso",
"nameLabel": "Nombre del caso",
"description": "Elige un caso existente o crea uno nuevo.",
- "selectLabel": "Caso"
+ "selectLabel": "Caso",
+ "descriptionLabel": "Descripción"
+ },
+ "toolbar": {
+ "addExport": "Agregar exportación",
+ "newCase": "Nuevo caso",
+ "editCase": "Editar caso",
+ "deleteCase": "Eliminar caso"
+ },
+ "deleteCase": {
+ "label": "Eliminar caso",
+ "desc": "¿Estás seguro de que quieres eliminar {{caseName}}?",
+ "descKeepExports": "Las exportaciones seguirán disponibles como exportaciones sin categoría.",
+ "descDeleteExports": "Todas las exportaciones de este caso se eliminarán de forma permanente.",
+ "deleteExports": "Eliminar también las exportaciones"
+ },
+ "caseCard": {
+ "emptyCase": "Aún no hay exportaciones"
+ },
+ "jobCard": {
+ "defaultName": "Exportación de {{camera}}",
+ "queued": "En cola",
+ "running": "En ejecución",
+ "preparing": "Preparando",
+ "copying": "Copiando",
+ "encoding": "Codificando",
+ "encodingRetry": "Codificando (reintento)",
+ "finalizing": "Finalizando"
+ },
+ "caseView": {
+ "noDescription": "Sin descripción",
+ "createdAt": "Creado {{value}}",
+ "exportCount_one": "1 exportación",
+ "exportCount_other": "{{count}} exportaciones",
+ "cameraCount_one": "1 cámara",
+ "cameraCount_other": "{{count}} cámaras",
+ "showMore": "Mostrar más",
+ "showLess": "Mostrar menos",
+ "emptyTitle": "Este caso está vacío",
+ "emptyDescription": "Añade exportaciones existentes sin categorizar para mantener el caso organizado.",
+ "emptyDescriptionNoExports": "Todavía no hay exportaciones sin categorizar disponibles para añadir."
+ },
+ "caseEditor": {
+ "createTitle": "Crear caso",
+ "editTitle": "Editar caso",
+ "namePlaceholder": "Nombre del caso",
+ "descriptionPlaceholder": "Añade notas o contexto para este caso"
+ },
+ "addExportDialog": {
+ "title": "Añadir exportación a {{caseName}}",
+ "searchPlaceholder": "Buscar exportaciones sin categorizar",
+ "empty": "Ninguna exportación sin categorizar coincide con esta búsqueda.",
+ "addButton_one": "Añadir 1 exportación",
+ "addButton_other": "Añadir {{count}} exportaciones",
+ "adding": "Añadiendo..."
+ },
+ "selected_one": "{{count}} seleccionados",
+ "selected_other": "{{count}} seleccionados",
+ "bulkActions": {
+ "addToCase": "Añadir al caso",
+ "moveToCase": "Mover al caso",
+ "removeFromCase": "Eliminar del caso",
+ "delete": "Eliminar",
+ "deleteNow": "Eliminar ahora"
+ },
+ "bulkDelete": {
+ "title": "Eliminar exportaciones",
+ "desc_one": "¿Seguro que quieres eliminar {{count}} exportación?",
+ "desc_other": "¿Seguro que quieres eliminar {{count}} exportaciones?"
+ },
+ "bulkRemoveFromCase": {
+ "title": "Eliminar del caso",
+ "desc_one": "¿Eliminar {{count}} exportación de este caso?",
+ "desc_other": "¿Eliminar {{count}} exportaciones de este caso?",
+ "descKeepExports": "Las exportaciones se moverán a sin categorizar.",
+ "descDeleteExports": "Las exportaciones se eliminarán permanentemente.",
+ "deleteExports": "Eliminar exportaciones en su lugar"
+ },
+ "bulkToast": {
+ "success": {
+ "delete": "Exportaciones eliminadas correctamente",
+ "reassign": "Asignación de caso actualizada correctamente",
+ "remove": "Exportaciones eliminadas del caso correctamente"
+ },
+ "error": {
+ "deleteFailed": "No se pudieron eliminar las exportaciones: {{errorMessage}}",
+ "reassignFailed": "No se pudo actualizar la asignación del caso: {{errorMessage}}"
+ }
}
}
diff --git a/web/public/locales/es/views/faceLibrary.json b/web/public/locales/es/views/faceLibrary.json
index f923082dac..8014830fae 100644
--- a/web/public/locales/es/views/faceLibrary.json
+++ b/web/public/locales/es/views/faceLibrary.json
@@ -30,7 +30,11 @@
"title": "Reconocimientos Recientes",
"aria": "Seleccionar reconocimientos recientes",
"empty": "No hay intentos recientes de reconocimiento facial",
- "titleShort": "Reciente"
+ "titleShort": "Reciente",
+ "emptyNoLibrary": {
+ "title": "Subir una cara",
+ "description": "Debes añadir al menos una cara a la biblioteca para que el reconocimiento facial funcione."
+ }
},
"selectItem": "Seleccionar {{item}}",
"selectFace": "Seleccionar rostro",
diff --git a/web/public/locales/es/views/live.json b/web/public/locales/es/views/live.json
index fa473384a2..877e3ef63b 100644
--- a/web/public/locales/es/views/live.json
+++ b/web/public/locales/es/views/live.json
@@ -17,7 +17,7 @@
"label": "Haz clic en el marco para centrar la cámara",
"enable": "Habilitar clic para mover",
"disable": "Deshabilitar clic para mover",
- "enableWithZoom": "Activar clic para mover / arrastrar para hacer zoom"
+ "enableWithZoom": "Habilitar clic para mover / arrastrar para aumentar"
},
"up": {
"label": "Mover la cámara PTZ hacia arriba"
@@ -57,7 +57,9 @@
},
"camera": {
"enable": "Habilitar cámara",
- "disable": "Deshabilitar cámara"
+ "disable": "Deshabilitar cámara",
+ "turnOn": "Encender cámara",
+ "turnOff": "Apagar cámara"
},
"muteCameras": {
"enable": "Silenciar todas las cámaras",
@@ -69,7 +71,8 @@
},
"recording": {
"enable": "Habilitar grabación",
- "disable": "Deshabilitar grabación"
+ "disable": "Deshabilitar grabación",
+ "disabledInConfig": "La grabación debe activarse primero en Ajustes para esta cámara."
},
"snapshots": {
"enable": "Habilitar capturas de pantalla",
@@ -150,7 +153,8 @@
"snapshots": "Capturas de pantalla",
"autotracking": "Seguimiento automático",
"cameraEnabled": "Cámara habilitada",
- "transcription": "Transcripción de Audio"
+ "transcription": "Transcripción de Audio",
+ "camera": "Cámara"
},
"history": {
"label": "Mostrar grabaciones históricas"
diff --git a/web/public/locales/es/views/motionSearch.json b/web/public/locales/es/views/motionSearch.json
new file mode 100644
index 0000000000..2d1103aeb4
--- /dev/null
+++ b/web/public/locales/es/views/motionSearch.json
@@ -0,0 +1,82 @@
+{
+ "documentTitle": "Búsqueda por movimiento - Frigate",
+ "title": "Búsqueda por movimiento",
+ "description": "Dibuja un polígono para definir la región de interés y especifica un intervalo de tiempo para buscar cambios de movimiento dentro de esa región.",
+ "selectCamera": "Búsqueda por movimiento se está cargando",
+ "startSearch": "Iniciar búsqueda",
+ "searchStarted": "Búsqueda iniciada",
+ "searchCancelled": "Búsqueda cancelada",
+ "cancelSearch": "Cancelar",
+ "searching": "Búsqueda en progreso.",
+ "searchComplete": "Búsqueda completada",
+ "noResultsYet": "Ejecuta una búsqueda para encontrar cambios de movimiento en la región seleccionada",
+ "noChangesFound": "No se detectaron cambios de píxeles en la región seleccionada",
+ "changesFound_one": "Encontrado {{count}} cambio de movimiento",
+ "changesFound_many": "Encontrados {{count}} cambios de movimiento",
+ "changesFound_other": "Encontrados {{count}} cambios de movimiento",
+ "framesProcessed": "{{count}} fotogramas procesados",
+ "jumpToTime": "Saltar a este tiempo",
+ "results": "Resultados",
+ "showSegmentHeatmap": "Mapa de calor",
+ "newSearch": "Nueva búsqueda",
+ "clearResults": "Borrar resultados",
+ "clearROI": "Borrar polígono",
+ "polygonControls": {
+ "points_one": "{{count}} punto",
+ "points_many": "{{count}} puntos",
+ "points_other": "{{count}} puntos",
+ "undo": "Deshacer el último punto",
+ "reset": "Restablecer polígono",
+ "drawMode": "Dibujar",
+ "moveMode": "Mover"
+ },
+ "motionHeatmapLabel": "Mapa de calor de movimiento",
+ "dialog": {
+ "title": "Búsqueda de movimiento",
+ "cameraLabel": "Cámara",
+ "previewAlt": "Vista previa de la cámara {{camera}}"
+ },
+ "timeRange": {
+ "title": "Rango de búsqueda",
+ "start": "Hora de inicio",
+ "end": "Hora de finalización"
+ },
+ "settings": {
+ "title": "Ajustes de búsqueda",
+ "parallelMode": "Modo paralelo",
+ "parallelModeDesc": "Analiza varios intervalos de grabación al mismo tiempo (más rápido, pero utiliza más recursos de decodificación)",
+ "threshold": "Umbral de sensibilidad",
+ "thresholdDesc": "Los valores más bajos detectan cambios más pequeños (1-255)",
+ "minArea": "Área mínima de cambio",
+ "minAreaDesc": "Tamaño mínimo de una única región en movimiento, expresado como porcentaje de la región de interés",
+ "frameSkip": "Salto de fotogramas",
+ "frameSkipDesc": "Procesa cada N fotogramas. Establécelo según la tasa de FPS de tu cámara para procesar un fotograma por segundo (p. ej., 5 para una cámara de 5 FPS, 30 para una cámara de 30 FPS). Los valores más altos serán más rápidos, pero pueden omitir eventos de movimiento breves.",
+ "maxResults": "Resultados máximos",
+ "maxResultsDesc": "Detener después de esta cantidad de marcas de tiempo coincidentes"
+ },
+ "errors": {
+ "noCamera": "Selecciona una cámara",
+ "noROI": "Dibuja una región de interés",
+ "noTimeRange": "Selecciona un rango de tiempo",
+ "invalidTimeRange": "La hora de fin debe ser posterior a la hora de inicio",
+ "searchFailed": "La búsqueda falló: {{message}}",
+ "polygonTooSmall": "El polígono debe tener al menos 3 puntos",
+ "unknown": "Error desconocido"
+ },
+ "changePercentage": "{{percentage}}% cambiado",
+ "metrics": {
+ "title": "Métricas de búsqueda",
+ "segmentsScanned": "Segmentos analizados",
+ "segmentsProcessed": "Procesado",
+ "segmentsSkippedInactive": "Omitido (sin actividad)",
+ "segmentsSkippedHeatmap": "Omitido (sin superposición de ROI)",
+ "fallbackFullRange": "Análisis completo de respaldo",
+ "framesDecoded": "Fotogramas decodificados",
+ "wallTime": "Tiempo de búsqueda",
+ "segmentErrors": "Errores de segmento",
+ "seconds": "{{seconds}} s",
+ "minutesSeconds": "{{minutes}}m {{seconds}}s",
+ "scanSummary": "{{segments}} segmentos · {{time}}"
+ },
+ "scanning": "Escaneando {{time}}"
+}
diff --git a/web/public/locales/es/views/replay.json b/web/public/locales/es/views/replay.json
new file mode 100644
index 0000000000..f1b7a84f97
--- /dev/null
+++ b/web/public/locales/es/views/replay.json
@@ -0,0 +1,59 @@
+{
+ "title": "Depuración de reproducción",
+ "description": "Reproducir grabaciones de cámara para depuración. La lista de objetos muestra un resumen con retraso temporal de los objetos detectados y la pestaña Mensajes muestra un flujo de los mensajes internos de Frigate de la grabación reproducida.",
+ "websocket_messages": "Mensajes",
+ "dialog": {
+ "title": "Iniciar depuración de reproducción",
+ "description": "Crea una cámara de reproducción temporal que reproduzca en bucle imágenes históricas para depurar problemas de detección y seguimiento de objetos. La cámara de reproducción tendrá la misma configuración de detección que la cámara de origen. Elige un intervalo de tiempo para comenzar.",
+ "camera": "Cámara de origen",
+ "timeRange": "Intervalo de tiempo",
+ "preset": {
+ "1m": "Último 1 minuto",
+ "5m": "Últimos 5 minutos",
+ "timeline": "Desde la línea de tiempo",
+ "custom": "Personalizado"
+ },
+ "startButton": "Iniciar reproducción",
+ "selectFromTimeline": "Seleccionar",
+ "starting": "Iniciando reproducción...",
+ "startLabel": "Iniciar",
+ "endLabel": "Fin",
+ "toast": {
+ "error": "No se pudo iniciar la reproducción de depuración: {{error}}",
+ "alreadyActive": "Ya hay una sesión de reproducción activa",
+ "stopError": "No se pudo detener la reproducción de depuración: {{error}}",
+ "goToReplay": "Ir a la reproducción"
+ }
+ },
+ "page": {
+ "noSession": "No hay ninguna sesión activa de reproducción de depuración",
+ "noSessionDesc": "Inicia una reproducción de depuración desde la vista Historial haciendo clic en el botón Acciones de la barra de herramientas y seleccionando Reproducción de depuración.",
+ "goToRecordings": "Ir al historial",
+ "preparingClip": "Preparando clip…",
+ "preparingClipDesc": "Frigate está uniendo las grabaciones del intervalo de tiempo seleccionado. Esto puede tardar un minuto en intervalos más largos.",
+ "startingCamera": "Iniciando reproducción de depuración…",
+ "startError": {
+ "title": "No se pudo iniciar la reproducción de depuración",
+ "back": "Volver al historial"
+ },
+ "sourceCamera": "Cámara de origen",
+ "replayCamera": "Cámara de reproducción",
+ "initializingReplay": "Inicializando reproducción de depuración…",
+ "stoppingReplay": "Deteniendo repetición de depuración...",
+ "stopReplay": "Detener repetición",
+ "confirmStop": {
+ "title": "¿Detener repetición de depuración?",
+ "description": "Esto detendrá la sesión y eliminará todos los datos temporales. ¿Estás seguro?",
+ "confirm": "Detener repetición",
+ "cancel": "Cancelar"
+ },
+ "activity": "Actividad",
+ "objects": "Lista de objetos",
+ "audioDetections": "Detecciones de audio",
+ "noActivity": "No se detectó actividad",
+ "activeTracking": "Seguimiento activo",
+ "noActiveTracking": "No hay seguimiento activo",
+ "configuration": "Configuración",
+ "configurationDesc": "Ajusta con precisión la detección de movimiento y los ajustes de seguimiento de objetos para la cámara de repetición de depuración. No se guardará ningún cambio en el archivo de configuración de Frigate."
+ }
+}
diff --git a/web/public/locales/es/views/settings.json b/web/public/locales/es/views/settings.json
index c6157a7507..f5e98ebec3 100644
--- a/web/public/locales/es/views/settings.json
+++ b/web/public/locales/es/views/settings.json
@@ -16,12 +16,13 @@
"globalConfig": "Configuración Global - Frigate",
"cameraConfig": "Configuración de Cámara - Frigate",
"maintenance": "Mantenimiento - Frigate",
- "profiles": "Perfiles - Frigate"
+ "profiles": "Perfiles - Frigate",
+ "detectorsAndModel": "Detectores y modelo - Frigate"
},
"menu": {
"cameras": "Configuración de Cámara",
"debug": "Depuración",
- "ui": "Interfaz de usuario",
+ "ui": "Interfaz de Usuario",
"classification": "Clasificación",
"motionTuner": "Ajuste de movimiento",
"masksAndZones": "Máscaras / Zonas",
@@ -31,11 +32,68 @@
"enrichments": "Análisis avanzado",
"triggers": "Disparadores",
"roles": "Rols",
- "cameraManagement": "Administración",
+ "cameraManagement": "Gestión de cámaras",
"cameraReview": "Revisar",
"general": "General",
"globalConfig": "Configuración Global",
- "system": "Sistema"
+ "system": "Sistema",
+ "integrations": "Integraciones",
+ "uiSettings": "Configuración de Interfaz de Usuario",
+ "profiles": "Perfiles",
+ "globalDetect": "Detección de Objetos",
+ "globalRecording": "Grabación",
+ "globalSnapshots": "Instantáneas",
+ "globalFfmpeg": "arguments,Introduce",
+ "globalMotion": "Detección de Movimiento",
+ "globalObjects": "Objetos",
+ "globalReview": "Revisión",
+ "globalAudioEvents": "Eventos de Audio",
+ "globalLivePlayback": "Reproducción en Vivo",
+ "globalTimestampStyle": "Estilo de Marca de Tiempo",
+ "systemDatabase": "Base de Datos",
+ "systemAuthentication": "Autenticación",
+ "systemTls": "TLS",
+ "systemNetworking": "Red",
+ "systemProxy": "Proxy",
+ "systemUi": "Interfaz",
+ "systemLogging": "Registro",
+ "systemEnvironmentVariables": "Variables de entorno",
+ "systemTelemetry": "Telemetría",
+ "systemBirdseye": "Birdseye",
+ "systemFfmpeg": "FFmpeg",
+ "systemDetectorHardware": "Hardware del detector",
+ "systemDetectionModel": "Modelo de detección",
+ "systemMqtt": "MQTT",
+ "systemGo2rtcStreams": "Flujos go2rtc",
+ "integrationSemanticSearch": "Búsqueda semántica",
+ "integrationGenerativeAi": "IA generativa",
+ "integrationFaceRecognition": "Reconocimiento facial",
+ "integrationLpr": "Reconocimiento de matrículas",
+ "integrationObjectClassification": "Clasificación de objetos",
+ "integrationAudioTranscription": "Transcripción de audio",
+ "cameraDetect": "Detección de objetos",
+ "cameraFfmpeg": "Flujos (FFmpeg)",
+ "cameraRecording": "Grabación",
+ "cameraSnapshots": "Instantáneas",
+ "cameraMotion": "Detección de movimiento",
+ "cameraObjects": "Objetos",
+ "cameraConfigReview": "Revisión",
+ "cameraAudioEvents": "Detección de audio",
+ "cameraAudioTranscription": "Transcripción de audio",
+ "cameraNotifications": "Notificaciones",
+ "cameraLivePlayback": "Reproducción en directo",
+ "cameraBirdseye": "Birdseye",
+ "cameraFaceRecognition": "Reconocimiento facial",
+ "cameraLpr": "Reconocimiento de matrículas",
+ "cameraMqttConfig": "MQTT",
+ "cameraOnvif": "ONVIF",
+ "cameraUi": "Interfaz de cámara",
+ "cameraTimestampStyle": "Estilo de marca de tiempo",
+ "cameraMqtt": "MQTT de cámara",
+ "maintenance": "Mantenimiento",
+ "mediaSync": "Sincronización de medios",
+ "regionGrid": "Cuadrícula de regiones",
+ "systemDetectorsAndModel": "Detectores y modelo"
},
"dialog": {
"unsavedChanges": {
@@ -44,7 +102,7 @@
}
},
"cameraSetting": {
- "camera": "Cámara",
+ "camera": "Overrides,Sobrescrituras",
"noCamera": "Sin cámara"
},
"general": {
@@ -288,6 +346,10 @@
"zone": "zona",
"motion_mask": "máscara de movimiento",
"object_mask": "máscara de objeto"
+ },
+ "revertOverride": {
+ "title": "Revertir a la configuración base",
+ "desc": "Esto eliminará la sobrescritura del perfil para {{type}} {{name}} y revertirá a la configuración base."
}
},
"speed": {
@@ -299,6 +361,12 @@
"error": {
"mustNotBeEmpty": "El nombre no puede estar vacío."
}
+ },
+ "id": {
+ "error": {
+ "mustNotBeEmpty": "El ID no puede estar vacío.",
+ "alreadyExists": "Ya existe una máscara con este ID para esta cámara."
+ }
}
},
"zones": {
@@ -353,6 +421,10 @@
"allObjects": "Todos los objetos",
"toast": {
"success": "La zona ({{zoneName}}) ha sido guardada."
+ },
+ "enabled": {
+ "description": "Indica si esta zona está activa y habilitada en la configuración. Si está deshabilitado, no puede ser habilitado por MQTT. Las zonas deshabilitadas se ignoran durante la ejecución.",
+ "title": "Habilitado"
}
},
"toast": {
@@ -393,7 +465,13 @@
"documentTitle": "Editar Máscara de Movimiento - Frigate",
"point_one": "{{count}} punto",
"point_many": "{{count}} puntos",
- "point_other": "{{count}} puntos"
+ "point_other": "{{count}} puntos",
+ "defaultName": "Máscara de movimiento {{number}}",
+ "name": {
+ "title": "Nombre",
+ "description": "Un nombre descriptivo opcional para esta máscara de movimiento.",
+ "placeholder": "Introduce un nombre..."
+ }
},
"objectMasks": {
"label": "Máscaras de Objetos",
@@ -419,11 +497,26 @@
"point_one": "{{count}} punto",
"point_many": "{{count}} puntos",
"point_other": "{{count}} puntos",
- "clickDrawPolygon": "Haz clic para dibujar un polígono en la imagen."
+ "clickDrawPolygon": "Haz clic para dibujar un polígono en la imagen.",
+ "name": {
+ "title": "Nombre",
+ "description": "Un nombre descriptivo opcional para esta máscara de objeto.",
+ "placeholder": "Introduce un nombre..."
+ }
},
"restart_required": "Es necesario reiniciar (se han cambiado las máscaras/zonas)",
"motionMaskLabel": "Máscara de movimiento {{number}}",
- "objectMaskLabel": "Máscara de objeto {{number}}"
+ "objectMaskLabel": "Máscara de objeto {{number}}",
+ "disabledInConfig": "El elemento está deshabilitado en el archivo de configuración",
+ "addDisabledProfile": "Añádelo primero a la configuración base y luego sobrescríbelo en el perfil",
+ "profileBase": "(base)",
+ "profileOverride": "(sobrescritura)",
+ "masks": {
+ "enabled": {
+ "title": "Habilitado",
+ "description": "Indica si esta máscara está habilitada en el archivo de configuración. Si está deshabilitada, no se puede habilitar mediante MQTT. Las máscaras deshabilitadas se ignoran en tiempo de ejecución."
+ }
+ }
},
"motionDetectionTuner": {
"title": "Sintonizador de Detección de Movimiento",
@@ -634,7 +727,8 @@
"notificationUnavailable": {
"title": "Notificaciones no disponibles",
"documentation": "Leer la documentación",
- "desc": "Las notificaciones push web requieren un contexto seguro (https://…). Esto es una limitación del navegador. Accede a Frigate de forma segura para usar las notificaciones."
+ "desc": "Las notificaciones push web requieren un contexto seguro (https://…). Esto es una limitación del navegador. Accede a Frigate de forma segura para usar las notificaciones.",
+ "descPwa": "En iOS, las notificaciones push web solo están disponibles cuando Frigate está instalado en la pantalla de inicio. Abre el menú Compartir , selecciona Añadir a la pantalla de inicio y, a continuación, abre Frigate desde el nuevo icono para registrar este dispositivo para las notificaciones."
},
"globalSettings": {
"title": "Configuración global",
@@ -696,8 +790,8 @@
"snapshots": "Instantáneas",
"cleanCopySnapshots": "clean_copy Instantáneas"
},
- "desc": "Enviar a Frigate+ requiere que tanto las capturas instantáneas como las capturas clean_copy estén habilitadas en tu configuración.",
- "cleanCopyWarning": "Algunas cámaras tienen las instantáneas habilitadas pero tienen la copia limpia desactivada. Necesitas habilitar clean_copy en tu configuración de instantáneas para poder enviar imágenes de estas cámaras a Frigate+."
+ "desc": "Enviar a Frigate+ requiere que las instantáneas estén habilitadas en tu configuración.",
+ "cleanCopyWarning": "Algunas cámaras tienen las instantáneas deshabilitadas"
},
"modelInfo": {
"title": "Información del modelo",
@@ -708,13 +802,21 @@
"cameras": "Cámaras",
"loading": "Cargando información del modelo…",
"error": "No se pudo cargar la información del modelo",
- "availableModels": "Modelos disponibles",
+ "availableModels": "Modelos de Frigate+ disponibles",
"loadingAvailableModels": "Cargando modelos disponibles…",
"modelSelect": "Tus modelos disponibles en Frigate+ se pueden seleccionar aquí. Ten en cuenta que solo se pueden seleccionar modelos compatibles con tu configuración actual de detectores.",
"trainDate": "Fecha de entrenamiento",
"plusModelType": {
"baseModel": "Modelo Base",
"userModel": "Ajustado Finamente"
+ },
+ "noModelLoaded": "Actualmente no hay ningún modelo de Frigate+ cargado.",
+ "selectModel": "Selecciona un modelo",
+ "noModelsAvailable": "No hay modelos disponibles",
+ "filter": {
+ "ariaLabel": "Filtrar modelos por tipo",
+ "baseModels": "Modelos base",
+ "fineTunedModels": "Modelos ajustados"
}
},
"toast": {
@@ -722,7 +824,15 @@
"error": "No se pudieron guardar los cambios en la configuración: {{errorMessage}}"
},
"restart_required": "Es necesario reiniciar (se ha cambiado el modelo Frigate+)",
- "unsavedChanges": "Cambios en la configuración de Frigate+ no guardados"
+ "unsavedChanges": "Cambios en la configuración de Frigate+ no guardados",
+ "description": "Frigate+ es un servicio de suscripción que proporciona acceso a funciones y capacidades adicionales para su instancia de Frigate, incluida la posibilidad de utilizar modelos de detección de objetos personalizados entrenados con sus propios datos. Puede gestionar la configuración de sus modelos de Frigate+ aquí.",
+ "cardTitles": {
+ "api": "API",
+ "currentModel": "Modelo actual",
+ "otherModels": "Otros modelos",
+ "configuration": "Configuración"
+ },
+ "changeInDetectorsAndModel": "Cambiar modelo"
},
"enrichments": {
"title": "Configuración de Enriquecimientos",
@@ -748,11 +858,11 @@
"modelSize": {
"label": "Tamaño del Modelo",
"small": {
- "title": "pequeño",
+ "title": "size",
"desc": "Usar la opción small emplea una versión cuantizada del modelo que consume menos memoria RAM y se ejecuta más rápido en la CPU, con una diferencia muy pequeña o casi imperceptible en la calidad de las representaciones (embeddings)."
},
"large": {
- "title": "grande",
+ "title": "model",
"desc": "Usar la opción large emplea el modelo completo de Jina y se ejecutará automáticamente en la GPU, si está disponible."
},
"desc": "Tamaño del modelo usado para la búsqueda semántica."
@@ -982,7 +1092,7 @@
"nameLength": "El nombre de la cámara debe tener 64 caracteres o menos",
"invalidCharacters": "El nombre de la cámara contiene caracteres no válidos",
"nameExists": "El nombre de la cámara ya existe",
- "customUrlRtspRequired": "Las URL personalizadas deben comenzar con \"rtsp://\". Se requiere configuración manual para transmisiones de cámara sin RTSP.",
+ "customUrlRtspRequired": "Las URL personalizadas deben comenzar por “rtsp://” o “rtsps://”. Se requiere configuración manual para flujos de cámara que no sean RTSP.",
"brandOrCustomUrlRequired": "Seleccione una marca de cámara con host/IP o elija \"Otro\" con una URL personalizada"
},
"description": "Ingrese los detalles de su cámara y elija probar la cámara o seleccionar manualmente la marca.",
@@ -1138,7 +1248,8 @@
},
"hikvision": {
"substreamWarning": "La subtransmisión 1 está limitada a una resolución baja. Muchas cámaras Hikvision admiten subtransmisiones adicionales que deben habilitarse en la configuración de la cámara. Se recomienda comprobar y utilizar dichas transmisiones si están disponibles."
- }
+ },
+ "resolutionUnknown": "No se pudo detectar la resolución de este flujo. Debes establecer manualmente la resolución de detección en Ajustes o en tu configuración."
}
},
"title": "Añadir cámara",
@@ -1171,8 +1282,45 @@
"selectCamera": "Seleccione una cámara",
"backToSettings": "Volver a configuración de la cámara",
"streams": {
- "title": "Habilitar/deshabilitar cámaras",
- "desc": "Desactiva temporalmente una cámara hasta que Frigate se reinicie. Desactivar una cámara detiene por completo el procesamiento de las transmisiones de Frigate. La detección, la grabación y la depuración no estarán disponibles. Nota: Esto no desactiva las retransmisiones de go2rtc. "
+ "title": "Estado y detalles de la cámara",
+ "desc": "Desactiva temporalmente una cámara hasta que Frigate se reinicie. Desactivar una cámara detiene por completo el procesamiento de las transmisiones de Frigate. La detección, la grabación y la depuración no estarán disponibles. Nota: Esto no desactiva las retransmisiones de go2rtc. ",
+ "enableDesc": "Deshabilita temporalmente una cámara habilitada hasta que Frigate se reinicie. Deshabilitar una cámara detiene completamente el procesamiento de los flujos de esa cámara por parte de Frigate. La detección, la grabación y la depuración no estarán disponibles. Nota: Esto no deshabilita las retransmisiones de go2rtc.Arrastra el controlador para reordenar las cámaras tal y como aparecen en la interfaz. El orden de las cámaras habilitadas se reflejará en toda la interfaz, incluido el panel en directo y los menús desplegables de selección de cámaras.",
+ "enableLabel": "Cámaras habilitadas",
+ "disableLabel": "Cámaras deshabilitadas",
+ "disableDesc": "Habilita una cámara que actualmente no está visible en la interfaz y está deshabilitada en la configuración. Es necesario reiniciar Frigate después de habilitarla.",
+ "enableSuccess": "{{cameraName}} habilitada. Reinicia Frigate para aplicar los cambios.",
+ "friendlyName": {
+ "edit": "Editar nombre visible de la cámara",
+ "title": "Editar nombre visible",
+ "description": "Establece el nombre descriptivo que se mostrará para esta cámara en toda la interfaz de Frigate. Déjalo en blanco para usar el ID de la cámara.",
+ "rename": "Renombrar"
+ },
+ "reorderHandle": "Arrastrar para reordenar",
+ "saving": "Guardando…",
+ "saved": "Guardado",
+ "details": {
+ "edit": "Editar detalles de la cámara",
+ "title": "Editar detalles de la cámara",
+ "description": "Actualiza el nombre visible, la URL externa y la visibilidad usados para esta cámara en toda la interfaz de Frigate.",
+ "friendlyNameLabel": "Nombre visible",
+ "friendlyNameHelp": "Nombre descriptivo que se muestra para esta cámara en toda la interfaz de Frigate. Déjalo en blanco para usar el ID de la cámara.",
+ "webuiUrlLabel": "URL de la interfaz web de la cámara",
+ "webuiUrlHelp": "URL para acceder directamente a la interfaz web de la cámara desde la vista de depuración. Déjala en blanco para deshabilitar el enlace.",
+ "webuiUrlInvalid": "Debe ser una URL válida (p. ej., https://ejemplo.com).",
+ "dashboardLabel": "Mostrar en el panel En directo",
+ "dashboardHelp": "Mostrar esta cámara en el panel en directo.",
+ "reviewLabel": "Mostrar en Revisión",
+ "reviewHelp": "Mostrar esta cámara en Revisión, incluido el filtro de cámaras, la revisión de movimiento y la vista de historial."
+ },
+ "label": "Estado de la cámara",
+ "description": "Set the operating state for each camera. On : las transmisiones se procesan con normalidad.Off : pausa temporalmente el procesamiento. No persiste tras reinicios de Frigate.Disabled : detiene el procesamiento y guarda el cambio en tu configuración. Es necesario reiniciar para volver a activar una cámara desactivada.Note: Desactivar no afecta a las retransmisiones de go2rtc. Arrastra el asa para reordenar las cámaras activas tal como aparecen en toda la interfaz, incluido el panel de Live y los menús desplegables de selección de cámara.",
+ "disabledSubheading": "Deshabilitado en la configuración",
+ "status": {
+ "on": "On",
+ "off": "Off",
+ "disabled": "Deshabilitado"
+ },
+ "disableSuccess": "{{cameraName}} deshabilitada y guardada en la configuración."
},
"cameraConfig": {
"add": "Añadir cámara",
@@ -1202,6 +1350,125 @@
"toast": {
"success": "Cámara {{cameraName}} guardada correctamente"
}
+ },
+ "deleteCameraDialog": {
+ "description": "Eliminar una cámara borrará permanentemente todas las grabaciones, los objetos rastreados y la configuración de esa cámara. Es posible que sea necesario eliminar manualmente cualquier transmisión go2rtc asociada a esta cámara.",
+ "title": "Eliminar cámara",
+ "selectPlaceholder": "Elegir cámara...",
+ "confirmTitle": "¿Estás seguro?",
+ "confirmWarning": "Eliminar {{cameraName}} no se puede deshacer.",
+ "deleteExports": "Eliminar también las exportaciones de esta cámara",
+ "confirmButton": "Eliminar permanentemente",
+ "success": "La cámara {{cameraName}} se ha eliminado correctamente",
+ "error": "No se pudo eliminar la cámara {{cameraName}}"
+ },
+ "deleteCamera": "Eliminar cámara",
+ "profiles": {
+ "title": "Sobrescrituras de cámaras del perfil",
+ "selectLabel": "Seleccionar perfil",
+ "description": "Configura qué cámaras se activan o desactivan cuando se activa un perfil. Las cámaras configuradas como “Heredar” conservan su estado predeterminado.",
+ "inherit": "Heredar",
+ "enabled": "Habilitado",
+ "disabled": "Deshabilitado",
+ "on": "Encendido",
+ "off": "Apagado"
+ },
+ "cameraType": {
+ "title": "Tipo de cámara",
+ "label": "Tipo de cámara",
+ "description": "Establece el tipo de cada cámara. Las cámaras LPR dedicadas son cámaras de un solo propósito con un zoom óptico potente para capturar matrículas de vehículos lejanos. La mayoría de cámaras deberían usar el tipo de cámara normal salvo que la cámara esté específicamente destinada a LPR y tenga una vista muy enfocada a matrículas.",
+ "normal": "Normal",
+ "dedicatedLpr": "LPR dedicada",
+ "saveSuccess": "Se ha actualizado el tipo de cámara de {{cameraName}}. Reinicia Frigate para aplicar los cambios."
+ },
+ "description": "Añade, edita y elimina cámaras, controla el estado de cada cámara y configura sobrescrituras por perfil y tipo de cámara. Para configurar flujos, detección, movimiento y otros ajustes específicos de cámara, selecciona la sección correspondiente dentro de Configuración de cámara.",
+ "clone": {
+ "sectionTitle": "Clonar configuración",
+ "sectionDescription": "Copia la configuración de una cámara a otra cámara o a una nueva.",
+ "button": "Clonar configuración",
+ "title": "Clonar configuración de la cámara",
+ "description": "Copia la configuración de una cámara a una o varias cámaras existentes o a una cámara nueva. La identidad de la cámara (nombre, nombre visible, URL de la interfaz web y orden de visualización) nunca se copia.",
+ "source": {
+ "label": "Cámara de origen",
+ "placeholder": "Selecciona una cámara de origen",
+ "required": "Selecciona una cámara de origen"
+ },
+ "target": {
+ "legend": "Destino",
+ "newRadio": "Nueva cámara",
+ "newNameLabel": "Nombre de la cámara",
+ "newNamePlaceholder": "p. ej., puerta_trasera o Puerta trasera",
+ "newNameRequired": "El nombre de la cámara es obligatorio",
+ "newNameInvalid": "Nombre de cámara no válido",
+ "newNameCollision": "Ya existe una cámara con este nombre",
+ "newStreamsForced": "Los flujos siempre se copian al crear una cámara nueva.",
+ "existingCamerasRadio": "Cámaras existentes",
+ "allCameras": "Todas las cámaras",
+ "existingPlaceholder": "Selecciona al menos una cámara",
+ "existingDisabled": "No hay otras cámaras a las que copiar la configuración"
+ },
+ "categories": {
+ "legend": "Configuración para clonar",
+ "description": "Elige qué ajustes copiar desde la cámara de origen.",
+ "selectAll": "Seleccionar todo",
+ "selectNone": "No seleccionar ninguno",
+ "resetDefaults": "Restablecer valores predeterminados",
+ "general": "General",
+ "spatial": "Configuración espacial",
+ "streams": "Flujos",
+ "spatialWarningTitle": "Resolución no coincidente",
+ "spatialWarning": "La resolución de detección de la cámara de origen {{srcCamera}} ({{srcWidth}}×{{srcHeight}}) es diferente de la de: {{cameras}}. Es posible que los polígonos no se alineen correctamente en esas cámaras. Estas opciones están desactivadas de forma predeterminada; actívalas para copiarlas tal cual.",
+ "restartHint": "Reinicio necesario",
+ "items": {
+ "record": "Grabación",
+ "snapshots": "Instantáneas",
+ "review": "Revisión",
+ "motion": "Detección de movimiento",
+ "objects": "Objetos",
+ "audio": "Detección de audio",
+ "audio_transcription": "Transcripción de audio",
+ "notifications": "Notificaciones",
+ "birdseye": "Birdseye",
+ "mqtt": "MQTT",
+ "timestamp_style": "Estilo de marca de tiempo",
+ "onvif": "ONVIF",
+ "lpr": "Reconocimiento de matrículas",
+ "face_recognition": "Reconocimiento facial",
+ "semantic_search": "Búsqueda semántica",
+ "genai": "IA generativa",
+ "type": "Tipo de cámara (normal / LPR dedicada)",
+ "profiles": "Perfiles",
+ "detect": "Dimensiones de detección",
+ "zones": "Zonas",
+ "motion_mask": "Máscaras de movimiento",
+ "object_masks": "Máscaras de objetos",
+ "ffmpeg_live": "URL y roles de los flujos"
+ }
+ },
+ "footer": {
+ "changeCount_one": "Se aplicará {{count}} cambio",
+ "changeCount_many": "Se aplicarán {{count}} cambios",
+ "changeCount_other": "Se aplicarán {{count}} cambios",
+ "restartNeeded": "Será necesario reiniciar para aplicar algunos cambios.",
+ "liveOnly": "Todos los cambios se aplicarán en tiempo real sin necesidad de reiniciar.",
+ "submit": "Clonar",
+ "submitting": "Clonando…"
+ },
+ "toast": {
+ "success": "Configuración copiada a {{cameraName}}",
+ "successWithRestart": "Configuración copiada a {{cameraName}}. Reinicia Frigate para aplicar todos los cambios.",
+ "successMulti_one": "Configuración copiada a {{count}} cámara",
+ "successMulti_many": "Configuración copiada a {{count}} cámaras",
+ "successMulti_other": "Configuración copiada a {{count}} cámaras",
+ "successMultiWithRestart_one": "Configuración copiada a {{count}} cámara. Reinicia Frigate para aplicar todos los cambios.",
+ "successMultiWithRestart_many": "Configuración copiada a {{count}} cámaras. Reinicia Frigate para aplicar todos los cambios.",
+ "successMultiWithRestart_other": "Configuración copiada a {{count}} cámaras. Reinicia Frigate para aplicar todos los cambios.",
+ "partialFailure": "Se aplicaron {{successCount}} secciones; ‘{{failedSection}}’ falló: {{errorMessage}}",
+ "partialFailureMulti": "Copiado a {{successCount}} cámara(s); error en {{failed}}: {{errorMessage}}",
+ "newCameraPartialFailure": "La cámara {{cameraName}} se creó, pero no se pudieron copiar algunos ajustes: {{errorMessage}}",
+ "sourceMissing": "La cámara de origen ya no existe",
+ "submitError": "No se pudo clonar la cámara: {{errorMessage}}"
+ }
}
},
"cameraReview": {
@@ -1245,20 +1512,665 @@
"overriddenGlobal": "Sobrescrito (Global)",
"overriddenBaseConfigTooltip": "El perfil {{profile}} sobrescribe los ajustes de configuración de esta sección",
"overriddenGlobalTooltip": "Esta cámara sobrescribe los ajustes de configuración global en esta sección",
- "overriddenBaseConfig": "Sobrescrito (Configuración Base)"
+ "overriddenBaseConfig": "Sobrescrito (Configuración Base)",
+ "overriddenInCameras": {
+ "label_one": "Sobrescrito en {{count}} cámara",
+ "label_many": "Sobrescrito en {{count}} cámaras",
+ "label_other": "Sobrescrito en {{count}} cámaras",
+ "tooltip_one": "{{count}} cámaras sobrescriben los valores de esta sección. Haz clic para ver los detalles.",
+ "tooltip_many": "{{count}} cámaras sobrescriben los valores de esta sección. Haz clic para ver los detalles.",
+ "tooltip_other": "{{count}} cámaras sobrescriben los valores de esta sección. Haz clic para ver los detalles.",
+ "heading_one": "This global section has fields that are overridden in {{count}} camera.",
+ "heading_many": "Esta sección global tiene campos que están sobrescritos en {{count}} cámaras.",
+ "heading_other": "Esta sección global tiene campos que están sobrescritos en {{count}} cámaras.",
+ "othersField_one": "{{count}} más",
+ "othersField_many": "{{count}} más",
+ "othersField_other": "{{count}} más",
+ "profilePrefix": "Perfil {{profile}}: {{fields}}"
+ },
+ "overriddenGlobalHeading_one": "Esta cámara sobrescribe {{count}} campo de la configuración global:",
+ "overriddenGlobalHeading_many": "Esta cámara sobrescribe {{count}} campos de la configuración global:",
+ "overriddenGlobalHeading_other": "Esta cámara sobrescribe {{count}} campos de la configuración global:",
+ "overriddenGlobalNoDeltas": "Esta cámara sobrescribe la configuración global, pero no hay diferencias en los valores de los campos.",
+ "overriddenBaseConfigHeading_one": "El perfil {{profile}} sobrescribe {{count}} campo de la configuración base:",
+ "overriddenBaseConfigHeading_many": "El perfil {{profile}} sobrescribe {{count}} campos de la configuración base:",
+ "overriddenBaseConfigHeading_other": "El perfil {{profile}} sobrescribe {{count}} campos de la configuración base:",
+ "overriddenBaseConfigNoDeltas": "El perfil {{profile}} sobrescribe esta sección, pero no hay diferencias en los valores de los campos respecto a la configuración base."
},
"onvif": {
- "profileLoading": "Cargando perfiles..."
+ "profileLoading": "Cargando perfiles...",
+ "profileAuto": "Auto",
+ "autotracking": {
+ "zooming": {
+ "disabled": "Deshabilitado",
+ "absolute": "Absoluto",
+ "relative": "Relativo"
+ }
+ }
},
"maintenance": {
"sync": {
"verboseDesc": "Escribe una lista completa de archivos huérfanos en el disco para su revisión.",
- "verbose": "Detallado"
- }
+ "verbose": "Detallado",
+ "desc": "Frigate limpiará periódicamente los archivos multimedia según un cronograma regular, de acuerdo con su configuración de retención. Es normal ver algunos archivos huérfanos mientras Frigate se ejecuta. Utilice esta función para eliminar del disco los archivos multimedia huérfanos que ya no se referencian en la base de datos.",
+ "forceDesc": "Omitir el umbral de seguridad y completar la sincronización incluso si se eliminara más del 50% de los archivos.",
+ "title": "Sincronización de medios",
+ "started": "Sincronización de medios iniciada.",
+ "alreadyRunning": "Ya hay una tarea de sincronización en ejecución",
+ "error": "No se pudo iniciar la sincronización",
+ "currentStatus": "Estado",
+ "jobId": "ID de tarea",
+ "startTime": "Hora de inicio",
+ "endTime": "Hora de finalización",
+ "statusLabel": "Estado",
+ "results": "Resultados",
+ "errorLabel": "Error",
+ "mediaTypes": "Tipos de medios",
+ "allMedia": "Todos los medios",
+ "dryRun": "Simulación",
+ "dryRunEnabled": "No se eliminará ningún archivo",
+ "dryRunDisabled": "Se eliminarán archivos",
+ "force": "Forzar",
+ "running": "Sincronización en curso...",
+ "start": "Iniciar sincronización",
+ "inProgress": "La sincronización está en curso. Esta página está deshabilitada.",
+ "status": {
+ "queued": "En cola",
+ "running": "En ejecución",
+ "completed": "Completado",
+ "failed": "Fallido",
+ "notRunning": "No está en ejecución"
+ },
+ "resultsFields": {
+ "filesChecked": "Archivos comprobados",
+ "orphansFound": "Huérfanos encontrados",
+ "orphansDeleted": "Huérfanos eliminados",
+ "aborted": "Abortado. La eliminación superaría el umbral de seguridad.",
+ "error": "Error",
+ "totals": "Totales"
+ },
+ "event_snapshots": "Instantáneas de objetos rastreados",
+ "event_thumbnails": "Miniaturas de objetos rastreados",
+ "review_thumbnails": "Miniaturas de revisión",
+ "previews": "Vistas previas",
+ "exports": "Exportaciones",
+ "recordings": "Grabaciones"
+ },
+ "regionGrid": {
+ "clearConfirmDesc": "No se recomienda borrar la cuadrícula de la región a menos que haya cambiado recientemente el tamaño del modelo de su detector o la posición física de su cámara y esté experimentando problemas de seguimiento de objetos. La cuadrícula se reconstruirá automáticamente con el tiempo a medida que se realice el seguimiento de los objetos. Es necesario reiniciar Frigate para que los cambios surtan efecto.",
+ "desc": "La cuadrícula de regiones es una optimización que aprende dónde suelen aparecer los objetos de diferentes tamaños en el campo de visión de cada cámara. Frigate utiliza estos datos para dimensionar de forma eficiente las regiones de detección. La cuadrícula se construye automáticamente a lo largo del tiempo a partir de los datos de los objetos rastreados.",
+ "title": "Cuadrícula de regiones",
+ "clear": "Borrar cuadrícula de regiones",
+ "clearConfirmTitle": "Borrar cuadrícula de regiones",
+ "clearSuccess": "Cuadrícula de regiones borrada correctamente",
+ "clearError": "No se pudo borrar la cuadrícula de regiones",
+ "restartRequired": "Es necesario reiniciar para que los cambios de la cuadrícula de regiones surtan efecto"
+ },
+ "title": "Mantenimiento"
},
"configForm": {
"camera": {
- "noCameras": "No hay cámaras disponibles"
+ "noCameras": "No hay cámaras disponibles",
+ "description": "Estos ajustes se aplican únicamente a esta cámara y anulan los ajustes globales.",
+ "title": "Ajustes de cámara"
+ },
+ "genaiModel": {
+ "noModels": "No hay modelos disponibles",
+ "placeholder": "Selecciona o introduce un modelo…",
+ "search": "Busca o introduce un modelo…",
+ "available": "Modelos disponibles",
+ "useCustom": "Usar “{{value}}”",
+ "refresh": "Actualizar modelos",
+ "probeFailed": "No se pudieron detectar los modelos",
+ "fetchedModels": "La lista de modelos se ha obtenido correctamente"
+ },
+ "global": {
+ "description": "Estos ajustes se aplican a todas las cámaras, a menos que se anulen en los ajustes específicos de cada cámara.",
+ "title": "Ajustes globales"
+ },
+ "sections": {
+ "go2rtc": "streams",
+ "detect": "Detección",
+ "record": "Grabación",
+ "snapshots": "Instantáneas",
+ "motion": "Movimiento",
+ "objects": "Objetos",
+ "review": "Revisión",
+ "audio": "Audio",
+ "notifications": "Notificaciones",
+ "live": "Vista en directo",
+ "timestamp_style": "Marcas de tiempo",
+ "mqtt": "MQTT",
+ "database": "Base de datos",
+ "telemetry": "Telemetría",
+ "auth": "Autenticación",
+ "tls": "TLS",
+ "proxy": "Proxy",
+ "ffmpeg": "FFmpeg",
+ "detectors": "Detectores",
+ "model": "Modelo",
+ "semantic_search": "Búsqueda semántica",
+ "genai": "GenAI",
+ "face_recognition": "Reconocimiento facial",
+ "lpr": "Reconocimiento de matrículas",
+ "birdseye": "Birdseye",
+ "masksAndZones": "Máscaras / zonas"
+ },
+ "advancedSettingsCount": "Ajustes avanzados ({{count}})",
+ "advancedCount": "Avanzado ({{count}})",
+ "showAdvanced": "Mostrar ajustes avanzados",
+ "tabs": {
+ "sharedDefaults": "Valores predeterminados compartidos",
+ "system": "Sistema",
+ "integrations": "Integraciones"
+ },
+ "additionalProperties": {
+ "keyLabel": "Clave",
+ "valueLabel": "Valor",
+ "keyPlaceholder": "Nueva clave",
+ "remove": "Eliminar"
+ },
+ "knownPlates": {
+ "namePlaceholder": "p. ej., Coche de mi mujer",
+ "platePlaceholder": "Número de matrícula o regex"
+ },
+ "timezone": {
+ "defaultOption": "Usar zona horaria del navegador"
+ },
+ "roleMap": {
+ "empty": "No hay asignaciones de roles",
+ "roleLabel": "Rol",
+ "groupsLabel": "Grupos",
+ "addMapping": "Añadir asignación de rol",
+ "remove": "Eliminar"
+ },
+ "ffmpegArgs": {
+ "preset": "Preajuste",
+ "manual": "Argumentos manuales",
+ "inherit": "Heredar del ajuste de cámara",
+ "none": "Ninguno",
+ "useGlobalSetting": "Heredar del ajuste global",
+ "selectPreset": "Seleccionar preajuste",
+ "manualPlaceholder": "Introduce argumentos de FFmpeg",
+ "presetLabels": {
+ "preset-rpi-64-h264": "Raspberry Pi (H.264)",
+ "preset-rpi-64-h265": "Raspberry Pi (H.265)",
+ "preset-vaapi": "VAAPI (GPU Intel/AMD)",
+ "preset-intel-qsv-h264": "Intel QuickSync (H.264)",
+ "preset-intel-qsv-h265": "Intel QuickSync (H.265)",
+ "preset-nvidia": "GPU NVIDIA",
+ "preset-jetson-h264": "NVIDIA Jetson (H.264)",
+ "preset-jetson-h265": "NVIDIA Jetson (H.265)",
+ "preset-rkmpp": "Rockchip RKMPP",
+ "preset-http-jpeg-generic": "HTTP JPEG (genérico)",
+ "preset-http-mjpeg-generic": "HTTP MJPEG (genérico)",
+ "preset-http-reolink": "HTTP - Cámaras Reolink",
+ "preset-rtmp-generic": "RTMP (genérico)",
+ "preset-rtsp-generic": "RTSP (genérico)",
+ "preset-rtsp-restream": "RTSP - Retransmisión desde go2rtc",
+ "preset-rtsp-restream-low-latency": "RTSP - Retransmisión desde go2rtc (baja latencia)",
+ "preset-rtsp-udp": "RTSP - UDP",
+ "preset-rtsp-blue-iris": "RTSP - Blue Iris",
+ "preset-record-generic": "Grabación (genérica, sin audio)",
+ "preset-record-generic-audio-copy": "Grabación (genérica + copiar audio)",
+ "preset-record-generic-audio-aac": "Grabación (genérica + audio a AAC)",
+ "preset-record-mjpeg": "Grabación - Cámaras MJPEG",
+ "preset-record-jpeg": "Grabación - Cámaras JPEG",
+ "preset-record-ubiquiti": "Grabación - Cámaras Ubiquiti"
+ }
+ },
+ "cameraInputs": {
+ "itemTitle": "Flujo {{index}}",
+ "sourceMode": {
+ "restream": "Retransmisión (go2rtc)",
+ "manual": "Ruta de entrada manual",
+ "go2rtcStreamLabel": "Flujo go2rtc",
+ "go2rtcStreamPlaceholder": "Selecciona un flujo go2rtc",
+ "noGo2rtcStreams": "No hay flujos go2rtc configurados",
+ "go2rtcStreamSearch": "Buscar flujos…",
+ "availableStreams": "Flujos disponibles",
+ "noMatchingStreams": "No hay flujos coincidentes"
+ }
+ },
+ "restartRequiredField": "Reinicio necesario",
+ "restartRequiredFooter": "Configuración modificada - reinicio necesario",
+ "detect": {
+ "title": "Ajustes de detección"
+ },
+ "detectors": {
+ "title": "Ajustes de detector",
+ "singleType": "Solo se permite un detector {{type}}.",
+ "keyRequired": "El nombre del detector es obligatorio.",
+ "keyDuplicate": "El nombre del detector ya existe.",
+ "noSchema": "No hay esquemas de detector disponibles.",
+ "none": "No hay instancias de detector configuradas.",
+ "add": "Añadir detector",
+ "addCustomKey": "Añadir clave personalizada"
+ },
+ "record": {
+ "title": "Ajustes de grabación"
+ },
+ "snapshots": {
+ "title": "Ajustes de instantáneas"
+ },
+ "motion": {
+ "title": "Ajustes de movimiento"
+ },
+ "objects": {
+ "title": "Ajustes de objetos"
+ },
+ "audioLabels": {
+ "summary": "{{count}} etiquetas de audio seleccionadas",
+ "empty": "No hay etiquetas de audio disponibles"
+ },
+ "objectLabels": {
+ "summary": "{{count}} tipos de objeto seleccionados",
+ "empty": "No hay etiquetas de objeto disponibles"
+ },
+ "reviewLabels": {
+ "summary": "{{count}} etiquetas seleccionadas",
+ "empty": "No hay etiquetas disponibles"
+ },
+ "filters": {
+ "objectFieldLabel": "{{field}} para {{label}}"
+ },
+ "zoneNames": {
+ "summary": "{{count}} seleccionados",
+ "empty": "No hay zonas disponibles"
+ },
+ "inputRoles": {
+ "summary": "{{count}} roles seleccionados",
+ "empty": "No hay roles disponibles",
+ "options": {
+ "detect": "Detectar",
+ "record": "Grabar",
+ "audio": "Audio"
+ }
+ },
+ "genaiRoles": {
+ "options": {
+ "embeddings": "Embedding",
+ "descriptions": "Descripciones",
+ "chat": "Chat"
+ }
+ },
+ "semanticSearchModel": {
+ "placeholder": "Seleccionar modelo…",
+ "builtIn": "Modelos integrados",
+ "genaiProviders": "Proveedores de GenAI"
+ },
+ "review": {
+ "title": "Ajustes de revisión"
+ },
+ "audio": {
+ "title": "Ajustes de audio"
+ },
+ "notifications": {
+ "title": "Ajustes de notificaciones"
+ },
+ "live": {
+ "title": "Ajustes de vista en directo"
+ },
+ "timestamp_style": {
+ "title": "Ajustes de marcas de tiempo"
+ },
+ "searchPlaceholder": "Buscar...",
+ "addCustomLabel": "Añadir etiqueta personalizada...",
+ "semanticSearchModelSize": {
+ "notApplicable": "No aplicable a proveedores GenAI"
+ },
+ "liveStreams": {
+ "streamNameLabel": "Nombre del flujo",
+ "streamNamePlaceholder": "p. ej., Flujo principal HD",
+ "go2rtcStreamLabel": "Flujo go2rtc",
+ "go2rtcStreamPlaceholder": "Selecciona un flujo go2rtc",
+ "go2rtcStreamSearch": "Busca o introduce un nombre de flujo…",
+ "noGo2rtcStreams": "No hay flujos go2rtc configurados",
+ "availableStreams": "Flujos disponibles",
+ "useCustom": "Usar “{{value}}”",
+ "addStream": "Añadir flujo"
+ },
+ "ptzPresets": {
+ "placeholder": "Selecciona o introduce un preajuste…",
+ "search": "Busca o introduce un preajuste…",
+ "noPresets": "No hay preajustes disponibles",
+ "available": "Preajustes de cámara",
+ "useCustom": "Usar “{{value}}”"
+ },
+ "defaultRole": {
+ "admin": "Admin",
+ "viewer": "Visualizador"
}
+ },
+ "globalConfig": {
+ "title": "Configuración global",
+ "description": "Configura los ajustes globales que se aplican a todas las cámaras, a menos que se sobrescriban.",
+ "toast": {
+ "success": "Ajustes globales guardados con éxito",
+ "error": "Error al guardar los ajustes globales",
+ "validationError": "Error de validación"
+ }
+ },
+ "cameraConfig": {
+ "title": "Configuración de cámara",
+ "description": "Configura los ajustes de cámaras individuales. Estos ajustes sobrescriben los valores globales predeterminados.",
+ "overriddenBadge": "Sobrescrito",
+ "resetToGlobal": "Restablecer al valor global",
+ "toast": {
+ "success": "Ajustes de cámara guardados con éxito",
+ "error": "Error al guardar los ajustes de cámara"
+ }
+ },
+ "toast": {
+ "success": "Ajustes guardados con éxito",
+ "applied": "Ajustes aplicados con éxito",
+ "successRestartRequired": "Ajustes guardados con éxito. Reinicia Frigate para aplicar los cambios.",
+ "error": "Error al guardar los ajustes",
+ "validationError": "Error de validación: {{message}}",
+ "resetSuccess": "Restablecido a los valores globales predeterminados",
+ "resetError": "Error al restablecer los ajustes",
+ "saveAllSuccess_one": "Se ha guardado {{count}} sección con éxito.",
+ "saveAllSuccess_many": "Se han guardado las {{count}} secciones con éxito.",
+ "saveAllSuccess_other": "Se han guardado {{count}} secciones con éxito.",
+ "saveAllPartial_one": "Se ha guardado {{successCount}} de {{totalCount}} sección. {{failCount}} ha fallado.",
+ "saveAllPartial_many": "Se han guardado {{successCount}} de {{totalCount}} secciones. {{failCount}} han fallado.",
+ "saveAllPartial_other": "Se han guardado {{successCount}} de {{totalCount}} secciones. {{failCount}} han fallado.",
+ "saveAllFailure": "Error al guardar todas las secciones.",
+ "saveAllSuccessRestartRequired_one": "La sección {{count}} se ha guardado correctamente. Reinicia Frigate para aplicar los cambios.",
+ "saveAllSuccessRestartRequired_many": "Las {{count}} secciones se han guardado correctamente. Reinicia Frigate para aplicar los cambios.",
+ "saveAllSuccessRestartRequired_other": "Las {{count}} secciones se han guardado correctamente. Reinicia Frigate para aplicar los cambios."
+ },
+ "profiles": {
+ "title": "Perfiles",
+ "activeProfile": "Perfil activo",
+ "noActiveProfile": "Sin perfil activo",
+ "active": "Activo",
+ "activated": "Perfil '{{profile}}' activado",
+ "activateFailed": "Error al establecer el perfil",
+ "deactivated": "Perfil desactivado",
+ "noProfiles": "No hay perfiles definidos.",
+ "noOverrides": "Sin sobrescripciones",
+ "cameraCount_one": "{{count}} cámara",
+ "cameraCount_many": "{{count}} de cámaras",
+ "cameraCount_other": "{{count}} cámaras",
+ "columnCamera": "Cámara",
+ "columnOverrides": "Sobrescripciones del perfil",
+ "baseConfig": "Configuración base",
+ "addProfile": "Añadir perfil",
+ "newProfile": "Nuevo perfil",
+ "profileNamePlaceholder": "ej. Armado, Fuera de casa, Modo noche",
+ "friendlyNameLabel": "Nombre del perfil",
+ "profileIdLabel": "ID del perfil",
+ "profileIdDescription": "Identificador interno utilizado en la configuración y automatizaciones",
+ "nameInvalid": "Solo se permiten letras minúsculas, números y guiones bajos",
+ "nameDuplicate": "Ya existe un perfil con este nombre",
+ "error": {
+ "mustBeAtLeastTwoCharacters": "Debe tener al menos 2 caracteres",
+ "mustNotContainPeriod": "No debe contener puntos",
+ "alreadyExists": "Ya existe un perfil con este ID"
+ },
+ "renameProfile": "Renombrar perfil",
+ "renameSuccess": "Perfil renombrado a '{{profile}}'",
+ "enabledDescription": "Los perfiles están habilitados. Cree un nuevo perfil a continuación, navegue a una sección de configuración de cámara para realizar sus cambios y guarde para que estos surtan efecto.",
+ "disabledDescription": "Los perfiles le permiten definir conjuntos con nombre de anulaciones de configuración de la cámara (por ejemplo: armado, fuera, noche) que pueden activarse bajo demanda.",
+ "deleteProfile": "Eliminar perfil",
+ "deleteProfileConfirm": "¿Eliminar el perfil \"{{profile}}\" de todas las cámaras? Esta acción no se puede deshacer.",
+ "deleteSuccess": "Perfil '{{profile}}' eliminado",
+ "createSuccess": "Perfil '{{profile}}' creado",
+ "removeOverride": "Eliminar sobrescritura de perfil",
+ "deleteSection": "Eliminar sobrescrituras de sección",
+ "deleteSectionConfirm": "¿Eliminar las sobrescrituras de {{section}} del perfil {{profile}} en {{camera}}?",
+ "deleteSectionSuccess": "Sobrescrituras de {{section}} eliminadas para {{profile}}",
+ "enableSwitch": "Habilitar perfiles"
+ },
+ "go2rtcStreams": {
+ "renameStreamDesc": "Introduce un nuevo nombre para esta transmisión. Cambiar el nombre de una transmisión puede provocar fallos en las cámaras u otras transmisiones que hagan referencia a ella por su nombre.",
+ "addStreamDesc": "Introduce un nombre para la nueva transmisión. Este nombre se utilizará para hacer referencia a la transmisión en la configuración de su cámara.",
+ "description": "Gestione las configuraciones de transmisión de go2rtc para la retransmisión de cámaras. Cada transmisión tiene un nombre y una o más URL de origen.",
+ "deleteStreamConfirm": "¿Está seguro de que desea eliminar la transmisión \"{{streamName}}\"? Las cámaras que hagan referencia a esta transmisión podrían dejar de funcionar.",
+ "title": "Flujos go2rtc",
+ "addStream": "Añadir flujo",
+ "addUrl": "Añadir URL",
+ "streamName": "Nombre del flujo",
+ "streamNamePlaceholder": "p. ej., puerta_principal",
+ "streamUrlPlaceholder": "p. ej., rtsp://usuario:contraseña@192.168.1.100/stream",
+ "deleteStream": "Eliminar flujo",
+ "noStreams": "No hay flujos go2rtc configurados. Añade un flujo para empezar.",
+ "validation": {
+ "nameRequired": "El nombre del flujo es obligatorio",
+ "nameDuplicate": "Ya existe un flujo con este nombre",
+ "nameInvalid": "El nombre del flujo solo puede contener letras, números, guiones bajos y guiones",
+ "urlRequired": "Se requiere al menos una URL"
+ },
+ "renameStream": "Renombrar flujo",
+ "newStreamName": "Nuevo nombre del flujo",
+ "ffmpeg": {
+ "useFfmpegModule": "Usar modo de compatibilidad (ffmpeg)",
+ "video": "Vídeo",
+ "audio": "Audio",
+ "hardware": "Aceleración por hardware",
+ "videoCopy": "Copiar",
+ "videoH264": "Transcodificar a H.264",
+ "videoH265": "Transcodificar a H.265",
+ "videoExclude": "Excluir",
+ "audioCopy": "Copiar",
+ "audioAac": "Transcodificar a AAC",
+ "audioOpus": "Transcodificar a Opus",
+ "audioPcmu": "Transcodificar a PCM μ-law",
+ "audioPcma": "Transcodificar a PCM A-law",
+ "audioPcm": "Transcodificar a PCM",
+ "audioMp3": "Transcodificar a MP3",
+ "audioExclude": "Excluir",
+ "hardwareNone": "Sin aceleración por hardware",
+ "hardwareAuto": "Automático (recomendado)",
+ "hardwareVaapi": "VAAPI",
+ "hardwareCuda": "CUDA",
+ "hardwareV4l2m2m": "V4L2 M2M",
+ "hardwareDxva2": "DXVA2",
+ "hardwareVideotoolbox": "VideoToolbox",
+ "addVideoCodec": "Añadir códec de vídeo",
+ "addAudioCodec": "Añadir códec de audio",
+ "removeCodec": "Eliminar códec"
+ },
+ "streamNumber": "Flujo {{index}}",
+ "sourceNumber": "Origen {{index}}"
+ },
+ "configMessages": {
+ "birdseye": {
+ "objectsModeDetectDisabled": "Birdseye está configurado en modo 'objects', pero la detección de objetos está desactivada para esta cámara. La cámara no aparecerá en Birdseye."
+ },
+ "lpr": {
+ "globalDisabled": "El reconocimiento de matrículas no está habilitado a nivel global. Habilítelo en la configuración global para que funcione el reconocimiento de matrículas a nivel de cámara.",
+ "vehicleNotTracked": "El reconocimiento de matrículas requiere rastrear 'car' o 'motorcycle'. Habilita 'car' o 'motorcycle' en Objetos para esta cámara.",
+ "modelSizeLarge": "El modelo 'large' está optimizado para matrículas de varias líneas. El modelo 'small' ofrece mejor rendimiento que 'large' y debería usarse salvo que tu región use formatos de matrícula de varias líneas."
+ },
+ "audio": {
+ "noAudioRole": "Ninguna transmisión tiene definido el rol de audio. Debe habilitar el rol de audio para que funcione la detección de audio."
+ },
+ "faceRecognition": {
+ "personNotTracked": "El reconocimiento facial requiere que se realice el seguimiento del objeto 'person'. Asegúrese de que 'person' se encuentre en la lista de seguimiento de objetos.",
+ "globalDisabled": "El enriquecimiento de reconocimiento facial debe estar habilitado para que las funciones de reconocimiento facial funcionen en esta cámara.",
+ "modelSizeLarge": "El modelo 'large' requiere una GPU o NPU para ofrecer un rendimiento razonable. Usa 'small' en sistemas solo con CPU."
+ },
+ "audioTranscription": {
+ "audioDetectionDisabled": "La detección de audio no está habilitada para esta cámara. La transcripción de audio requiere que la detección de audio esté activa."
+ },
+ "snapshots": {
+ "detectDisabled": "La detección de objetos está desactivada. Las instantáneas se generan a partir de los objetos rastreados y no se crearán."
+ },
+ "detectors": {
+ "mixedTypes": "Todos los detectores deben ser del mismo tipo. Retire los detectores existentes para utilizar un tipo diferente.",
+ "mixedTypesSuggestion": "Todos los detectores deben usar el mismo tipo. Elimina los detectores existentes o selecciona {{type}}."
+ },
+ "review": {
+ "detectDisabled": "La detección de objetos está desactivada. Los elementos de revisión requieren objetos detectados para categorizar las alertas y detecciones.",
+ "recordDisabled": "La grabación está deshabilitada; no se generarán elementos de revisión.",
+ "allNonAlertDetections": "Toda la actividad que no sea de alerta se incluirá como detecciones.",
+ "genaiImageSourceRecordingsRecordDisabled": "El origen de imagen está establecido en 'recordings', pero la grabación está deshabilitada. Frigate usará imágenes de vista previa como alternativa."
+ },
+ "detect": {
+ "fpsGreaterThanFive": "No se recomienda establecer los FPS de detección por encima de 5. Valores más altos pueden causar problemas de rendimiento y no aportarán ningún beneficio.",
+ "disabled": "La detección de objetos está deshabilitada. Las instantáneas, los elementos de revisión y enriquecimientos como el reconocimiento facial, el reconocimiento de matrículas y la IA generativa no funcionarán.",
+ "resolutionShouldBeMultipleOfFour": "Para obtener mejores resultados, la anchura y la altura de detección deberían ser múltiplos de 4. Otros valores pares pueden producir artefactos visuales o una ligera distorsión en el flujo de detección.",
+ "aspectRatioMismatch": "La anchura y la altura que has introducido no coinciden con la relación de aspecto de la resolución de detección actual. Esto puede producir una imagen estirada o distorsionada.",
+ "maxFramesSet": "Establecer un número máximo de fotogramas (frames) reemplaza el comportamiento predeterminado y desactiva el seguimiento de objetos estáticos. Hay muy pocas situaciones en las que esto sea necesario; utilícelo con precaución.",
+ "squareResolution": "Una resolución de detección cuadrada es poco habitual. El ancho y la altura de detección deberían coincidir con la relación de aspecto de tu cámara (por ejemplo, 16:9), no con las dimensiones del modelo de detección de objetos. Una relación de aspecto incorrecta puede distorsionar la imagen y reducir la precisión de la detección.",
+ "resolutionHigh": "Esta resolución de detección es superior a la recomendada y puede provocar un mayor consumo de recursos sin mejorar la precisión de la detección. Para la mayoría de las cámaras se recomienda una resolución de detección de 1080p o inferior.",
+ "globalResolutionMultipleCameras": "La resolución de detección global se establece al configurar varias cámaras. A menos que todas las cámaras compartan la misma resolución y relación de aspecto, se deben definir el ancho y la altura de detección en cada cámara, de modo que se ajusten a la relación de aspecto nativa de cada una."
+ },
+ "objects": {
+ "genaiNoDescriptionsProvider": "Debes configurar un proveedor GenAI con el rol 'descriptions' para que se generen descripciones."
+ },
+ "record": {
+ "noRecordRole": "Ningún flujo tiene definido el rol de grabación. La grabación no funcionará."
+ },
+ "semanticSearch": {
+ "jinav2SmallModelSize": "El tamaño 'small' con el modelo Jina V2 tiene un alto consumo de RAM y coste de inferencia. Se recomienda el modelo 'large' con una GPU dedicada.",
+ "modelSizeIgnoredForProvider": "El tamaño del modelo solo se aplica a los modelos Jina integrados. Este valor se ignorará al usar un proveedor de embeddings GenAI."
+ },
+ "onvif": {
+ "autotrackingNoZones": "El seguimiento automático requiere al menos una zona. Define una zona para esta cámara en Máscaras / Zonas y, a continuación, establécela como zona obligatoria a continuación."
+ },
+ "ffmpeg": {
+ "hwaccelManualNotRecommended": "No son remontados los argumentos de aceleración por hardware manual. A no ser que un requisito específico exista, seleccione la preselección que coincida con su hardware."
+ }
+ },
+ "resetToDefaultDescription": "Esto restablecerá todos los ajustes de esta sección a sus valores predeterminados. Esta acción no se puede deshacer.",
+ "resetToGlobalDescription": "Esto restablecerá la configuración de esta sección a los valores predeterminados globales. Esta acción no se puede deshacer.",
+ "detectionModel": {
+ "plusActive": {
+ "description": "Esta instancia está ejecutando un modelo de Frigate+. Seleccione o cambie su modelo en la configuración de Frigate+.",
+ "title": "Gestión de modelos de Frigate+",
+ "label": "Origen del modelo actual",
+ "goToFrigatePlus": "Ir a los ajustes de Frigate+",
+ "showModelForm": "Configurar un modelo manualmente"
+ }
+ },
+ "saveAllPreview": {
+ "profile": {
+ "label": "Override,Eliminar"
+ },
+ "title": "Cambios pendientes de guardar",
+ "triggerLabel": "Revisar cambios pendientes",
+ "empty": "No hay cambios pendientes.",
+ "scope": {
+ "label": "Ámbito",
+ "global": "Global",
+ "camera": "Cámara: {{cameraName}}"
+ },
+ "field": {
+ "label": "Campo"
+ },
+ "value": {
+ "label": "Nuevo valor",
+ "reset": "Restablecer"
+ }
+ },
+ "timestampPosition": {
+ "tl": "Arriba a la izquierda",
+ "tr": "Arriba a la derecha",
+ "bl": "Abajo a la izquierda",
+ "br": "Abajo a la derecha"
+ },
+ "unsavedChanges": "Tienes cambios sin guardar",
+ "confirmReset": "Confirmar restablecimiento",
+ "birdseye": {
+ "trackingMode": {
+ "objects": "Objetos",
+ "motion": "Movimiento",
+ "continuous": "Continuo"
+ },
+ "cameraOrder": {
+ "label": "Orden de cámaras",
+ "description": "Arrastra las cámaras para establecer su orden en el diseño de Birdseye.",
+ "reorderHandle": "Arrastrar para reordenar",
+ "saving": "Guardando…",
+ "saved": "Guardado"
+ }
+ },
+ "snapshot": {
+ "retainMode": {
+ "all": "Todo",
+ "motion": "Movimiento",
+ "active_objects": "Objetos activos"
+ }
+ },
+ "ui": {
+ "timeFormat": {
+ "browser": "Navegador",
+ "12hour": "12 horas",
+ "24hour": "24 horas"
+ },
+ "TimeOrDateStyle": {
+ "full": "Completo",
+ "long": "Largo",
+ "medium": "Medio",
+ "short": "Corto"
+ },
+ "unitSystem": {
+ "metric": "Métrico",
+ "imperial": "Imperial"
+ }
+ },
+ "review": {
+ "imageSource": {
+ "recordings": "Grabaciones",
+ "previews": "Vistas previas"
+ }
+ },
+ "logger": {
+ "logLevel": {
+ "debug": "Depuración",
+ "info": "Información",
+ "warning": "Advertencia",
+ "error": "Error",
+ "critical": "Crítico"
+ }
+ },
+ "modelSize": {
+ "small": "Pequeño",
+ "large": "Grande"
+ },
+ "retainMode": {
+ "all": "Todo",
+ "motion": "Movimiento",
+ "active_objects": "Objetos activos"
+ },
+ "previewQuality": {
+ "very_high": "Muy alto",
+ "high": "Alto",
+ "medium": "Medio",
+ "low": "Bajo",
+ "very_low": "Muy bajo"
+ },
+ "detectorsAndModel": {
+ "title": "Detectores y modelo",
+ "description": "Configura el backend del detector que ejecuta la detección de objetos y el modelo que utiliza. Los cambios se guardan juntos para que el detector y el modelo permanezcan sincronizados.",
+ "cardTitles": {
+ "detector": "Hardware del detector",
+ "model": "Modelo de detección"
+ },
+ "tabs": {
+ "plus": "Frigate+",
+ "custom": "Modelo personalizado"
+ },
+ "mismatch": {
+ "warning": "El modelo actual de Frigate+ “{{model}}” requiere el detector {{required}}. Selecciona un modelo compatible a continuación o cambia a Modelo personalizado antes de guardar."
+ },
+ "plusModel": {
+ "requiresDetector": "Requiere: {{detector}}",
+ "noModelSelected": "Selecciona un modelo de Frigate+"
+ },
+ "toast": {
+ "saveSuccess": "Los ajustes de detectores y modelo se han guardado. Reinicia Frigate para aplicar los cambios.",
+ "saveError": "No se pudieron guardar los ajustes del detector y del modelo"
+ },
+ "unsavedChanges": "Cambios sin guardar en el detector y el modelo",
+ "restartRequired": "Reinicio necesario (se ha cambiado el detector o el modelo)"
+ },
+ "menuDot": {
+ "overrideGlobal": "Esta sección sobrescribe la configuración global",
+ "overrideProfile": "Esta sección está sobrescrita por el perfil {{profile}}",
+ "unsaved": "Esta sección tiene cambios sin guardar"
}
}
diff --git a/web/public/locales/es/views/system.json b/web/public/locales/es/views/system.json
index 6c211a77c4..09f26febd4 100644
--- a/web/public/locales/es/views/system.json
+++ b/web/public/locales/es/views/system.json
@@ -45,10 +45,20 @@
"reviews": "Revisiones",
"face_recognition": "Reconocimiento facial",
"camera_activity": "Actividad de cámara",
- "classification": "Clasificación"
+ "classification": "Clasificación",
+ "system": "Sistema",
+ "camera": "Cámara",
+ "all_cameras": "Todas las cámaras",
+ "cameras_count_one": "{{count}} Cámara",
+ "cameras_count_other": "{{count}} Cámaras",
+ "lpr": "Reconocimiento de matriculas"
},
"count_other": "{{count}} mensajes",
- "count_one": "{{count}} mensaje"
+ "count_one": "{{count}} mensaje",
+ "empty": "No se han capturado mensaje aún",
+ "expanded": {
+ "payload": "Carga útil"
+ }
}
},
"title": "Sistema",
@@ -71,16 +81,16 @@
"gpuInfo": {
"vainfoOutput": {
"title": "Salida de Vainfo",
- "returnCode": "Código de Retorno: {{code}}",
- "processOutput": "Salida del Proceso:",
- "processError": "Error del Proceso:"
+ "returnCode": "Código de retorno: {{code}}",
+ "processOutput": "Salida del proceso:",
+ "processError": "Error del proceso:"
},
"nvidiaSMIOutput": {
- "cudaComputerCapability": "Capacidad de Cómputo CUDA: {{cuda_compute}}",
+ "cudaComputerCapability": "Capacidad de cómputo CUDA: {{cuda_compute}}",
"title": "Salida de Nvidia SMI",
"driver": "Controlador: {{driver}}",
"name": "Nombre: {{name}}",
- "vbios": "Información de VBios: {{vbios}}"
+ "vbios": "Informe de VBios: {{vbios}}"
},
"toast": {
"success": "Información de GPU copiada al portapapeles"
@@ -94,12 +104,15 @@
},
"gpuMemory": "Memoria de GPU",
"npuMemory": "Memoria de NPU",
- "npuUsage": "Uso de NPU",
+ "npuUsage": "Modo de empleo de NPU",
"intelGpuWarning": {
"title": "Aviso de estadísticas Intel GPU",
"message": "Estadísticas de GPU no disponibles",
"description": "Este es un error conocido en las herramientas de informes de estadísticas de GPU de Intel (intel_gpu_top). El error se produce y muestra repetidamente un uso de GPU del 0 %, incluso cuando la aceleración de hardware y la detección de objetos se ejecutan correctamente en la (i)GPU. No se trata de un error de Frigate. Puede reiniciar el host para solucionar el problema temporalmente y confirmar que la GPU funciona correctamente. Esto no afecta al rendimiento."
- }
+ },
+ "npuTemperature": "Temperatura NPU",
+ "gpuCompute": "Cálculo GPU / Codificación",
+ "gpuTemperature": "Temperatura GPU"
},
"otherProcesses": {
"title": "Otros Procesos",
@@ -109,7 +122,7 @@
"go2rtc": "go2rtc",
"recording": "grabación",
"review_segment": "revisar segmento",
- "embeddings": "embeddings",
+ "embeddings": "empotrados",
"audio_detector": "detector de audio"
}
}
@@ -136,7 +149,11 @@
},
"shm": {
"title": "Asignación de SHM (memoria compartida)",
- "warning": "El tamaño actual de SHM de {{total}}MB es muy pequeño. Aumente al menos a {{min_shm}}MB."
+ "warning": "El tamaño actual de SHM de {{total}}MB es muy pequeño. Aumente al menos a {{min_shm}}MB.",
+ "frameLifetime": {
+ "title": "Tiempo de vida del fotograma",
+ "description": "Cada cámara tiene espacio en la memoria compartida para {{frames}} cuadros. Si la velocidad de cuadros de la cámara es alta, cada cuadro se guarda aproximadamente {{lifetime}} antes de ser sobreescrito."
+ }
}
},
"cameras": {
@@ -148,8 +165,8 @@
"codec": "Codec:",
"fetching": "Obteniendo Datos de la Cámara",
"stream": "Flujo {{idx}}",
- "video": "Video:",
- "fps": "FPS:",
+ "video": "Vídeo:",
+ "fps": "CPS:",
"resolution": "Resolución:",
"error": "Error: {{error}}",
"unknown": "Desconocido",
@@ -157,7 +174,22 @@
"tips": {
"title": "Información de Sondeo de la Cámara"
},
- "aspectRatio": "Relación de aspecto"
+ "aspectRatio": "Relación de aspecto",
+ "keyframes": {
+ "title": "Análisis de clave fotograma",
+ "recordStream": "Flujo de grabación:",
+ "segmentLength": "Longitud de segmento en grabación:",
+ "unknown": "No pudo determinar el espaciado del fotograma.",
+ "analyzing": "Analizando fotogramas clave… {{seconds}} segundos restantes",
+ "stillAnalyzing": "Todavía analizando fotogramas clave…",
+ "keyframeCount": "Fotogramas clave observados:",
+ "observedDuration": "Duración observada:",
+ "gap": "Brecha de fotogramas clave (mín / med / máx):",
+ "recordDisabled": "La grabación está desactivada para esta cámara.",
+ "ok": "Cuadros cada ~{{seconds}}s, bueno para grabación y reproducción.",
+ "warning": "Fotograma clave escaso o variable (hueco más largo ~{{seconds}}s), probablemente un códec inteligente (H.264+/H.265+), esto no es recomendado.",
+ "error": "El intervalo entre fotogramas clave (~{{seconds}} s) supera la duración del segmento de grabación ({{segmentTime}} s). Es posible que algunos segmentos no contengan ningún fotograma clave, lo que impedirá su reproducción. Desactiva el códec inteligente (+) de la cámara o reduce el intervalo entre fotogramas clave."
+ }
},
"framesAndDetections": "Fotogramas / Detecciones",
"label": {
@@ -174,7 +206,8 @@
"cameraDetect": "{{camName}} detectar",
"cameraFramesPerSecond": "{{camName}} cuadros por segundo",
"cameraDetectionsPerSecond": "{{camName}} detecciones por segundo",
- "overallSkippedDetectionsPerSecond": "detecciones omitidas por segundo totales"
+ "overallSkippedDetectionsPerSecond": "detecciones omitidas por segundo totales",
+ "cameraGpu": "{{camName}} GPU"
},
"toast": {
"success": {
@@ -183,6 +216,20 @@
"error": {
"unableToProbeCamera": "No se pudo sondear la cámara: {{errorMessage}}"
}
+ },
+ "connectionQuality": {
+ "excellent": "Excelente",
+ "poor": "Pobre",
+ "title": "Calidad de Conexión",
+ "fps": "CPS",
+ "expectedFps": "Cuadros por segundo esperados",
+ "reconnectsLastHour": "Reconexiones (última hora)",
+ "unusable": "No usable",
+ "fair": "Normal",
+ "stallsLastHour": "Bloqueos (última hora)"
+ },
+ "noCameras": {
+ "title": "No se han encontrado cámaras"
}
},
"lastRefreshed": "Última actualización: ",
@@ -190,25 +237,25 @@
"infPerSecond": "Inferencias Por Segundo",
"embeddings": {
"plate_recognition_speed": "Velocidad de Reconocimiento de Matrículas",
- "face_embedding_speed": "Velocidad de Incrustación de Rostros",
- "image_embedding_speed": "Velocidad de Incrustación de Imágenes",
- "text_embedding_speed": "Velocidad de Incrustación de Texto",
+ "face_embedding_speed": "Velocidad de Empotrado Facial",
+ "image_embedding_speed": "Velocidad de Empotrado de Imagen",
+ "text_embedding_speed": "Velocidad de Empotrado de Texto",
"face_recognition_speed": "Velocidad de Reconocimiento Facial",
- "text_embedding": "Incrustación de Texto",
+ "text_embedding": "Empotrado de Texto",
"face_recognition": "Reconocimiento Facial",
"plate_recognition": "Reconocimiento de Matrículas",
"yolov9_plate_detection": "Detección de Matrículas YOLOv9",
- "image_embedding": "Incrustación de Imágenes",
+ "image_embedding": "Empotrado de Imagen",
"yolov9_plate_detection_speed": "Velocidad de Detección de Matrículas YOLOv9",
"review_description": "Revisión de descripción",
- "review_description_speed": "Velocidad de revisión de la descripción",
- "review_description_events_per_second": "Revisión de la descripción",
+ "review_description_speed": "Revisión de velocidad de descripción",
+ "review_description_events_per_second": "Revisión de Descripción",
"object_description": "Descripción de Objeto",
- "object_description_speed": "Velocidad de descripción de objeto",
- "object_description_events_per_second": "Descripción de objeto",
+ "object_description_speed": "Velocidad de Descripción de Objeto",
+ "object_description_events_per_second": "Descripción de Objeto",
"classification": "Clasificación de {{name}}",
- "classification_speed": "Velocidad de clasificación de {{name}}",
- "classification_events_per_second": "Clasificacion de eventos por segundo de {{name}}"
+ "classification_speed": "Velocidad de Clasificación de {{name}}",
+ "classification_events_per_second": "Clasificación de Eventos por Segundo de {{name}}"
},
"title": "Enriquecimientos",
"averageInf": "Tiempo promedio de inferencia"
@@ -217,10 +264,11 @@
"ffmpegHighCpuUsage": "{{camera}} tiene un uso elevado de CPU por FFmpeg ({{ffmpegAvg}}%)",
"detectHighCpuUsage": "{{camera}} tiene un uso elevado de CPU por detección ({{detectAvg}}%)",
"healthy": "El sistema está saludable",
- "reindexingEmbeddings": "Reindexando incrustaciones ({{processed}}% completado)",
+ "reindexingEmbeddings": "Reindexando empotrados ({{processed}}% completado)",
"detectIsSlow": "{{detect}} es lento ({{speed}} ms)",
"cameraIsOffline": "{{camera}} está desconectada",
"detectIsVerySlow": "{{detect}} es muy lento ({{speed}} ms)",
- "shmTooLow": "Asignación de /dev/shm ({{total}} MB) debe aumentarse al menos a {{min}} MB."
+ "shmTooLow": "Asignación de /dev/shm ({{total}} MB) debe aumentarse al menos a {{min}} MB.",
+ "debugReplayActive": "Sesión de depuración activa"
}
}
diff --git a/web/public/locales/et/audio.json b/web/public/locales/et/audio.json
index b0dfec6609..541ffa88ce 100644
--- a/web/public/locales/et/audio.json
+++ b/web/public/locales/et/audio.json
@@ -113,5 +113,165 @@
"quack": "Prääksumine",
"goose": "Hani",
"honk": "Kaagatamine",
- "wild_animals": "Metsloomad"
+ "wild_animals": "Metsloomad",
+ "roaring_cats": "Möirgavad kassid",
+ "roar": "Möirgamine",
+ "chirp": "Sirisemine",
+ "squawk": "Prääksatamine",
+ "pigeon": "Tuvi",
+ "coo": "Kudrutamine",
+ "crow": "Vares",
+ "caw": "Kraaksumine",
+ "owl": "Öökull",
+ "hoot": "Huikamine",
+ "flapping_wings": "Tiibade laperdamine",
+ "buzz": "Sumisemine",
+ "frog": "Konn",
+ "croak": "Krooksumine",
+ "snake": "Madu",
+ "rattle": "Kõristamine/lõgistamine",
+ "whale_vocalization": "Vaalaskala häälitsused",
+ "music": "Muusika",
+ "musical_instrument": "Pill",
+ "plucked_string_instrument": "Keelpill",
+ "guitar": "Kitarr",
+ "electric_guitar": "Elektrikitarr",
+ "bass_guitar": "Basskitarr",
+ "acoustic_guitar": "Akustiline kitarr",
+ "sitar": "Sitar",
+ "mandolin": "Mandoliin",
+ "banjo": "Bändžo",
+ "zither": "Kannel/tsitter",
+ "ukulele": "Ukulele",
+ "piano": "Klaver",
+ "electric_piano": "Elektriklaver",
+ "organ": "Orel",
+ "electronic_organ": "Elektriorel",
+ "hammond_organ": "Hammond-orel",
+ "synthesizer": "Süntesaator",
+ "sampler": "Sämpler",
+ "harpsichord": "Klavessiin",
+ "percussion": "Löökriistad",
+ "drum_kit": "Trummikomplekt",
+ "bass_drum": "Basstrumm",
+ "tambourine": "Tamburiin",
+ "glockenspiel": "Ksülofon",
+ "vibraphone": "Vibrafon (metalltorudega ksülofon)",
+ "marimba": "Marimbafon",
+ "tubular_bells": "Torukellad",
+ "gong": "Gong",
+ "orchestra": "Orkester",
+ "cello": "Tšello",
+ "pizzicato": "Pizzicato (poogenpilli sõrmega mängimine)",
+ "violin": "Viiul",
+ "string_section": "Keelpillid",
+ "trombone": "Tromboon",
+ "trumpet": "Trompet",
+ "french_horn": "Metsasarv",
+ "brass_instrument": "Puhkpillid",
+ "double_bass": "Kontrabass",
+ "wind_instrument": "Puhkpill",
+ "flute": "Flööt",
+ "saxophone": "Saksofon",
+ "clarinet": "Klarnet",
+ "harp": "Harf",
+ "bell": "Kellad",
+ "church_bell": "Kirikukell",
+ "jingle_bell": "Aisakell",
+ "bicycle_bell": "Rattakell",
+ "tuning_fork": "Helihark/kammertoon",
+ "bagpipes": "Torupillid",
+ "didgeridoo": "Didžeriduu",
+ "pop_music": "Popmuusika",
+ "hip_hop_music": "Hiphop muusika",
+ "rock_music": "Rokkmuusika",
+ "beatboxing": "Beatbox",
+ "heavy_metal": "Hevimuusika",
+ "punk_rock": "Punkrokk",
+ "grunge": "Grunge",
+ "progressive_rock": "Progressiivne rokk",
+ "rhythm_and_blues": "Rütmibluus",
+ "soul_music": "Soulmuusika",
+ "reggae": "Reggae",
+ "country": "Kantrimuusika",
+ "funk": "Funkmuusika",
+ "folk_music": "Rahvamuusika",
+ "middle_eastern_music": "Lähis-Ida muusika",
+ "jazz": "Džäss",
+ "disco": "Disko",
+ "classical_music": "Klassikaline muusika",
+ "opera": "Ooper",
+ "electronic_music": "Elektrooniline muusika",
+ "house_music": "House-muusika",
+ "techno": "Tekno",
+ "dubstep": "Dubstep",
+ "drum_and_bass": "Drum and Bass",
+ "electronic_dance_music": "Elektrooniline tantsumuusika",
+ "music_of_latin_america": "Ladina-Ameerika muusika",
+ "salsa_music": "Salsa",
+ "flamenco": "Flamenko",
+ "blues": "Bluus",
+ "music_for_children": "Lastemuusika",
+ "new-age_music": "New Age muusika",
+ "vocal_music": "Laulmine",
+ "a_capella": "A Capella",
+ "music_of_africa": "Aafrika muusika",
+ "afrobeat": "Afrobeat",
+ "christian_music": "Kristlik muusika",
+ "gospel_music": "Gospelmuusika",
+ "music_of_asia": "Aasia muusika",
+ "music_of_bollywood": "Bollywoodi muusika",
+ "ska": "Ska",
+ "carnatic_music": "Karnataka muusika",
+ "trance_music": "Trance muusika",
+ "ambient_music": "Ambient muusika",
+ "electronica": "Electronica",
+ "swing_music": "Svingmuusika",
+ "bluegrass": "Bluegrass",
+ "psychedelic_rock": "Psühhedeelne rokk",
+ "rock_and_roll": "Rock'n'roll",
+ "scratching": "Kriipimine/kraapimine",
+ "theremin": "Teremin",
+ "accordion": "Akordion",
+ "harmonica": "Suupill",
+ "wind_chime": "Tuulekell",
+ "chime": "Kelluke",
+ "traditional_music": "Traditsiooniline muusika",
+ "independent_music": "Sõltumatu muusika",
+ "song": "Laul",
+ "background_music": "Taustamuusika",
+ "lullaby": "Hällilaul",
+ "christmas_music": "Jõulumuusika",
+ "video_game_music": "Videomängude muusika",
+ "dance_music": "Tantsumuusika",
+ "wedding_music": "Pulmamuusika",
+ "happy_music": "Rõõmus muusika",
+ "sad_music": "Kurb muusika",
+ "tender_music": "Tundeline muusika",
+ "angry_music": "Vihane muusika",
+ "exciting_music": "Põnev muusika",
+ "scary_music": "Hirmutav muusika",
+ "wind": "Tuul",
+ "thunderstorm": "Äikesetorm",
+ "thunder": "Kõu/äike",
+ "water": "Vesi",
+ "rain": "Vihm",
+ "raindrop": "Vihmapiisk",
+ "spray": "Pritsimine",
+ "pump": "Pumpamine",
+ "stir": "Segamine/nihelemine",
+ "boiling": "Keemine",
+ "sonar": "Kajalood",
+ "arrow": "Nool",
+ "whoosh": "Vuhh/vuhisemine",
+ "thump": "Potsatus/mütsatus",
+ "thunk": "Põmakas",
+ "doorbell": "Uksekell",
+ "traffic_noise": "Liiklusmüra",
+ "rail_transport": "Raudteetransport",
+ "train_whistle": "Rongivile",
+ "sailboat": "Purjekas",
+ "soundtrack_music": "Filmimuusika",
+ "jingle": "Kõlisemine/tilisemine",
+ "theme_music": "Tunnusmuusika"
}
diff --git a/web/public/locales/et/common.json b/web/public/locales/et/common.json
index d066f85142..512eb6ca88 100644
--- a/web/public/locales/et/common.json
+++ b/web/public/locales/et/common.json
@@ -140,7 +140,9 @@
"gl": "Galego (galeegi keel)",
"id": "Bahasa Indonesia (indoneesia keel)",
"ur": "اردو (urdu keel)",
- "hr": "Hrvatski (horvaadi keel)"
+ "hr": "Hrvatski (horvaadi keel)",
+ "bs": "Bosanski (bosnia keel)",
+ "zhHant": "繁體中文 (hiina keel traditsiooniliste hieroglüüfidega)"
},
"system": "Süsteem",
"systemMetrics": "Süsteemi meetrika",
@@ -181,7 +183,8 @@
"classification": "Klassifikatsioon",
"chat": "Vestlus",
"actions": "Tegevused",
- "profiles": "Profiilid"
+ "profiles": "Profiilid",
+ "features": "Funktsionaalsused"
},
"unit": {
"speed": {
@@ -314,5 +317,8 @@
"pixels": "{{area}} px"
},
"no_items": "Objekte pole",
- "validation_errors": "Valideerimise vead"
+ "validation_errors": "Valideerimise vead",
+ "credentialField": {
+ "savedPlaceholder": "Salvestatud - senise kasutamiseks jäta tühjaks"
+ }
}
diff --git a/web/public/locales/et/components/camera.json b/web/public/locales/et/components/camera.json
index 5c467f81cc..27cf0a2ede 100644
--- a/web/public/locales/et/components/camera.json
+++ b/web/public/locales/et/components/camera.json
@@ -67,7 +67,10 @@
"desc": "Vali kaamerad selle grupi jaoks."
},
"icon": "Ikoon",
- "success": "Kaameragrupp ({{name}}) on salvestatud."
+ "success": "Kaameragrupp ({{name}}) on salvestatud.",
+ "showAll": "Näita kõiki kaameragruppe",
+ "showLess": "Näita vähem",
+ "editGroups": "Muuda kaameragruppe"
},
"debug": {
"options": {
diff --git a/web/public/locales/et/components/filter.json b/web/public/locales/et/components/filter.json
index 0df74f6d03..316aeebe25 100644
--- a/web/public/locales/et/components/filter.json
+++ b/web/public/locales/et/components/filter.json
@@ -4,13 +4,16 @@
"toast": {
"error": "Jälgitavate objektide kustutamine ei õnnestunud: {{errorMessage}}",
"success": "Jälgitavate objektide kustutamine õnnestus."
- }
+ },
+ "title": "Kinnita kustutamine",
+ "desc": "Nende {{objectLength}} jälgitava objekti kustutamine eemaldab tõmmise salvestuse, kõik seotud salvestatud sissekanded ja kõik seotud objekti elutsükli kirjed. Ajaloo vaates salvestatud videomaterjali nende jälgitavate objektide kohta EI kustutata. Kas soovid kindlasti jätkata? Hoia Shift -klahvi all, et seda teateakent tulevikus vahele jätta."
},
"cameras": {
"all": {
"title": "Kõik kaamerad",
"short": "Kaamerad"
- }
+ },
+ "label": "Kaamerate filter"
},
"labels": {
"all": {
@@ -38,7 +41,8 @@
"defaultView": {
"title": "Vaikimisi vaade",
"summary": "Kokkuvõte",
- "unfilteredGrid": "Filtreerimata ruudustik"
+ "unfilteredGrid": "Filtreerimata ruudustik",
+ "desc": "Kui filtreid pole valitud, näita viimaste jälgitud objektide kokkuvõtet sildi kohta või näita filtreerimata ruudustikuvaadet."
},
"gridColumns": {
"title": "Ruudustiku veerud",
@@ -48,16 +52,26 @@
"options": {
"thumbnailImage": "Pisipilt",
"description": "Kirjeldus"
- }
+ },
+ "label": "Otsinguallikas",
+ "desc": "Vali, kas soovid otsida sinu jälgitavate objektide pisipilte või kirjeldusi."
+ }
+ },
+ "date": {
+ "selectDateBy": {
+ "label": "Vali kuupäev, mille alusel tahad filtreerida"
}
}
},
"logSettings": {
"loading": {
- "title": "Laadin"
+ "title": "Laadin",
+ "desc": "Kui logipaneeli vaade on keritud lõpuni, siis kuvatakse lisanduvad logikirjed automaatselt kohe."
},
"disableLogStreaming": "Keela logi voogedastus",
- "allLogs": "Kõik logid"
+ "allLogs": "Kõik logid",
+ "label": "Logimistase filtri jaoks",
+ "filterBySeverity": "Kriitilisus filtri jaoks"
},
"classes": {
"label": "Klassid",
@@ -83,6 +97,44 @@
"estimatedSpeed": "Hinnanguline kiirus: ({{unit}})",
"features": {
"label": "Omadused",
- "hasSnapshot": "Leidub hetkvõte"
+ "hasSnapshot": "Leidub hetkvõte",
+ "hasVideoClip": "Videoklipp on olemas",
+ "submittedToFrigatePlus": {
+ "label": "Saadetud teenusesse Frigate+",
+ "tips": "Sa pead filtreerima jälgitavaid objekte, millel on tõmmis. Kui jälgitaval objektil pole tõmmist, siis teda Frigate+ teenusesse saata ei saa."
+ }
+ },
+ "attributes": {
+ "label": "Klassifitseerimisatribuudid",
+ "all": "Kõik atribuudid"
+ },
+ "sort": {
+ "label": "Järjestus",
+ "dateAsc": "Kuupäev (kasvavalt)",
+ "dateDesc": "Kuupäev (kahanevalt)",
+ "scoreAsc": "Objekti punktiskoor (kasvavalt)",
+ "scoreDesc": "Objekti punktiskoor (kahanevalt)",
+ "speedAsc": "Hinnanguline kiirus (kasvavalt)",
+ "speedDesc": "Hinnanguline kiirus (kahanevalt)",
+ "relevance": "Teemakohasus"
+ },
+ "review": {
+ "showReviewed": "Näita ülevaadatuid"
+ },
+ "motion": {
+ "showMotionOnly": "Näita vaid liikumisega klippe"
+ },
+ "zoneMask": {
+ "filterBy": "Tsoonimask filtri jaoks"
+ },
+ "recognizedLicensePlates": {
+ "title": "Tuvastatud sõiduki numbrimärgid",
+ "loadFailed": "Tuvastatud sõiduki numbrimärkide laadimine ei õnnestunud.",
+ "loading": "Laadin tuvastatud sõiduki numbrimärke…",
+ "placeholder": "Sõidukite numbrimärkide otsimiseks kirjuta midagi…",
+ "noLicensePlatesFound": "Sõidukite numbrimärke ei leidu.",
+ "selectPlatesFromList": "Vali loendist üks või enam sõiduki numbrimärki.",
+ "selectAll": "Vali kõik",
+ "clearAll": "Eemalda kõik"
}
}
diff --git a/web/public/locales/et/components/player.json b/web/public/locales/et/components/player.json
index 76d41dd28b..7553e3c6a1 100644
--- a/web/public/locales/et/components/player.json
+++ b/web/public/locales/et/components/player.json
@@ -4,7 +4,8 @@
"noPreviewFoundFor": "{{cameraName}} kaamera eelvaadet ei leidu",
"submitFrigatePlus": {
"submit": "Saada",
- "title": "Kas saadad selle kaadri Frigate+ teenusesse?"
+ "title": "Kas saadad selle kaadri Frigate+ teenusesse?",
+ "previewError": "Hetktõmmise eelvaate laadimine ei õnnestu. Salvestus ei pruugi olla hetkel saadaval."
},
"cameraDisabled": "Kaamera on kasutuselt eemaldatud",
"stats": {
@@ -47,5 +48,6 @@
"error": {
"submitFrigatePlusFailed": "Kaadri saatmine Frigate+ teenusesse ei õnnestunud"
}
- }
+ },
+ "cameraOff": "Kaamera on lülitatud välja"
}
diff --git a/web/public/locales/et/config/cameras.json b/web/public/locales/et/config/cameras.json
index c2ff153faa..157daddb4c 100644
--- a/web/public/locales/et/config/cameras.json
+++ b/web/public/locales/et/config/cameras.json
@@ -2,5 +2,38 @@
"name": {
"label": "Kaamera nimi",
"description": "Kaamera nimi on nõutav"
+ },
+ "friendly_name": {
+ "label": "Sõbralik nimi",
+ "description": "Frigate UI-s kasutatud kaamerasõbralik nimi"
+ },
+ "enabled": {
+ "label": "Kasutusel",
+ "description": "Kasutusel"
+ },
+ "audio": {
+ "label": "Helisündmused"
+ },
+ "birdseye": {
+ "mode": {
+ "label": "Jälgimisrežiim"
+ }
+ },
+ "label": "Kaameraseadistus",
+ "semantic_search": {
+ "triggers": {
+ "threshold": {
+ "description": "Minimaalne sarnasuse punktiskoor (0-1), mis on vajalik selle päästiku käivitamiseks."
+ }
+ }
+ },
+ "lpr": {
+ "label": "Sõidukite numbrimärkide tuvastus",
+ "description": "Sõidukite numbrimärkide tuvastuse seadistus sisaldab tuvastuse lävendeid, vormindust ja teadaolevaid numbrimärke."
+ },
+ "review": {
+ "genai": {
+ "description": "Kontrollib generatiivse tehisaru kasutamist kirjelduste ja kokkuvõtete koostamiseks ülevaatamisele kuuluvate objektide jaoks."
+ }
}
}
diff --git a/web/public/locales/et/config/global.json b/web/public/locales/et/config/global.json
index 0967ef424b..adb9777008 100644
--- a/web/public/locales/et/config/global.json
+++ b/web/public/locales/et/config/global.json
@@ -1 +1,56 @@
-{}
+{
+ "audio": {
+ "label": "Helisündmused"
+ },
+ "birdseye": {
+ "mode": {
+ "label": "Jälgimisrežiim"
+ }
+ },
+ "version": {
+ "label": "Praegune seadistuse versioon",
+ "description": "Aktiivse seadistuse numbriline või tekstiline versioon, mis aitab tuvastada vormingumuudatusi."
+ },
+ "classification": {
+ "bird": {
+ "threshold": {
+ "label": "Minimaalne punktiskoor",
+ "description": "Objekti määratlemiseks linnina vajalik mlassifitseerimise minimaalne punktiskoor."
+ }
+ },
+ "custom": {
+ "threshold": {
+ "label": "Punktiskoori lävend",
+ "description": "Punktiskoori lävend, mida kasutatakse klassifitseerimise oleku muutmiseks."
+ }
+ }
+ },
+ "semantic_search": {
+ "triggers": {
+ "threshold": {
+ "description": "Minimaalne sarnasuse punktiskoor (0-1), mis on vajalik selle päästiku käivitamiseks."
+ }
+ }
+ },
+ "face_recognition": {
+ "unknown_score": {
+ "label": "Tundmatu punktiskoori lävend"
+ }
+ },
+ "lpr": {
+ "label": "Sõidukite numbrimärkide tuvastus",
+ "description": "Sõidukite numbrimärkide tuvastuse seadistus sisaldab tuvastuse lävendeid, vormindust ja teadaolevaid numbrimärke.",
+ "enabled": {
+ "description": "Lülita sõidukite numbrimärkide tuvastus kõikide kaamerate jaoks sisse; seda saad kaamerakohaselt ka sürjutada."
+ }
+ },
+ "genai": {
+ "label": "Generatiivse tehisaru seadistus",
+ "description": "Seadistsued generatiivse tehisaru teenusepakkujate kasutamisel kirjelduste ja kokkuvõtete loomiseks ülevaatamisele kuuluvate objektide jaoks."
+ },
+ "review": {
+ "genai": {
+ "description": "Kontrollib generatiivse tehisaru kasutamist kirjelduste ja kokkuvõtete koostamiseks ülevaatamisele kuuluvate objektide jaoks."
+ }
+ }
+}
diff --git a/web/public/locales/et/config/groups.json b/web/public/locales/et/config/groups.json
index 0967ef424b..e8c7956b5d 100644
--- a/web/public/locales/et/config/groups.json
+++ b/web/public/locales/et/config/groups.json
@@ -1 +1,73 @@
-{}
+{
+ "audio": {
+ "global": {
+ "detection": "Üldine tuvastamine",
+ "sensitivity": "Üldine tundlikkus"
+ },
+ "cameras": {
+ "detection": "Tuvastamine",
+ "sensitivity": "Tundlikkus"
+ }
+ },
+ "motion": {
+ "global": {
+ "sensitivity": "Üldine tundlikkus",
+ "algorithm": "Üldine algoritm"
+ },
+ "cameras": {
+ "sensitivity": "Tundlikkus",
+ "algorithm": "Algoritm"
+ }
+ },
+ "snapshots": {
+ "global": {
+ "display": "Üldine vaade"
+ },
+ "cameras": {
+ "display": "Vaade"
+ }
+ },
+ "timestamp_style": {
+ "global": {
+ "appearance": "Üldine välimus"
+ },
+ "cameras": {
+ "appearance": "Välimus"
+ }
+ },
+ "detect": {
+ "global": {
+ "resolution": "Üldine eraldusvõime",
+ "tracking": "Üldine jälgimine"
+ },
+ "cameras": {
+ "resolution": "Eraldusvõime",
+ "tracking": "Jälgimine"
+ }
+ },
+ "objects": {
+ "global": {
+ "filtering": "Üldine filtreerimine",
+ "tracking": "Üldine jälgimine"
+ },
+ "cameras": {
+ "filtering": "Filtreerimine",
+ "tracking": "Jälgimine"
+ }
+ },
+ "record": {
+ "global": {
+ "retention": "Üldine säilitamine",
+ "events": "Üldised sündmused"
+ },
+ "cameras": {
+ "retention": "Säilitamine",
+ "events": "Sündmused"
+ }
+ },
+ "ffmpeg": {
+ "cameras": {
+ "cameraFfmpeg": "Kaamerakohased FFmpegi argumendid"
+ }
+ }
+}
diff --git a/web/public/locales/et/config/validation.json b/web/public/locales/et/config/validation.json
index 0967ef424b..c9eb465ed5 100644
--- a/web/public/locales/et/config/validation.json
+++ b/web/public/locales/et/config/validation.json
@@ -1 +1,35 @@
-{}
+{
+ "minimum": "Peab olema vähemalt {{limit}}",
+ "maximum": "Võib olla kuni {{limit}}",
+ "exclusiveMinimum": "Peab olema suurem, kui {{limit}}",
+ "exclusiveMaximum": "Peab olema väiksem, kui {{limit}}",
+ "minLength": "Peab olema vähemalt {{limit}} tähemärk(i) pikk",
+ "maxLength": "Võib olla kuni {{limit}} tähemärk(i) pikk",
+ "minItems": "Peab sisaldama vähemalt {{limit}} objekti",
+ "maxItems": "Võib sisaldada kuni {{limit}} objekti",
+ "pattern": "Vigane vorming",
+ "required": "See väli on kohustuslik",
+ "type": "Vigane väärtuse tüüp",
+ "enum": "Peab olema üks lubatud väärtustest",
+ "const": "Väärtus ei vasta eeldatud konstandile",
+ "uniqueItems": "Kõik väärtused peavad olema unikaalsed",
+ "format": "Vigane vorming",
+ "additionalProperties": "Tundmatu omadus pole lubatud",
+ "oneOf": "Peab vastama täpselt ühele lubatud skeemile",
+ "anyOf": "Peab vastama vähemalt ühele lubatud skeemile",
+ "proxy": {
+ "header_map": {
+ "roleHeaderRequired": "Kui rollide vastendused on seadistatud, siis rollide päis on nõutav."
+ }
+ },
+ "ffmpeg": {
+ "inputs": {
+ "rolesUnique": "Iga rolli saad määrata ühele sisendvoole.",
+ "detectRequired": "„Tuvasta“ rollile pead määrama vähemalt ühe sisendvoo.",
+ "hwaccelDetectOnly": "Vaid „Tuvasta“ rolliga sisendvoog võib määratleda raudvaralise kiirenduse argumente."
+ }
+ },
+ "detect": {
+ "dimensionMustBeEven": "Peab olema paarisarv."
+ }
+}
diff --git a/web/public/locales/et/objects.json b/web/public/locales/et/objects.json
index 5cd7398b39..2e7e6c5aa5 100644
--- a/web/public/locales/et/objects.json
+++ b/web/public/locales/et/objects.json
@@ -121,5 +121,10 @@
"royal_mail": "Royal Mail",
"school_bus": "Koolibuss",
"skunk": "Vinukloom (skunk)",
- "kangaroo": "Känguru"
+ "kangaroo": "Känguru",
+ "baby": "Väikelaps",
+ "baby_stroller": "Lapsevanker",
+ "rickshaw": "Rikša",
+ "Rodent": "Näriline",
+ "rodent": "Näriline"
}
diff --git a/web/public/locales/et/views/chat.json b/web/public/locales/et/views/chat.json
new file mode 100644
index 0000000000..ca3f7f6e4d
--- /dev/null
+++ b/web/public/locales/et/views/chat.json
@@ -0,0 +1,10 @@
+{
+ "documentTitle": "Frigate - vestlus tehisaruga",
+ "title": "Vestlus tehisaruga Frigate'is",
+ "subtitle": "Tehisaru abil töötav abiline kaamerate haldamiseks ja analüüside koostamiseks",
+ "placeholder": "Küsi mida iganes…",
+ "error": "Midagi läks valesti. Palun proovi uuesti.",
+ "processing": "Töötlen…",
+ "toolsUsed": "Kasutatud: {{tools}}",
+ "similarity_score": "Sarnasus"
+}
diff --git a/web/public/locales/et/views/classificationModel.json b/web/public/locales/et/views/classificationModel.json
index 93db04cba1..36a4ef618c 100644
--- a/web/public/locales/et/views/classificationModel.json
+++ b/web/public/locales/et/views/classificationModel.json
@@ -7,7 +7,7 @@
},
"documentTitle": "Klassifitseerimise mudelid - Frigate",
"details": {
- "scoreInfo": "Skoor näitab selle objekti kõigi tuvastuste keskmist klassifitseerimise usaldusväärsust.",
+ "scoreInfo": "Punktiskoor näitab selle objekti kõigi tuvastuste keskmist klassifitseerimise usaldusväärsust.",
"none": "Puudub",
"unknown": "Pole teada"
},
diff --git a/web/public/locales/et/views/events.json b/web/public/locales/et/views/events.json
index 75e4a3d5ce..b8f0e3ff6d 100644
--- a/web/public/locales/et/views/events.json
+++ b/web/public/locales/et/views/events.json
@@ -34,7 +34,9 @@
"normalActivity": "Tavaline",
"needsReview": "Vajab ülevaatamist",
"securityConcern": "Võib olla turvaprobleem",
- "timeline": "Ajajoon",
+ "timeline": {
+ "label": "Ajajoon"
+ },
"timeline.aria": "Vali ajajoon",
"zoomIn": "Suumi sisse",
"zoomOut": "Suumi välja",
@@ -53,7 +55,9 @@
},
"documentTitle": "Ülevaatamine - Frigate",
"recordings": {
- "documentTitle": "Salvestised - Frigate"
+ "documentTitle": "Salvestised - Frigate",
+ "invalidSharedLink": "Töötlemisvea tõttu ei õnnestu avada ajatempliga salvestuse linki.",
+ "invalidSharedCamera": "Tundmatu või volituseta kaamera tõttu ei õnnestu avada ajatempliga salvestuse linki."
},
"calendarFilter": {
"last24Hours": "Viimased 24 tundi"
@@ -61,5 +65,28 @@
"objectTrack": {
"clickToSeek": "Klõpsa siia ajapunkti kerimiseks",
"trackedPoint": "Jälgitav punkt"
+ },
+ "motionSearch": {
+ "menuItem": "Liikumise otsing",
+ "openMenu": "Kaamera valikud"
+ },
+ "motionPreviews": {
+ "menuItem": "Vaata liikumiste eelvaateid",
+ "title": "Liikumiste eelvaated: {{camera}}",
+ "mobileSettingsTitle": "Liikumiste eelvaadete seadistused",
+ "mobileSettingsDesc": "Kohenda taasesituse kiirust ja heledust ning vali kuupäev, et vaadata läbi ainult liikumist kajastavaid klipid.",
+ "dim": "Hämarus",
+ "dimAria": "Muuda hämarust",
+ "dimDesc": "Kohenda hämarust parandamaks liikumisala nähtavust.",
+ "speed": "Kiirus",
+ "speedAria": "Vali eelvaate taasesituse kiirus",
+ "speedDesc": "Määratle kiirus, millega eelvaate klippe näidatakse.",
+ "back": "Tagasi",
+ "empty": "Ühtegi eelvaadet pole saadaval",
+ "noPreview": "Eelvaade pole saadaval",
+ "seekAria": "Keri „{{camera}}“ kaamera vaade ajatempli juurde: {{time}}",
+ "filter": "Filtreeri",
+ "filterDesc": "Näitamaks ainult liikumisega klippe antud aladel, vali soovitud piirkonnad.",
+ "filterClear": "Tühjenda"
}
}
diff --git a/web/public/locales/et/views/explore.json b/web/public/locales/et/views/explore.json
index 1676f3ccdb..dee52f6e20 100644
--- a/web/public/locales/et/views/explore.json
+++ b/web/public/locales/et/views/explore.json
@@ -35,8 +35,13 @@
"zones": "Tsoonid",
"ratio": "Suhtarv",
"area": "Ala",
- "score": "Punktiskoor"
- }
+ "score": "Punktiskoor",
+ "computedScore": "Arvutatud punktiskoor",
+ "topScore": "Suuremad punktiskoorid"
+ },
+ "external": "{{label}} on tuvastatud",
+ "heard": "{{label}} on kuuldud",
+ "gone": "{{label}} on jäänud"
},
"title": "Jälgimise üksikasjad",
"noImageFound": "Selle ajatempli kohta ei leidu pilti.",
@@ -44,7 +49,8 @@
"carousel": {
"previous": "Eelmine slaid",
"next": "Järgmine slaid"
- }
+ },
+ "count": "{{first}} / {{second}}"
},
"documentTitle": "Avasta - Frigate",
"generativeAI": "Generatiivne tehisaru",
@@ -75,13 +81,34 @@
"mismatch_other": "Tuvastasin {{count}} võõrast objekti ja need on lisatud ülevaatamiseks. Need objektid kas ei ole piisavad häire või tuvastamise jaoks, aga ka võivad juba olla eemaldatud või kustutatud."
},
"title": "Vaata objekti üksikasju",
- "desc": "Vaata objekti üksikasju"
+ "desc": "Vaata objekti üksikasju",
+ "toast": {
+ "success": {
+ "updatedLPR": "Sõiduki numbrimärgi uuendamine õnnestus."
+ },
+ "error": {
+ "updatedLPRFailed": "Sõiduki numbrimärgi uuendamine ei õnnestunud: {{errorMessage}}"
+ }
+ }
},
"snapshotScore": {
- "label": "Hetkvõttete punktiskoor"
+ "label": "Hetkvõtete punktiskoor"
},
"regenerateFromSnapshot": "Loo uuesti hetkvõttest",
- "timestamp": "Ajatampel"
+ "timestamp": "Ajatampel",
+ "score": {
+ "label": "Punktiskoor"
+ },
+ "scoreInfo": "Punktiskoori teave",
+ "editLPR": {
+ "title": "Muuda sõiduki numbrimärki",
+ "desc": "Sisesta sõiduki numbrimärgi uus väärtus: {{label}}",
+ "descNoLabel": "Sisesta sõiduki numbrimärgi uus väärtus selle jälgitava objekti jaoks"
+ },
+ "recognizedLicensePlate": "Tuvastatud sõiduki numbrimärk",
+ "description": {
+ "aiTips": "Frigate ei küsi sinu generatiivse tehisaru teenusepakkujalt kirjeldust enne, kui jälgitava objekti elutsükkel on lõppenud."
+ }
},
"trackedObjectDetails": "Jälgitava objekti üksikasjad"
}
diff --git a/web/public/locales/et/views/faceLibrary.json b/web/public/locales/et/views/faceLibrary.json
index 7e47792d45..11a3fb104d 100644
--- a/web/public/locales/et/views/faceLibrary.json
+++ b/web/public/locales/et/views/faceLibrary.json
@@ -18,14 +18,16 @@
},
"toast": {
"error": {
- "addFaceLibraryFailed": "Näo sidumine nimega ei õnnestunud: {{errorMessage}}"
+ "addFaceLibraryFailed": "Näo sidumine nimega ei õnnestunud: {{errorMessage}}",
+ "updateFaceScoreFailed": "Näo punktiskoori uuendamine ei õnnestunud: {{errorMessage}}"
},
"success": {
"addFaceLibrary": "Lisamine Näoteeki õnnestus: {{name}}!",
"deletedFace_one": "{{count}} näo kustutamine õnnestus.",
"deletedFace_other": "{{count}} näo kustutamine õnnestus.",
"deletedName_one": "{{count}} näo kustutamine õnnestus.",
- "deletedName_other": "{{count}} näo kustutamine õnnestus."
+ "deletedName_other": "{{count}} näo kustutamine õnnestus.",
+ "updatedFaceScore": "Näo punktiskoori uuendamine õnnestus: {{name}} ({{score}})."
}
},
"deleteFaceAttempts": {
@@ -34,6 +36,11 @@
},
"details": {
"timestamp": "Ajatampel",
- "unknown": "Pole teada"
+ "unknown": "Pole teada",
+ "scoreInfo": "Punktiskoor on kõigi nägude hinnete kaalutud keskmine, kus kaalukoefitsiendiks on iga pildi näo suurus."
+ },
+ "uploadFaceImage": {
+ "title": "Laadi näopilt üles",
+ "desc": "Laadi üles pilt, et otsida sellelt nägusid ja lisada see {{pageToggle}}'i jaoks"
}
}
diff --git a/web/public/locales/et/views/live.json b/web/public/locales/et/views/live.json
index 891568c4de..e7f050758b 100644
--- a/web/public/locales/et/views/live.json
+++ b/web/public/locales/et/views/live.json
@@ -12,9 +12,12 @@
"transcription": "Heli üleskirjutus",
"snapshots": "Hetkvõtted",
"autotracking": "Automaatne jälgimine",
- "recording": "Salvestus"
+ "recording": "Salvestus",
+ "camera": "Kaamera"
+ },
+ "documentTitle": {
+ "default": "Frigate reaalajas"
},
- "documentTitle": "Otseülekanne - Frigate",
"documentTitle.withCamera": "{{camera}} - Otseülekanne - Frigate",
"lowBandwidthMode": "Väikese ribalaiusega režiim",
"twoWayTalk": {
@@ -30,7 +33,8 @@
"clickMove": {
"label": "Kaamerapildi joondamiseks keskele klõpsa kaadris",
"enable": "Kasuta klõpsamisega teisaldamist",
- "disable": "Ära kasuta klõpsamisega teisaldamist"
+ "disable": "Ära kasuta klõpsamisega teisaldamist",
+ "enableWithZoom": "Luba liigutamine klõpsuga / suumimine lohistamisega"
},
"left": {
"label": "Pööra liigutatavat kaamerat vasakule"
@@ -70,7 +74,9 @@
},
"camera": {
"enable": "Lülita kaamera sisse",
- "disable": "Lülita kaamera välja"
+ "disable": "Lülita kaamera välja",
+ "turnOn": "Lülita kaamera sisse",
+ "turnOff": "Lülita kaamera välja"
},
"detect": {
"enable": "Lülita tuvastamine sisse",
@@ -78,7 +84,8 @@
},
"recording": {
"enable": "Lülita salvestamine sisse",
- "disable": "Lülita salvestamine välja"
+ "disable": "Lülita salvestamine välja",
+ "disabledInConfig": "Pead olema selle kaamera jaoks Seadistustest määranud salvestamise."
},
"snapshots": {
"enable": "Lülita hetkvõtted sisse",
@@ -100,11 +107,18 @@
},
"audio": {
"available": "Selles voogedastuses on heliriba saadaval",
- "unavailable": "Selles voogedastuses pole heliriba saadaval"
+ "unavailable": "Selles voogedastuses pole heliriba saadaval",
+ "tips": {
+ "title": "Heli peab tulema sinu kaamerast ja selle voogedastuse jaoks peab see go2rtc-s olema seadistatud."
+ }
},
"title": "Voogedastus",
"lowBandwidth": {
- "resetStream": "Lähtesta voogedastus"
+ "resetStream": "Lähtesta voogedastus",
+ "tips": "Reaalaja pilt on puhverdamise või voogedastuse vigade tõttu madala ribalaiusega režiimis."
+ },
+ "debug": {
+ "picker": "Voogedastuse osa valik pole silumisrežiimis saadaval. Silumisvaade kasutab alati voogedastust, millele on määratud tuvastamisroll."
}
},
"notifications": "Teavitused",
@@ -137,7 +151,15 @@
"showStats": {
"label": "Näita statistikat",
"desc": "Selle eelistuse puhul näidatakse voogedastuse statistikat kaamerapildi peal."
- }
+ },
+ "tips": "Laadi alla hetktõmmis või käivita käsitsi sündmus vastavalt selle kaamera salvestiste säilitamise seadistustele.",
+ "start": "Alusta tellimuspõhist salvestamist",
+ "started": "Alustasin käsitsi tellitavat salvestamist.",
+ "failedToStart": "Käsitsi tellitava salvestamise alustamine ei õnnestunud.",
+ "recordDisabledTips": "Kuna selle kaamera seadistustes on salvestamine keelatud või piiratud, siis salvestatakse ainult pilt.",
+ "end": "Lõpeta tellimuspõhine salvestamine",
+ "ended": "Lõpetasin käsitsi tellitava salvestamise.",
+ "failedToEnd": "Käsitsi tellitava salvestamise lõpetamine ei õnnestunud."
},
"noCameras": {
"buttonText": "Lisa kaamera",
@@ -174,5 +196,8 @@
},
"history": {
"label": "Näita varasemat sisu"
+ },
+ "suspend": {
+ "forTime": "Peatamise aeg: "
}
}
diff --git a/web/public/locales/et/views/motionSearch.json b/web/public/locales/et/views/motionSearch.json
new file mode 100644
index 0000000000..72d1bad347
--- /dev/null
+++ b/web/public/locales/et/views/motionSearch.json
@@ -0,0 +1,4 @@
+{
+ "documentTitle": "Liikumise tuvastus - Frigate",
+ "title": "Liikumise otsing"
+}
diff --git a/web/public/locales/et/views/replay.json b/web/public/locales/et/views/replay.json
new file mode 100644
index 0000000000..4ef29cd2b2
--- /dev/null
+++ b/web/public/locales/et/views/replay.json
@@ -0,0 +1,18 @@
+{
+ "dialog": {
+ "camera": "Lähtekaamera",
+ "timeRange": "Ajavahemik",
+ "preset": {
+ "1m": "Viimase ühe minuti jooksul",
+ "5m": "Viimase viie minuti jooksul",
+ "timeline": "Ajajoonelt",
+ "custom": "Kohandatud"
+ },
+ "startButton": "Käivita kordus",
+ "selectFromTimeline": "Vali",
+ "starting": "Käivitan kordust…",
+ "startLabel": "Algus",
+ "endLabel": "Lõpp"
+ },
+ "title": "Kordus veaotsinguks"
+}
diff --git a/web/public/locales/et/views/search.json b/web/public/locales/et/views/search.json
index 52b917d226..6388029696 100644
--- a/web/public/locales/et/views/search.json
+++ b/web/public/locales/et/views/search.json
@@ -9,7 +9,8 @@
"clear": "Tühjenda otsing",
"save": "Salvesta otsing",
"delete": "Kustuta salvestatud otsing",
- "filterInformation": "Filtri teave"
+ "filterInformation": "Filtri teave",
+ "filterActive": "Filtreid valituna"
},
"filter": {
"label": {
@@ -17,7 +18,56 @@
"cameras": "Kaamerad",
"labels": "Sildid",
"zones": "Tsoonid",
- "sub_labels": "Alamsildid"
+ "sub_labels": "Alamsildid",
+ "attributes": "Omadused",
+ "search_type": "Otsingutüüp",
+ "time_range": "Ajavahemik",
+ "before": "Enne",
+ "after": "Pärast",
+ "min_score": "Minimaalne punktiskoor",
+ "max_score": "Maksimaalne punktiskoor",
+ "min_speed": "Miinimumkiirus",
+ "max_speed": "Maksimumkiirus",
+ "recognized_license_plate": "Tuvastatud sõiduki numbrimärk",
+ "has_clip": "Klipp on olemas"
+ },
+ "searchType": {
+ "thumbnail": "Pisipilt",
+ "description": "Kirjeldus"
+ },
+ "toast": {
+ "error": {
+ "beforeDateBeLaterAfter": "„Enne“ kuupäev peab olema varasem, kui „Pärast“ kuupäev.",
+ "afterDatebeEarlierBefore": "„Pärast“ kuupäev peab olema hilisem, kui „Enne“ kuupäev.",
+ "minScoreMustBeLessOrEqualMaxScore": "Minimaalne punktiskoor peab olema väiksem või võrdne kui maksimaalne punktiskoor.",
+ "maxScoreMustBeGreaterOrEqualMinScore": "Maksimaalne punktiskoor peab olema suurem või võrdne kui minimaalne punktiskoor.",
+ "minSpeedMustBeLessOrEqualMaxSpeed": "Minimaalne kiirus peab olema väiksem või võrdne kui maksimaalne kiirus.",
+ "maxSpeedMustBeGreaterOrEqualMinSpeed": "Maksimaalne kiirus peab olema suurem või võrdne kui minimaalne kiirus."
+ }
+ },
+ "tips": {
+ "title": "Kuidas saad kasutada tekstifiltreid",
+ "desc": {
+ "text": "Filtrid aitavad sul otsingutulemusi kitsendada. Siin on juhised nende kasutamiseks sisestusväljal:",
+ "step1": "Sisesta filtri nimi, millele järgnev koolon (nt, „cameras:“).",
+ "step2": "Vali soovitatud väärtus või sisesta enda oma.",
+ "step3": "Kasuta mitmeid filtreid lisades neid üksteise järgi ning eraldades tühikuga.",
+ "step4": "Kuupäevafiltrid (before: ja after:) kasutavad {{DateFormat}} vormingut.",
+ "step5": "Ajavahemiku filter kasutab {{exampleTime}} vormingut.",
+ "step6": "Filtreid saad eemaldada klõpsates nende kõrval leiduvad märget „x“.",
+ "exampleLabel": "Näide:"
+ }
+ },
+ "header": {
+ "currentFilterType": "Filtri väärtused",
+ "noFilters": "Filtrid",
+ "activeFilters": "Aktiivsed filtrid"
}
+ },
+ "trackedObjectId": "Jälgitava objekti tunnus",
+ "similaritySearch": {
+ "title": "Sarnaste objektide otsing",
+ "active": "Sarnaste objektide otsing on aktiivne",
+ "clear": "Eemalda sarnaste objektide otsing"
}
}
diff --git a/web/public/locales/et/views/settings.json b/web/public/locales/et/views/settings.json
index a5b2c76700..4c2e46cd1a 100644
--- a/web/public/locales/et/views/settings.json
+++ b/web/public/locales/et/views/settings.json
@@ -115,11 +115,13 @@
"placeholder": "Sisesta oma senine salasõna"
},
"user": {
- "title": "Kasutajanimi"
+ "title": "Kasutajanimi",
+ "desc": "Lubatud on vaid tähed, numbrid, punktid ja alakriipsud."
}
},
"createUser": {
- "confirmPassword": "Palun kinnita oma uus salasõna"
+ "confirmPassword": "Palun kinnita oma uus salasõna",
+ "usernameOnlyInclude": "Kasutajanimes võivad olla vaid tähed, numbrid, punkt (.) või alakriips (_)"
},
"passwordSetting": {
"cannotBeEmpty": "Salasõna ei või jääda tühjaks",
@@ -213,6 +215,14 @@
"pathPlaceholder": "rtsp://...",
"roles": "Rollid"
}
+ },
+ "clone": {
+ "categories": {
+ "items": {
+ "lpr": "Sõidukite numbrimärkide tuvastus",
+ "genai": "Generatiivne tehisaru"
+ }
+ }
}
},
"notification": {
@@ -315,6 +325,10 @@
"form": {
"cameras": {
"title": "Kaamerad"
+ },
+ "role": {
+ "desc": "Lubatud on vaid tähed, numbrid, punktid ja alakriipsud.",
+ "roleOnlyInclude": "Rolli nimes võivad olla vaid tähed, numbrid, punkt (.) või alakriips (_)"
}
}
}
@@ -330,7 +344,36 @@
"notifications": "Teavitused",
"frigateplus": "Frigate+",
"cameraReview": "Ülevaatamine",
- "profiles": "Profiilid"
+ "profiles": "Profiilid",
+ "integrationLpr": "Sõidukite numbrimärkide tuvastus",
+ "cameraLpr": "Sõidukite numbrimärkide tuvastus",
+ "uiSettings": "Kasutajaliidese seadistused",
+ "globalDetect": "Objektide tuvastamine",
+ "globalRecording": "Salvestamine",
+ "globalSnapshots": "Hetkvõtted",
+ "globalFfmpeg": "FFmpeg",
+ "globalMotion": "Liikumise tuvastus",
+ "globalObjects": "Objektid",
+ "globalReview": "Ülevaatamine",
+ "globalAudioEvents": "Heli tuvastus",
+ "globalLivePlayback": "Reaalajas sisu taasesitus",
+ "globalTimestampStyle": "Ajatempli stiil",
+ "systemDatabase": "Andmebaas",
+ "systemTls": "TLS",
+ "systemAuthentication": "Autentimine",
+ "systemNetworking": "Võrgundus",
+ "systemProxy": "Proksiserver",
+ "systemUi": "Kasutajaliides",
+ "systemLogging": "Logimine",
+ "systemEnvironmentVariables": "Keskkonnamuutujad",
+ "systemTelemetry": "Telemeetria",
+ "systemBirdseye": "Vaade linnulennult",
+ "systemFfmpeg": "FFmpeg",
+ "systemDetectorsAndModel": "Tuvastamine ja mudelid",
+ "systemMqtt": "MQTT",
+ "systemGo2rtcStreams": "go2rtc voogedastus",
+ "integrationSemanticSearch": "Semantiline otsing",
+ "integrationGenerativeAi": "Generatiivne tehisaru"
},
"dialog": {
"unsavedChanges": {
@@ -370,6 +413,9 @@
},
"birdClassification": {
"title": "Lindude klassifikatsioon"
+ },
+ "licensePlateRecognition": {
+ "title": "Sõidukite numbrimärkide tuvastus"
}
},
"cameraReview": {
@@ -377,6 +423,13 @@
"title": "Ülevaatamine",
"alerts": "Hoiatused ",
"detections": "Tuvastamise tulemused "
+ },
+ "object_descriptions": {
+ "title": "Generatiivse tehisaru objektikirjeldused",
+ "desc": "Luba/keela ajutiselt selle kaamera jaoks generatiivse tehisaru objektikirjeldused kuni Frigate'i taaskäivitamiseni. Kui see on keelatud, ei küsita selle kaamera jälgitavate objektide kohta tehisintellekti poolt loodud kirjeldusi."
+ },
+ "review_descriptions": {
+ "title": "Generatiivne tehisaru ülevaatamisele kuuluva sisu kirjedlused"
}
},
"motionDetectionTuner": {
@@ -404,7 +457,10 @@
"dialog": {
"form": {
"name": {
- "title": "Nimi"
+ "title": "Nimi",
+ "error": {
+ "invalidCharacters": "Välja nimes võivad olla vaid tähed, numbrid, alakriipsud (_) või sidekriipsud (-)."
+ }
},
"type": {
"title": "Tüüp"
@@ -420,5 +476,26 @@
}
}
}
+ },
+ "profiles": {
+ "nameInvalid": "Lubatud on vaid väiketähed, numbrid ja alakriipsud"
+ },
+ "go2rtcStreams": {
+ "validation": {
+ "nameInvalid": "Voogedastuse nimes on lubatud vaid tähed, numbrid alakriipsud ja sidekriipsud"
+ }
+ },
+ "configForm": {
+ "sections": {
+ "lpr": "Sõidukite numbrimärkide tuvastus"
+ }
+ },
+ "configMessages": {
+ "detect": {
+ "disabled": "Objektide tuvastamine on lülitatud välja. Hetkepildid, läbivaatamisele kuuluvad objektid ja täiendavad funktsioonid, nagu näotuvastus, sõidukite numbrimärkide tuvastus ja generatiivne tehisintellekt, ei tööta."
+ },
+ "lpr": {
+ "vehicleNotTracked": "Sõidukite numbrimärkide tuvastus eeldab, et auto või mootorratas on jälgitav. Lülita menüüst Objektid sell kaamera jaoks sisse valikud „auto“ või „mootorratas“."
+ }
}
}
diff --git a/web/public/locales/et/views/system.json b/web/public/locales/et/views/system.json
index b3bbb33aa0..810899444f 100644
--- a/web/public/locales/et/views/system.json
+++ b/web/public/locales/et/views/system.json
@@ -10,7 +10,30 @@
},
"copy": {
"label": "Kopeeri lõikelauale",
- "success": "Logid on kopeeritud lõikelauale"
+ "success": "Logid on kopeeritud lõikelauale",
+ "error": "Logide kopeerimine lõikelauale ei õnnestunud"
+ },
+ "websocket": {
+ "filter": {
+ "cameras_count_one": "{{count}} kaamera",
+ "cameras_count_other": "{{count}} kaamerat"
+ },
+ "empty": "Ühtegi sõnumit pole veel hõivatud",
+ "count_one": "{{count}} sõnum",
+ "count_other": "{{count}} sõnumit"
+ },
+ "type": {
+ "label": "Tüüp",
+ "timestamp": "Ajatempel",
+ "tag": "Silt",
+ "message": "Sõnum"
+ },
+ "tips": "Logid on serverist voogedastamisel",
+ "toast": {
+ "error": {
+ "fetchingLogsFailed": "Viga logide laadimisel: {{errorMessage}}",
+ "whileStreamingLogs": "Viga logide voogedastamisel: {{errorMessage}}"
+ }
}
},
"title": "Süsteem"
diff --git a/web/public/locales/fa/common.json b/web/public/locales/fa/common.json
index 12a5a13eba..5cd5a1e1c9 100644
--- a/web/public/locales/fa/common.json
+++ b/web/public/locales/fa/common.json
@@ -102,7 +102,8 @@
"all": "همه",
"back": "برگشت به قبل",
"show": "نمایش {{item}}",
- "none": "هیچکدام"
+ "none": "هیچکدام",
+ "other": "دیگر"
},
"list": {
"many": "{{items}}، و {{last}}",
@@ -136,7 +137,7 @@
"enabled": "فعال",
"disable": "غیرفعال کردن",
"save": "ذخیره",
- "saving": "در حال ذخیره…",
+ "saving": "در حال ذخیرهسازی…",
"copy": "کپی",
"history": "تاریخچه",
"pictureInPicture": "تصویر در تصویر",
@@ -149,7 +150,18 @@
"continue": "ادامه",
"on": "روشن",
"edit": "ویرایش",
- "suspended": "تعلیقشده"
+ "suspended": "تعلیقشده",
+ "add": "افزودن",
+ "applying": "در حال اعمال…",
+ "undo": "برگرداندن",
+ "copiedToClipboard": "در کلیپبورد کپی شد",
+ "modified": "اصلاح شده",
+ "retry": "تلاش دوباره",
+ "resetToGlobal": "بازنشانی به حالت جهانی",
+ "resetToDefault": "بازنشانی به پیشفرض",
+ "saveAll": "ذخیره همه",
+ "savingAll": "ذخیرهسازی همه…",
+ "undoAll": "بازگردانی همه"
},
"menu": {
"systemMetrics": "شاخصهای سیستم",
@@ -197,7 +209,9 @@
"ur": "اردو (زبان اردو)",
"withSystem": {
"label": "برای زبان از تنظیمات سامانه استفاده کنید"
- }
+ },
+ "zhHant": "چینی سنتی",
+ "hr": "کرواسیایی"
},
"system": "سامانه",
"systemLogs": "لاگهای سامانه",
@@ -250,7 +264,11 @@
"anonymous": "ناشناس",
"logout": "خروج",
"setPassword": "تنظیم گذرواژه"
- }
+ },
+ "profiles": "پروفایلها",
+ "actions": "اقدامها",
+ "features": "ویژگیها",
+ "chat": "چت"
},
"toast": {
"copyUrlToClipboard": "نشانی اینترنتی در کلیپبورد کپی شد.",
@@ -259,7 +277,8 @@
"error": {
"title": "ذخیرهٔ تغییرات پیکربندی ناموفق بود: {{errorMessage}}",
"noMessage": "ذخیرهٔ تغییرات پیکربندی ناموفق بود"
- }
+ },
+ "success": "تغییرات پیکربندی با موفقیت ذخیره شد."
}
},
"role": {
@@ -294,5 +313,10 @@
"readTheDocumentation": "مستندات را بخوانید",
"information": {
"pixels": "{{area}}px"
+ },
+ "no_items": "هیچ چیزی یافت نشد",
+ "validation_errors": "خطاهای اعتبارسنجی",
+ "credentialField": {
+ "savedPlaceholder": "ذخیره شد — برای نگهداشت وضعیت کنونی، خالی بگذارید"
}
}
diff --git a/web/public/locales/fa/components/camera.json b/web/public/locales/fa/components/camera.json
index 35f7ec5177..a44533d29f 100644
--- a/web/public/locales/fa/components/camera.json
+++ b/web/public/locales/fa/components/camera.json
@@ -67,7 +67,10 @@
"desc": "گزینههای پخش زنده را برای داشبورد این گروه دوربین تغییر دهید. این تنظیمات مخصوص دستگاه/مرورگر هستند. "
},
"birdseye": "نمای پرنده"
- }
+ },
+ "showAll": "نمایش تمام گروه های دوربین",
+ "showLess": "نمایش کمتر",
+ "editGroups": "ویرایش گروه های دوربین"
},
"debug": {
"options": {
@@ -81,6 +84,7 @@
"zones": "ناحیهها",
"mask": "ماسک",
"motion": "حرکت",
- "regions": "مناطق"
+ "regions": "مناطق",
+ "paths": "مسیرها"
}
}
diff --git a/web/public/locales/fa/components/player.json b/web/public/locales/fa/components/player.json
index 38e543fb1f..f28e1a1edb 100644
--- a/web/public/locales/fa/components/player.json
+++ b/web/public/locales/fa/components/player.json
@@ -4,7 +4,8 @@
"noPreviewFoundFor": "هیچ پیشنمایشی برای {{cameraName}} پیدا نشد",
"submitFrigatePlus": {
"title": "این فریم به فریگیت+ ارسال شود؟",
- "submit": "ارسال"
+ "submit": "ارسال",
+ "previewError": "نمی توان پیش نمایش عکس فوری را بارگذاری کرد. ممکن است ضبط در این زمان در دسترس نباشد."
},
"livePlayerRequiredIOSVersion": "برای این نوع پخش زنده، iOS 17.1 یا بالاتر لازم است.",
"streamOffline": {
@@ -47,5 +48,6 @@
"error": {
"submitFrigatePlusFailed": "ارسال فریم به Frigate+ ناموفق بود"
}
- }
+ },
+ "cameraOff": "دوربین خاموش است"
}
diff --git a/web/public/locales/fa/config/global.json b/web/public/locales/fa/config/global.json
index 2e17a72a74..ea5ad48461 100644
--- a/web/public/locales/fa/config/global.json
+++ b/web/public/locales/fa/config/global.json
@@ -768,5 +768,35 @@
},
"mqtt": {
"label": "MQTT یک پروتکل تبادل پیام سبک ."
+ },
+ "version": {
+ "label": "نسخه فعلی config"
+ },
+ "safe_mode": {
+ "label": "حالت امن"
+ },
+ "environment_vars": {
+ "label": "متغیر های محیطی"
+ },
+ "logger": {
+ "label": "گزارش گیری",
+ "default": {
+ "label": "سطح گزارش گیری"
+ }
+ },
+ "auth": {
+ "label": "احراز هویت",
+ "enabled": {
+ "label": "فعال سازی احراز هویت"
+ },
+ "reset_admin_password": {
+ "label": "بازنشانی رمز ادمین"
+ },
+ "trusted_proxies": {
+ "label": "پراکسی های مورد اعتماد"
+ }
+ },
+ "database": {
+ "label": "پایگاه داده"
}
}
diff --git a/web/public/locales/fa/views/chat.json b/web/public/locales/fa/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/fa/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/fa/views/exports.json b/web/public/locales/fa/views/exports.json
index c280aa5538..922569bcb2 100644
--- a/web/public/locales/fa/views/exports.json
+++ b/web/public/locales/fa/views/exports.json
@@ -1,5 +1,5 @@
{
- "search": "یافتن",
+ "search": "جستجو",
"documentTitle": "گرفتن خروجی - فریگیت",
"noExports": "هیچ خروجی یافت نشد",
"deleteExport": "حذف خروجی",
diff --git a/web/public/locales/fa/views/faceLibrary.json b/web/public/locales/fa/views/faceLibrary.json
index 18b3cfaf06..7326d3859f 100644
--- a/web/public/locales/fa/views/faceLibrary.json
+++ b/web/public/locales/fa/views/faceLibrary.json
@@ -3,7 +3,7 @@
"addFace": "با بارگزاری اولین عکستان، یک مجموعه جدید به کتابخانه چهره اضافه کنید.",
"placeholder": "نامی برای این مجموعه وارد کنید",
"invalidName": "نام نامعتبر، نام ها فقط می توانند شامل حروف، اعداد، فاصله، آپستروف، زیرخط و خط فاصله باشند.",
- "nameCannotContainHash": "نام نمیتواند شامل # باشد ."
+ "nameCannotContainHash": "نام نمیتواند شامل # باشد."
},
"details": {
"timestamp": "زمان دقیق",
@@ -48,7 +48,11 @@
"title": "تشخیصهای اخیر",
"titleShort": "اخیر",
"aria": "تشخیصهای اخیر را انتخاب کنید",
- "empty": "تلاشِ اخیر برای تشخیص چهره وجود ندارد"
+ "empty": "تلاشِ اخیر برای تشخیص چهره وجود ندارد",
+ "emptyNoLibrary": {
+ "title": "یک صورت را آپلود کنید",
+ "description": "شما باید حداقل یک چهره به کتابخانه اضافه کنید تا عملکرد تشخیص چهره کار کند."
+ }
},
"deleteFaceLibrary": {
"title": "حذف نام",
@@ -76,7 +80,8 @@
"deletedName_other": "{{count}} چهره با موفقیت حذف شدند.",
"renamedFace": "نام چهره با موفقیت به {{name}} تغییر یافت",
"trainedFace": "آموزش چهره با موفقیت انجام شد.",
- "updatedFaceScore": "امتیاز چهره با موفقیت به {{name}} ( {{score}}) بهروزرسانی شد."
+ "updatedFaceScore": "امتیاز چهره با موفقیت به {{name}} ( {{score}}) بهروزرسانی شد.",
+ "reclassifiedFace": "کلاسه بندی مجدد چهره با موفقیت انجام شد."
},
"error": {
"uploadingImageFailed": "آپلود تصویر ناموفق بود: {{errorMessage}}",
@@ -85,7 +90,10 @@
"deleteNameFailed": "حذف نام ناموفق بود: {{errorMessage}}",
"renameFaceFailed": "تغییر نام چهره ناموفق بود: {{errorMessage}}",
"trainFailed": "آموزش ناموفق بود: {{errorMessage}}",
- "updateFaceScoreFailed": "بهروزرسانی امتیاز چهره ناموفق بود: {{errorMessage}}"
+ "updateFaceScoreFailed": "بهروزرسانی امتیاز چهره ناموفق بود: {{errorMessage}}",
+ "reclassifyFailed": "کلاسه بندی مجدد برای چهره: {{errorMessage}} دچار مشکل شد"
}
- }
+ },
+ "reclassifyFaceAs": "کلاسه بندی مجدد چهره به عنوان:",
+ "reclassifyFace": "کلاسه بندی مجدد چهره"
}
diff --git a/web/public/locales/fa/views/motionSearch.json b/web/public/locales/fa/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/fa/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/fa/views/replay.json b/web/public/locales/fa/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/fa/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/fa/views/search.json b/web/public/locales/fa/views/search.json
index 007abe1068..1a72fe53c4 100644
--- a/web/public/locales/fa/views/search.json
+++ b/web/public/locales/fa/views/search.json
@@ -1,15 +1,15 @@
{
- "search": "یافتن",
+ "search": "جستجو",
"savedSearches": "جستجوهای ذخیره شده",
"searchFor": "جستجو برای {{inputValue}}",
"button": {
"clear": "پاک کردن جستجو",
- "save": "ذخیره جستوجو",
+ "save": "ذخیره جستجو",
"delete": "حذف جستجوی ذخیرهشده",
"filterInformation": "اطلاعات فیلتر",
"filterActive": "فیلترها فعالاند"
},
- "trackedObjectId": "شناسهٔ شیء ردیابیشده",
+ "trackedObjectId": "شناسهی شی ردیابیشده",
"filter": {
"label": {
"cameras": "دوربینها",
@@ -17,15 +17,15 @@
"sub_labels": "زیربرچسبها",
"attributes": "صفتها",
"search_type": "نوع جستجو",
- "time_range": "بازهٔ زمانی",
+ "time_range": "بازه زمانی",
"zones": "ناحیهها",
"before": "قبل از",
"after": "بعد از",
- "min_score": "حداقل امتیاز",
- "max_score": "حداکثر امتیاز",
- "min_speed": "حداقل سرعت",
- "max_speed": "حداکثر سرعت",
- "recognized_license_plate": "پلاک شناساییشده",
+ "min_score": "کمینه امتیاز",
+ "max_score": "بیشینه امتیاز",
+ "min_speed": "کمینه سرعت",
+ "max_speed": "بیشینه سرعت",
+ "recognized_license_plate": "پلاک شناسایی شده",
"has_clip": "دارای کلیپ",
"has_snapshot": "دارای عکس فوری"
},
@@ -40,7 +40,7 @@
}
},
"searchType": {
- "thumbnail": "پیشنمایش",
+ "thumbnail": "بندانگشتی",
"description": "توضیحات"
},
"tips": {
diff --git a/web/public/locales/fa/views/settings.json b/web/public/locales/fa/views/settings.json
index e69dd9a25f..3465fbf355 100644
--- a/web/public/locales/fa/views/settings.json
+++ b/web/public/locales/fa/views/settings.json
@@ -25,7 +25,8 @@
"users": "کاربران",
"roles": "نقشها",
"notifications": "اعلانها",
- "frigateplus": "فریگیت+"
+ "frigateplus": "فریگیت+",
+ "profiles": "پروفایلها"
},
"general": {
"title": "تنظیمات رابط کاربری",
diff --git a/web/public/locales/fi/common.json b/web/public/locales/fi/common.json
index 5cebc89399..ea16d95d81 100644
--- a/web/public/locales/fi/common.json
+++ b/web/public/locales/fi/common.json
@@ -38,11 +38,12 @@
"s": "{{time}}s",
"minute_one": "{{time}}minuutti",
"minute_other": "{{time}}minuuttia",
- "second_one": "{{time}}sekuntti",
- "second_other": "{{time}}sekunttia",
+ "second_one": "{{time}} sekunti",
+ "second_other": "{{time}} sekuntia",
"formattedTimestampHourMinute": {
"24hour": "HH:mm"
- }
+ },
+ "never": "Ei koskaan"
},
"pagination": {
"next": {
diff --git a/web/public/locales/fi/components/auth.json b/web/public/locales/fi/components/auth.json
index f81993d86b..167310fd32 100644
--- a/web/public/locales/fi/components/auth.json
+++ b/web/public/locales/fi/components/auth.json
@@ -10,6 +10,7 @@
"loginFailed": "Kirjautuminen epäonnistui",
"unknownError": "Tuntematon virhe. Tarkista logit.",
"webUnknownError": "Tuntematon virhe. Tarkista konsolilogi."
- }
+ },
+ "firstTimeLogin": "Ensimmäistä kertaa kirjautumassa sisään? Tunnukset löytyvät Frigaten lokista."
}
}
diff --git a/web/public/locales/fi/components/dialog.json b/web/public/locales/fi/components/dialog.json
index 819e4a55ea..7466d85132 100644
--- a/web/public/locales/fi/components/dialog.json
+++ b/web/public/locales/fi/components/dialog.json
@@ -6,7 +6,8 @@
"title": "Fregatti käynnistyy uudelleen",
"content": "Tämä sivu latautuu uudelleen {{countdown}} sekunnin kuluttua.",
"button": "Pakota uudelleenlataus nyt"
- }
+ },
+ "description": "Tämä sammuttaa Frigaten lyhyeksi aikaa uudelleenkäynnistyksen ajaksi."
},
"explore": {
"plus": {
diff --git a/web/public/locales/fi/components/player.json b/web/public/locales/fi/components/player.json
index e40d8b1145..ecb35f78ee 100644
--- a/web/public/locales/fi/components/player.json
+++ b/web/public/locales/fi/components/player.json
@@ -4,7 +4,8 @@
"noRecordingsFoundForThisTime": "Ei tallenteita valitulta ajalta",
"submitFrigatePlus": {
"title": "Lähetä tämä kuva Frigate+:aan?",
- "submit": "Lähetä"
+ "submit": "Lähetä",
+ "previewError": "Pysäytyskuvan esikatselua ei voi ladata. Tallenne ei ole ehkä saatavissa tällä hetkellä."
},
"livePlayerRequiredIOSVersion": "iOS 17.1 tai uudempi vaaditaan tälle suoratoistotyypille.",
"streamOffline": {
diff --git a/web/public/locales/fi/config/cameras.json b/web/public/locales/fi/config/cameras.json
index 0967ef424b..ac6cd5a49b 100644
--- a/web/public/locales/fi/config/cameras.json
+++ b/web/public/locales/fi/config/cameras.json
@@ -1 +1,22 @@
-{}
+{
+ "label": "Kamerakonfiguraatio",
+ "name": {
+ "label": "Kameran nimi",
+ "description": "Kameran nimi vaaditaan"
+ },
+ "friendly_name": {
+ "label": "Kutsumanimi",
+ "description": "Kameran kutsumanimeä käytetään Frigaten käyttöliittymässä"
+ },
+ "enabled": {
+ "description": "Käytössä"
+ },
+ "audio": {
+ "label": "Ääni tapahtumat",
+ "description": "Äänipohjaisen havaitsemisen asetukset tälle kameralle.",
+ "enabled": {
+ "label": "Ota ääni havainnointi käyttöön",
+ "description": "Ota tai poista käytöstä ääni tapahtuman havaiseminen tälle kameralle."
+ }
+ }
+}
diff --git a/web/public/locales/fi/config/global.json b/web/public/locales/fi/config/global.json
index 0967ef424b..fc3e5ac35c 100644
--- a/web/public/locales/fi/config/global.json
+++ b/web/public/locales/fi/config/global.json
@@ -1 +1,21 @@
-{}
+{
+ "version": {
+ "label": "Nykyinen konfigurointiversio"
+ },
+ "safe_mode": {
+ "label": "Vikasietotila",
+ "description": "Kun käytössä, käynnistä Frigate vikasietotilassa rajoitetuilla ominaisuuksilla vianselvitystä varten."
+ },
+ "logger": {
+ "label": "Lokitus",
+ "default": {
+ "label": "Lokituksen taso"
+ }
+ },
+ "audio": {
+ "label": "Ääni tapahtumat",
+ "enabled": {
+ "label": "Ota ääni havainnointi käyttöön"
+ }
+ }
+}
diff --git a/web/public/locales/fi/config/groups.json b/web/public/locales/fi/config/groups.json
index 0967ef424b..4e54fa92d5 100644
--- a/web/public/locales/fi/config/groups.json
+++ b/web/public/locales/fi/config/groups.json
@@ -1 +1,30 @@
-{}
+{
+ "audio": {
+ "global": {
+ "detection": "Globaali tunnistus",
+ "sensitivity": "Globaali herkkyys"
+ },
+ "cameras": {
+ "detection": "Havaitseminen",
+ "sensitivity": "Herkkyys"
+ }
+ },
+ "timestamp_style": {
+ "global": {
+ "appearance": "Globaali vaikutelma"
+ },
+ "cameras": {
+ "appearance": "Vaikutelma"
+ }
+ },
+ "motion": {
+ "global": {
+ "sensitivity": "Globaali herkkyys",
+ "algorithm": "Globaali algoritmi"
+ },
+ "cameras": {
+ "sensitivity": "Herkkyys",
+ "algorithm": "Algoritmi"
+ }
+ }
+}
diff --git a/web/public/locales/fi/config/validation.json b/web/public/locales/fi/config/validation.json
index 0967ef424b..a361e49c69 100644
--- a/web/public/locales/fi/config/validation.json
+++ b/web/public/locales/fi/config/validation.json
@@ -1 +1,13 @@
-{}
+{
+ "minimum": "Täytyy olla vähintään {{limit}}",
+ "maximum": "Täytyy olla korkeitaan {{limit}}",
+ "exclusiveMinimum": "Täytyy olla suurempi kuin {{limit}}",
+ "exclusiveMaximum": "Täytyy olla vähemmän kuin {{limit}}",
+ "minLength": "Täytyy olla vähintään {{limit}} merkkiä",
+ "maxLength": "Täytyy olla enintään {{limit}} merkkiä",
+ "minItems": "Täytyy olla vähintään {{limit}} kappaletta",
+ "maxItems": "Täytyy olla enintään {{limit}} kappaletta",
+ "pattern": "Väärä formaatti",
+ "required": "Tämä kenttä on pakollinen",
+ "type": "Väärä arvon tyyppi"
+}
diff --git a/web/public/locales/fi/views/chat.json b/web/public/locales/fi/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/fi/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/fi/views/classificationModel.json b/web/public/locales/fi/views/classificationModel.json
index 477b0e2e91..554a664dd3 100644
--- a/web/public/locales/fi/views/classificationModel.json
+++ b/web/public/locales/fi/views/classificationModel.json
@@ -2,10 +2,16 @@
"documentTitle": "Luokittelumallit - Frigate",
"details": {
"scoreInfo": "Pistemäärä edustaa tämän objektin kaikkien havaintojen keskimääräistä luokitteluvarmuutta.",
- "none": "Ei mitään"
+ "none": "Ei mitään",
+ "unknown": "Tuntematon"
},
"button": {
"deleteImages": "Poista kuvat",
- "trainModel": "Kouluta malli"
+ "trainModel": "Kouluta malli",
+ "deleteClassificationAttempts": "Poista luokitellut kuvat",
+ "deleteCategory": "Poista luokka",
+ "addClassification": "Lisää luokitus",
+ "deleteModels": "Poista mallit",
+ "editModel": "Muokkaa mallia"
}
}
diff --git a/web/public/locales/fi/views/events.json b/web/public/locales/fi/views/events.json
index 57eb44a808..4be6cc1dab 100644
--- a/web/public/locales/fi/views/events.json
+++ b/web/public/locales/fi/views/events.json
@@ -1,9 +1,12 @@
{
- "alerts": "Hälytyset",
+ "alerts": "Hälytykset",
"empty": {
"detection": "Ei havaintoja tarkastettavaksi",
"motion": "Ei liiketietoja",
- "alert": "Ei hälyytyksiä tarkastettavaksi"
+ "alert": "Ei hälyytyksiä tarkastettavaksi",
+ "recordingsDisabled": {
+ "title": "Tallenteet täytyy ottaa käyttöön"
+ }
},
"detections": "Havainnot",
"motion": {
@@ -11,7 +14,9 @@
"only": "Vain liike"
},
"allCameras": "Kaikki kamerat",
- "timeline": "Aikajana",
+ "timeline": {
+ "label": "Aikajana"
+ },
"timeline.aria": "Valitse aikajana",
"events": {
"label": "Tapahtumat",
diff --git a/web/public/locales/fi/views/exports.json b/web/public/locales/fi/views/exports.json
index 22f39ceb11..7ba06ef0e9 100644
--- a/web/public/locales/fi/views/exports.json
+++ b/web/public/locales/fi/views/exports.json
@@ -8,13 +8,21 @@
}
},
"noExports": "Ei vietyjä kohteita",
- "deleteExport": "Poista viety kohde",
+ "deleteExport": {
+ "label": "Poista vienti"
+ },
"editExport": {
"title": "Nimeä uudelleen",
"desc": "Anna uusi nimi viedylle kohteelle.",
"saveExport": "Tallenna vienti"
},
"tooltip": {
- "editName": "Muokkaa nimeä"
+ "editName": "Muokkaa nimeä",
+ "shareExport": "Jaa vienti",
+ "downloadVideo": "Lataa video"
+ },
+ "headings": {
+ "cases": "Tapaukset",
+ "uncategorizedExports": "Kategorisoimattomat viennit"
}
}
diff --git a/web/public/locales/fi/views/faceLibrary.json b/web/public/locales/fi/views/faceLibrary.json
index dc69f36940..6e1872ba3b 100644
--- a/web/public/locales/fi/views/faceLibrary.json
+++ b/web/public/locales/fi/views/faceLibrary.json
@@ -2,7 +2,8 @@
"description": {
"addFace": "Opastus: Uuden kokoelman lisääminen Kasvokirjastoon.",
"invalidName": "Virheellinen nimi. Nimi voi sisältää vain merkkejä, numeroita, välejä, heittomerkkejä, alaviivoja ja väliviivoja.",
- "placeholder": "Anna nimi kokoelmalle"
+ "placeholder": "Anna nimi kokoelmalle",
+ "nameCannotContainHash": "Nimi ei voi sisältää \"#\"."
},
"uploadFaceImage": {
"desc": "Lähetä kuva kasvojen tunnistukseen ja lisää se sivulle {{pageToggle}}",
diff --git a/web/public/locales/fi/views/live.json b/web/public/locales/fi/views/live.json
index d38703565a..93984cf502 100644
--- a/web/public/locales/fi/views/live.json
+++ b/web/public/locales/fi/views/live.json
@@ -1,5 +1,7 @@
{
- "documentTitle": "Suora - Frigate",
+ "documentTitle": {
+ "default": "Suora - Frigate"
+ },
"documentTitle.withCamera": "{{camera}} - Suora - Frigate",
"lowBandwidthMode": "Pienen kaistanleveyden tila",
"twoWayTalk": {
diff --git a/web/public/locales/fi/views/motionSearch.json b/web/public/locales/fi/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/fi/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/fi/views/replay.json b/web/public/locales/fi/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/fi/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/fi/views/settings.json b/web/public/locales/fi/views/settings.json
index df2f2eb563..c988472abc 100644
--- a/web/public/locales/fi/views/settings.json
+++ b/web/public/locales/fi/views/settings.json
@@ -8,10 +8,12 @@
"general": "Yleiset asetukset - Frigate",
"frigatePlus": "Frigate+ asetukset - Frigate",
"object": "Virheenjäljitys - Frigate",
- "authentication": "Autentikointiuasetukset - Frigate",
+ "authentication": "Autentikointiasetukset - Frigate",
"notifications": "Ilmoitusasetukset - Frigate",
"enrichments": "Laajennusasetukset – Frigate",
- "cameraManagement": "Hallitse Kameroita - Frigate"
+ "cameraManagement": "Hallitse Kameroita - Frigate",
+ "globalConfig": "Globaali konfiguraatio - Frigate",
+ "cameraConfig": "Kamera konfiguraatio - Frigate"
},
"menu": {
"ui": "Käyttöliittymä",
diff --git a/web/public/locales/fi/views/system.json b/web/public/locales/fi/views/system.json
index 04952692e2..2bb08286dd 100644
--- a/web/public/locales/fi/views/system.json
+++ b/web/public/locales/fi/views/system.json
@@ -20,6 +20,10 @@
"fetchingLogsFailed": "Virhe noudettaessa lokeja: {{errorMessage}}",
"whileStreamingLogs": "Virhe toistettaessa lokeja: {{errorMessage}}"
}
+ },
+ "websocket": {
+ "label": "Viestit",
+ "pause": "Pysäytä"
}
},
"documentTitle": {
@@ -30,7 +34,8 @@
"logs": {
"frigate": "Frigaten lokit - Frigate",
"go2rtc": "Go2RTC lokit - Frigate",
- "nginx": "Nginx lokit - Frigate"
+ "nginx": "Nginx lokit - Frigate",
+ "websocket": "Viestilokit - Frigate"
}
},
"title": "Järjestelmä",
diff --git a/web/public/locales/fr/common.json b/web/public/locales/fr/common.json
index 2ba13dd185..ebec9c47f9 100644
--- a/web/public/locales/fr/common.json
+++ b/web/public/locales/fr/common.json
@@ -1,6 +1,6 @@
{
"time": {
- "untilForRestart": "Jusqu'au redémarrage de Frigate",
+ "untilForRestart": "Jusqu'à ce que Frigate redémarre.",
"untilRestart": "Jusqu'au redémarrage",
"untilForTime": "Jusqu'à {{time}}",
"justNow": "À l'instant",
@@ -139,7 +139,9 @@
"resetToDefault": "Réinitialiser aux réglages par défaut",
"saveAll": "Tout enregistrer",
"savingAll": "Enregistrement de tout en cours…",
- "undoAll": "Tout annuler"
+ "undoAll": "Tout annuler",
+ "applying": "Enregistrement…",
+ "retry": "Réessayer"
},
"menu": {
"configuration": "Configuration",
@@ -186,7 +188,9 @@
"gl": "Galego (Galicien)",
"id": "Bahasa Indonesia (Indonésien)",
"ur": "اردو (Ourdou)",
- "hr": "Hrvatski (Croate)"
+ "hr": "Hrvatski (Croate)",
+ "bs": "Bosanski (Bosnien)",
+ "zhHant": "繁體中文 (chinois traditionnel)"
},
"appearance": "Apparence",
"darkMode": {
@@ -244,7 +248,10 @@
"faceLibrary": "Bibliothèque de visages",
"languages": "Langues",
"classification": "Classification",
- "profiles": "Profils"
+ "profiles": "Profils",
+ "actions": "Actions",
+ "features": "Fonctionnalités",
+ "chat": "Discuter"
},
"toast": {
"save": {
@@ -252,9 +259,10 @@
"error": {
"noMessage": "Echec lors de l'enregistrement des changements de configuration",
"title": "Échec de l'enregistrement des changements de configuration : {{errorMessage}}"
- }
+ },
+ "success": "Modifications enregistrées avec succès."
},
- "copyUrlToClipboard": "URL copiée dans le presse-papiers"
+ "copyUrlToClipboard": "URL copiée dans le presse-papiers."
},
"role": {
"title": "Rôle",
@@ -324,5 +332,10 @@
"two": "{{0}} et {{1}}",
"many": "{{items}}, et {{last}}",
"separatorWithSpace": ", "
+ },
+ "no_items": "Aucun élément",
+ "validation_errors": "Erreurs de validation",
+ "credentialField": {
+ "savedPlaceholder": "Enregistré — laissez vide pour conserver la version actuelle"
}
}
diff --git a/web/public/locales/fr/components/camera.json b/web/public/locales/fr/components/camera.json
index 0e95c70e32..8387b902ec 100644
--- a/web/public/locales/fr/components/camera.json
+++ b/web/public/locales/fr/components/camera.json
@@ -68,7 +68,10 @@
"placeholder": "Choisissez un flux."
},
"birdseye": "Birdseye"
- }
+ },
+ "showAll": "Afficher tout les groupes de caméras",
+ "showLess": "Réduire l'affichage",
+ "editGroups": "Modifier les groupes de caméras"
},
"debug": {
"timestamp": "Horodatage",
@@ -82,6 +85,7 @@
},
"boundingBox": "Cadre de détection",
"zones": "Zones",
- "regions": "Régions"
+ "regions": "Régions",
+ "paths": "Chemins"
}
}
diff --git a/web/public/locales/fr/components/dialog.json b/web/public/locales/fr/components/dialog.json
index a2accb9308..4e0fa88431 100644
--- a/web/public/locales/fr/components/dialog.json
+++ b/web/public/locales/fr/components/dialog.json
@@ -36,6 +36,9 @@
"ask_an": "Cet objet est-il un(e) {{label}} ?",
"ask_a": "Cet objet est-il un(e) {{label}} ?",
"ask_full": "Cet objet est-il un(e) {{translatedLabel}} ?"
+ },
+ "toast": {
+ "error": "Impossible d'envoyer à Frigate+. Merci de vérifier votre réseau puis de réessayer."
}
}
},
@@ -67,7 +70,16 @@
"noVaildTimeSelected": "La plage horaire sélectionnée n'est pas valide."
},
"success": "Exportation démarrée avec succès. Consultez le fichier sur la page des exportations.",
- "view": "Vue"
+ "view": "Vue",
+ "queued": "Exportation en attente. Consultez la progression sur la page des exportations.",
+ "batchSuccess_one": "Un export démarré. Ouverture du dossier en cours.",
+ "batchSuccess_many": "{{count}} exports démarrés. Ouverture du dossier en cours.",
+ "batchSuccess_other": "{{count}} exports démarrés. Ouverture du dossier en cours.",
+ "batchPartial": "{{successful}} exports ont démarré sur un total de : {{total}}. Échecs : {{failedCameras}}",
+ "batchFailed": "Échec du démarrage de {{total}} exports. Échecs : {{failedCameras}}",
+ "batchQueuedSuccess_one": "Un export rajouté au traitement. Ouverture du dossier en cours.",
+ "batchQueuedSuccess_many": "{{count}} exports rajoutés au traitement. Ouverture du dossier en cours.",
+ "batchQueuedSuccess_other": "{{count}} exports rajoutés au traitement. Ouverture du dossier en cours."
},
"select": "Sélectionner",
"name": {
@@ -80,7 +92,62 @@
},
"case": {
"label": "Dossier",
- "placeholder": "Sélectionner un dossier"
+ "placeholder": "Sélectionner un dossier",
+ "newCaseOption": "Créer un nouveau cas",
+ "newCaseNamePlaceholder": "Nouveau nom de cas",
+ "newCaseDescriptionPlaceholder": "Description de cas",
+ "nonAdminHelp": "Un nouveau cas sera créé pour ces exports."
+ },
+ "queueing": "Mise en file d'attente de l'export...",
+ "tabs": {
+ "export": "Caméra unique",
+ "multiCamera": "Multi-caméra"
+ },
+ "multiCamera": {
+ "timeRange": "Intervalle de temps",
+ "selectFromTimeline": "Sélectionner depuis la chronologie",
+ "cameraSelection": "Caméras",
+ "cameraSelectionHelp": "Les caméras avec des objets suivis dans cette intervalle sont pré-sélectionnées",
+ "checkingActivity": "Vérification de l'activité de la caméra...",
+ "noCameras": "Aucune caméra disponible",
+ "detectionCount_one": "{{count}} objet suivi",
+ "detectionCount_many": "{{count}} objets suivis",
+ "detectionCount_other": "{{count}} objets suivis",
+ "nameLabel": "Nom d'export",
+ "namePlaceholder": "Nom de base optionnel pour ces exports",
+ "queueingButton": "Mise en file d'attente des exports...",
+ "exportButton_one": "Exporter {{count}} caméra",
+ "exportButton_many": "Exporter {{count}} caméras",
+ "exportButton_other": "Exporter {{count}} caméras",
+ "searchOrSelectGroup": "Chercher ou sélectionner un groupe de caméras...",
+ "selectAll": "Sélectionner toutes les cameras",
+ "clearSelection": "Effacer la sélection",
+ "selectWithActivity": "Caméras avec des objets suivis",
+ "selectGroup": "Sélectionner un groupe",
+ "noMatchingCameras": "Aucune caméra ne correspond à votre recherche",
+ "selectedCount": "{{selected}} / {{total}} sélectionnées"
+ },
+ "multi": {
+ "title_one": "Export {{count}} revue",
+ "title_many": "Export {{count}} revues",
+ "title_other": "Export {{count}} revues",
+ "description": "Exporter chaque revue sélectionnée. Tous les exports sont regroupés sous un cas unique.",
+ "descriptionNoCase": "Exporter chaque revue sélectionnée.",
+ "caseNamePlaceholder": "Vérification de l'export – {{date}}",
+ "exportButton_one": "Exporter {{count}} revue",
+ "exportButton_many": "Exporter {{count}} revues",
+ "exportButton_other": "Exporter {{count}} revues",
+ "exportingButton": "Exportation...",
+ "toast": {
+ "started_one": "Un export a démarré. Ouverture du dossier en cours.",
+ "started_many": "{{count}} exports ont démarré. Ouverture du dossier en cours.",
+ "started_other": "{{count}} exports ont démarré. Ouverture du dossier en cours.",
+ "startedNoCase_one": "Un export a démarré.",
+ "startedNoCase_many": "{{count}} exports ont démarré.",
+ "startedNoCase_other": "{{count}} exports ont démarré.",
+ "partial": "{{successful}} exportations sur {{total}} lancées. Échecs : {{failedItems}}",
+ "failed": "Échec du démarrage des exports {{total}}. Échec : {{failedItems}}"
+ }
}
},
"search": {
diff --git a/web/public/locales/fr/components/player.json b/web/public/locales/fr/components/player.json
index 6450c12617..12e0108841 100644
--- a/web/public/locales/fr/components/player.json
+++ b/web/public/locales/fr/components/player.json
@@ -4,7 +4,8 @@
"noPreviewFound": "Aucun aperçu trouvé",
"submitFrigatePlus": {
"title": "Soumettre cette image à Frigate+ ?",
- "submit": "Soumettre"
+ "submit": "Soumettre",
+ "previewError": "Impossible de télécharger le snapshot. L'enregistrement ne pas disponible sur cette période."
},
"streamOffline": {
"title": "Flux hors ligne",
diff --git a/web/public/locales/fr/config/cameras.json b/web/public/locales/fr/config/cameras.json
index ca00146dba..919ee6df89 100644
--- a/web/public/locales/fr/config/cameras.json
+++ b/web/public/locales/fr/config/cameras.json
@@ -78,8 +78,8 @@
"label": "Détection d'objets",
"description": "Réglages pour la détection ou le rôle de détection utilisé pour exécuter la détection des objets et initialiser les traceurs.",
"enabled": {
- "label": "Détection activée",
- "description": "Activer ou désactiver la détection des objets pour cette caméra. La détection doit être activée pour que le suivi des objets fonctionne."
+ "label": "Activer la détection d'objet",
+ "description": "Activer ou désactiver la détection des objets pour cette caméra."
},
"height": {
"label": "Hauteur de détection",
@@ -299,6 +299,10 @@
},
"raw_mask": {
"label": "Masque brut"
+ },
+ "skip_motion_threshold": {
+ "label": "Ignorer le seuil de détection de mouvement",
+ "description": "Si une valeur entre 0,0 et 1,0 est définie, et que plus de cette fraction de l'image change en une seule trame, le détecteur ne retournera aucune zone de mouvement et se recalibrera immédiatement. Cela peut économiser du CPU et réduire les faux positifs lors d'éclairs, d'orages, etc., mais peut manquer des événements réels comme une caméra PTZ suivant automatiquement un objet. Le compromis est entre perdre quelques mégaoctets d'enregistrements ou visionner quelques courts clips. Laisser vide (None) pour désactiver cette fonctionnalité."
}
},
"objects": {
@@ -312,9 +316,17 @@
"label": "Filtres d'objets",
"description": "Filtres appliqués aux objets détectés afin de réduire les faux positifs (aire, rapport, facteur de confiance).",
"min_area": {
- "label": "Aire minimal de l'objet"
+ "label": "Aire minimal de l'objet",
+ "description": "Surface minimale de la boîte englobante (en pixels ou pourcentage) requise pour ce type d'objet. Peut être exprimée en pixels (entier) ou en pourcentage (flottant entre 0,000001 et 0,99)."
+ },
+ "max_area": {
+ "label": "Zone d'objet maximum",
+ "description": "Zone de boite englobante maximum (pixels ou pourcentage) autorisée pour ce type d'objet. Peut être en pixels (entier) ou pourcentage (décimale entre 0,000001 and 0,99)."
+ },
+ "min_ratio": {
+ "label": "Rapport d'aspect minimal"
}
}
},
- "label": "ConfigurationCamera"
+ "label": "Configuration de la caméra"
}
diff --git a/web/public/locales/fr/config/global.json b/web/public/locales/fr/config/global.json
index b3dd9d23ff..97093cc257 100644
--- a/web/public/locales/fr/config/global.json
+++ b/web/public/locales/fr/config/global.json
@@ -1,7 +1,7 @@
{
"version": {
"label": "Version actuelle de la configuration",
- "description": "Version numérique ou sous forme de chaîne de la configuration active, permettant de détecter les migrations ou les changements de format"
+ "description": "Version numérique ou textuelle de la configuration active, utilisée pour aider à détecter les migrations ou les changements de format."
},
"safe_mode": {
"label": "Mode sans échec",
@@ -77,5 +77,43 @@
"path": {
"label": "Chemin vers la base de donnée"
}
+ },
+ "genai": {
+ "provider": {
+ "label": "Fournisseur"
+ }
+ },
+ "birdseye": {
+ "quality": {
+ "label": "Qualité d'encodage"
+ }
+ },
+ "detect": {
+ "enabled": {
+ "label": "Activer la détection d'objet"
+ }
+ },
+ "motion": {
+ "skip_motion_threshold": {
+ "label": "Ignorer le seuil de détection de mouvement",
+ "description": "Si une valeur entre 0,0 et 1,0 est définie, et que plus de cette fraction de l'image change en une seule trame, le détecteur ne retournera aucune zone de mouvement et se recalibrera immédiatement. Cela peut économiser du CPU et réduire les faux positifs lors d'éclairs, d'orages, etc., mais peut manquer des événements réels comme une caméra PTZ suivant automatiquement un objet. Le compromis est entre perdre quelques mégaoctets d'enregistrements ou visionner quelques courts clips. Laisser vide (None) pour désactiver cette fonctionnalité."
+ }
+ },
+ "objects": {
+ "filters": {
+ "min_area": {
+ "description": "Surface minimale de la boîte englobante (en pixels ou pourcentage) requise pour ce type d'objet. Peut être exprimée en pixels (entier) ou en pourcentage (flottant entre 0,000001 et 0,99)."
+ },
+ "max_area": {
+ "label": "Zone d'objet maximum",
+ "description": "Zone de boite englobante maximum (pixels ou pourcentage) autorisée pour ce type d'objet. Peut être en pixels (entier) ou pourcentage (décimale entre 0,000001 and 0,99)."
+ },
+ "min_ratio": {
+ "label": "Rapport d'aspect minimal"
+ }
+ }
+ },
+ "audio_transcription": {
+ "label": "Transcription audio"
}
}
diff --git a/web/public/locales/fr/config/validation.json b/web/public/locales/fr/config/validation.json
index aa4acd887a..ac3853b8ff 100644
--- a/web/public/locales/fr/config/validation.json
+++ b/web/public/locales/fr/config/validation.json
@@ -1,6 +1,6 @@
{
"minimum": "Doit être au moins de {{limit}}",
- "maximum": "Ne doit pas dépasser {{limit}}",
+ "maximum": "Doit être au maximum {{limit}}",
"exclusiveMinimum": "Doit être supérieur à {{limit}}",
"exclusiveMaximum": "Doit être inférieur à {{limit}}",
"minLength": "Doit contenir au moins {{limit}} caractère(s)",
diff --git a/web/public/locales/fr/objects.json b/web/public/locales/fr/objects.json
index 9c9d5a6cf3..fdd517c84c 100644
--- a/web/public/locales/fr/objects.json
+++ b/web/public/locales/fr/objects.json
@@ -116,5 +116,14 @@
"dining_table": "Table à manger",
"vase": "Vase",
"purolator": "Purolator",
- "postnord": "PostNord"
+ "postnord": "PostNord",
+ "canada_post": "Poste du Canada",
+ "royal_mail": "Poste du Royaume Uni",
+ "school_bus": "Bus scolaire",
+ "skunk": "Mouffette",
+ "kangaroo": "Kangourou",
+ "baby": "Bébé",
+ "baby_stroller": "Poussette",
+ "rickshaw": "Pousse-pousse",
+ "Rodent": "Rongeur"
}
diff --git a/web/public/locales/fr/views/chat.json b/web/public/locales/fr/views/chat.json
new file mode 100644
index 0000000000..72736b3d91
--- /dev/null
+++ b/web/public/locales/fr/views/chat.json
@@ -0,0 +1,22 @@
+{
+ "documentTitle": "Messagerie - Frigate",
+ "title": "Messagerie Frigate",
+ "subtitle": "Votre assistant IA pour la gestion des camera et des aperçus",
+ "placeholder": "Posez moi toutes vos questions...",
+ "error": "Il y a eu un problème. Merci de bien vouloir réessayer.",
+ "processing": "Traitement en cours...",
+ "hideTools": "Masquer les outils",
+ "call": "Appeler",
+ "result": "Résultat",
+ "arguments": "Arguments :",
+ "response": "Réponse :",
+ "attachment_chip_label": "{{label}} sur {{camera}}",
+ "attachment_chip_remove": "Supprimer la pièce jointe",
+ "open_in_explore": "Ouvrir dans l'explorateur",
+ "attach_event_aria": "Attacher l'événement {{eventId}}",
+ "attachment_picker_paste_label": "Ou coller l'event ID",
+ "attachment_picker_placeholder": "Attacher un événement",
+ "quick_reply_find_similar": "Trouver des observations similaires",
+ "no_similar_objects_found": "Aucun objet similaire trouvé.",
+ "semantic_search_required": "La recherche sémantique doit être activée afin de trouver un objet similaire."
+}
diff --git a/web/public/locales/fr/views/explore.json b/web/public/locales/fr/views/explore.json
index 6379364508..6c116ef9c1 100644
--- a/web/public/locales/fr/views/explore.json
+++ b/web/public/locales/fr/views/explore.json
@@ -113,7 +113,8 @@
"attributes": "Attributs de classification",
"title": {
"label": "Titre"
- }
+ },
+ "scoreInfo": "Information score"
},
"type": {
"details": "détails",
@@ -222,12 +223,22 @@
"downloadCleanSnapshot": {
"label": "Télécharger l'instantané vierge",
"aria": "Télécharger l'instantané vierge"
+ },
+ "debugReplay": {
+ "label": "Relecture de débogage",
+ "aria": "Visualiser cet objet suivi dans la vue de la session de relecture de déboggage"
+ },
+ "more": {
+ "aria": "Plus"
}
},
"dialog": {
"confirmDelete": {
"title": "Confirmer la suppression",
"desc": "La suppression de cet objet suivi supprime l'instantané, les embeddings enregistrés et les entrées du cycle de vie de l'objet associé. Les images enregistrées de cet objet suivi dans la vue Chronologie NE seront PAS supprimées. Êtes-vous sûr de vouloir continuer ?"
+ },
+ "toast": {
+ "error": "Une erreur est survenue lors de la suppression de cet objet suivi : {{errorMessage}}"
}
},
"noTrackedObjects": "Aucun objet suivi trouvé",
@@ -278,7 +289,10 @@
"zones": "Zones",
"ratio": "Ratio",
"area": "Surface",
- "score": "Score"
+ "score": "Score",
+ "computedScore": "Score calculé",
+ "topScore": "Meilleur score",
+ "toggleAdvancedScores": "Afficher/masquer les scores avancés"
}
},
"annotationSettings": {
diff --git a/web/public/locales/fr/views/exports.json b/web/public/locales/fr/views/exports.json
index 9e26a27e57..f182eb60ec 100644
--- a/web/public/locales/fr/views/exports.json
+++ b/web/public/locales/fr/views/exports.json
@@ -35,5 +35,10 @@
"newCaseOption": "Créer un nouveau dossier",
"nameLabel": "Nom du dossier",
"descriptionLabel": "Description"
+ },
+ "deleteCase": {
+ "desc": "Êtes-vous sûr de vouloir supprimer {{caseName}}?",
+ "descKeepExports": "Les exports seront disponibles comme exports non catégorisés.",
+ "deleteExports": "Supprimer aussi les exports"
}
}
diff --git a/web/public/locales/fr/views/faceLibrary.json b/web/public/locales/fr/views/faceLibrary.json
index 83138d7eca..e61bfe9a2c 100644
--- a/web/public/locales/fr/views/faceLibrary.json
+++ b/web/public/locales/fr/views/faceLibrary.json
@@ -67,7 +67,8 @@
"deletedFace_many": "{{count}} visages supprimés avec succès",
"deletedFace_other": "{{count}} visages supprimés avec succès",
"trainedFace": "Visage entraîné avec succès",
- "renamedFace": "Visage renommé avec succès en {{name}}"
+ "renamedFace": "Visage renommé avec succès en {{name}}",
+ "reclassifiedFace": "Visage reclassifié avec succès."
},
"error": {
"uploadingImageFailed": "Échec du téléversement de l'image : {{errorMessage}}",
@@ -76,7 +77,8 @@
"updateFaceScoreFailed": "Échec de la mise à jour du score du visage : {{errorMessage}}",
"addFaceLibraryFailed": "Échec de l'attribution du nom au visage : {{errorMessage}}",
"deleteNameFailed": "Échec de la suppression du nom : {{errorMessage}}",
- "renameFaceFailed": "Échec du changement de nom du visage : {{errorMessage}}"
+ "renameFaceFailed": "Échec du changement de nom du visage : {{errorMessage}}",
+ "reclassifyFailed": "Échec de la reclassification du visage : {{errorMessage}}"
}
},
"trainFaceAs": "Entraîner le visage comme :",
@@ -101,5 +103,7 @@
"desc_other": "Êtes-vous sûr de vouloir supprimer {{count}} visages ? Cette action est irréversible."
},
"nofaces": "Aucun visage disponible",
- "pixels": "{{area}} pixels"
+ "pixels": "{{area}} pixels",
+ "reclassifyFaceAs": "Reclassifier le visage en :",
+ "reclassifyFace": "Reclassifier le visage"
}
diff --git a/web/public/locales/fr/views/motionSearch.json b/web/public/locales/fr/views/motionSearch.json
new file mode 100644
index 0000000000..0c99cad0c6
--- /dev/null
+++ b/web/public/locales/fr/views/motionSearch.json
@@ -0,0 +1,51 @@
+{
+ "documentTitle": "Recherche de mouvement - Frigate",
+ "title": "Recherche de mouvement",
+ "description": "Dessinez un polygone pour définir la zonecible, et spécifiez une fourchette de temps pour chercher les mouvements dans cette zone.",
+ "selectCamera": "Recherche de mouvement en cours de chargement",
+ "startSearch": "Commencer la recherche",
+ "searchStarted": "Recherche démarrée",
+ "searchCancelled": "Recherche annulée",
+ "cancelSearch": "Annuler recherche",
+ "searching": "Recherche en cours.",
+ "searchComplete": "Recherche terminée",
+ "noResultsYet": "Lancez une recherche pour trouver les changements de mouvement dans la zone sélectionnée",
+ "noChangesFound": "Aucun changement de pixel détecté dans la zone sélectionnée",
+ "changesFound_one": "{{count}} changement de mouvement détecté",
+ "changesFound_many": "{{count}} changements de mouvement détectés",
+ "changesFound_other": "{{count}} changements de mouvement détectés",
+ "framesProcessed": "{{count}} images traitées",
+ "jumpToTime": "Aller à ce moment",
+ "results": "Résultats",
+ "showSegmentHeatmap": "Carte thermique",
+ "newSearch": "Nouvelle recherche",
+ "clearResults": "Effacer les résultats",
+ "clearROI": "Effacer le polygone",
+ "polygonControls": {
+ "points_one": "{{count}} point",
+ "points_many": "{{count}} points",
+ "points_other": "{{count}} points",
+ "undo": "Annuler le dernier point",
+ "reset": "Réinitialiser le polygone",
+ "drawMode": "Dessiner",
+ "moveMode": "Déplacer"
+ },
+ "motionHeatmapLabel": "Carte thermique des mouvements",
+ "dialog": {
+ "title": "Recherche de mouvement",
+ "cameraLabel": "Caméra",
+ "previewAlt": "Aperçu de la caméra {{camera}}"
+ },
+ "timeRange": {
+ "title": "Plage de recherche",
+ "start": "Début de la plage de recherche",
+ "end": "Fin de la plage de recherche"
+ },
+ "settings": {
+ "title": "Paramètres de recherche",
+ "parallelMode": "Mode parallèle",
+ "parallelModeDesc": "Scanner plusieurs plages d'enregistrement simultanément (plus rapide ; nécessite plus de ressources de décodage)",
+ "threshold": "Seuil de sensitivité",
+ "thresholdDesc": "Des valeurs basses détectent de plus petits changements (1-255)"
+ }
+}
diff --git a/web/public/locales/fr/views/replay.json b/web/public/locales/fr/views/replay.json
new file mode 100644
index 0000000000..abafbe755f
--- /dev/null
+++ b/web/public/locales/fr/views/replay.json
@@ -0,0 +1,33 @@
+{
+ "title": "Debug - Rejouer",
+ "description": "Rejouer les enregistrement de la camera, à but de débogage. La liste d'objets montre un résumé avec retard des objets détectés; et l'onglet Messages montre le flux des messages internes à Frigate liés à la vidéo rejouée.",
+ "websocket_messages": "Messages",
+ "dialog": {
+ "title": "Démarrer le Rejeu-Debogage",
+ "timeRange": "Intervalle",
+ "preset": {
+ "1m": "Dernière minute",
+ "5m": "5 dernières minutes",
+ "timeline": "Depuis la chronologie",
+ "custom": "Personnalisé"
+ },
+ "startButton": "Démarrer le revisionnage",
+ "selectFromTimeline": "Sélectionner",
+ "starting": "Démarrage du revisionnage...",
+ "startLabel": "Démarrer",
+ "endLabel": "Fin",
+ "toast": {
+ "error": "Echec du démarrage du revisionnage de déboggage : {{error}}",
+ "alreadyActive": "Une session de revisionnage est déjà active",
+ "stopError": "Echec de l'arrêt du revisionnage de déboggage : {{error}}",
+ "goToReplay": "Vers le revisionnage"
+ }
+ },
+ "page": {
+ "noSession": "Aucune session de revisionnage de déboggage active",
+ "noSessionDesc": "Démarrer un revisionnage de déboggage depuis l'Historique en cliquant sur le boutons Actions dans la barre d'outils et choisir Revisionnage de déboggage.",
+ "goToRecordings": "Vers l'historique",
+ "preparingClip": "Préparation du clip…",
+ "preparingClipDesc": "Frigate est encore en train de recoller les enregistrements pour l'intervalle de temps sélectionnée. Cela peut prendre une minute pour les plus longues intervalles."
+ }
+}
diff --git a/web/public/locales/fr/views/settings.json b/web/public/locales/fr/views/settings.json
index c9b3ccb87c..d6891dc1d7 100644
--- a/web/public/locales/fr/views/settings.json
+++ b/web/public/locales/fr/views/settings.json
@@ -89,7 +89,8 @@
"cameraMqtt": "MQTT de la caméra",
"maintenance": "Maintenance",
"uiSettings": "Paramètres IU",
- "profiles": "Profils"
+ "profiles": "Profils",
+ "systemGo2rtcStreams": "Streams go2rtc"
},
"dialog": {
"unsavedChanges": {
@@ -448,6 +449,17 @@
"error": {
"mustBeGreaterOrEqualTo": "Le seuil de vitesse doit être supérieur ou égal à 0.1."
}
+ },
+ "id": {
+ "error": {
+ "mustNotBeEmpty": "L'ID ne doit pas être vide.",
+ "alreadyExists": "Un masque avec cet ID existe déjà pour cette caméra."
+ }
+ },
+ "name": {
+ "error": {
+ "mustNotBeEmpty": "Le nom ne doit pas être vide."
+ }
}
},
"zones": {
@@ -572,7 +584,11 @@
},
"restart_required": "Redémarrage requis (masques/zones changés)",
"objectMaskLabel": "Masque d'objet {{number}}",
- "motionMaskLabel": "Masque de mouvement {{number}}"
+ "motionMaskLabel": "Masque de mouvement {{number}}",
+ "disabledInConfig": "Cet objet est désactivé dans le fichier de configuration",
+ "addDisabledProfile": "Ajouter dans la configuration de base d’abord puis remplacez le dans le profil",
+ "profileBase": "(base)",
+ "profileOverride": "(remplacer)"
},
"motionDetectionTuner": {
"title": "Réglage de la détection de mouvement",
@@ -1308,7 +1324,11 @@
"backToSettings": "Retour aux paramètres de la caméra",
"streams": {
"title": "Activer / désactiver les caméras",
- "desc": "Désactive temporairement une caméra jusqu'au redémarrage de Frigate. La désactivation d'une caméra interrompt complètement le traitement des flux de la caméra par Frigate. La détection, l'enregistrement et le débogage deviennent alors indisponibles.Remarque : cela n'affecte pas les rediffusions des flux go2rtc. "
+ "desc": "Désactive temporairement une caméra jusqu'au redémarrage de Frigate. La désactivation d'une caméra interrompt complètement le traitement des flux de la caméra par Frigate. La détection, l'enregistrement et le débogage deviennent alors indisponibles.Remarque : cela n'affecte pas les rediffusions des flux go2rtc. ",
+ "enableLabel": "Caméras activées",
+ "disableLabel": "Caméra désactivées",
+ "disableDesc": "Activer une caméra qui n'est pas visible dans l'interface et désactivée dans la configuration. Un redémarrage de Frigate est nécessaire après l'activation.",
+ "enableSuccess": "Activer {{cameraName}} dans la configuration. Redémarrer Frigate pour appliquer les changements."
},
"cameraConfig": {
"add": "Ajouter une caméra",
@@ -1338,6 +1358,73 @@
"toast": {
"success": "La caméra {{cameraName}} a été enregistrée avec succès"
}
+ },
+ "deleteCamera": "Supprimer la caméra",
+ "deleteCameraDialog": {
+ "title": "Supprimer la caméra",
+ "description": "Supprimer la caméra va supprimer de façon permanente les enregistrements, les objets suivis, et la configuration de la caméra. Tous les streams go2rtc associés à la caméra devront être supprimés manuellement.",
+ "selectPlaceholder": "Choisir une caméra...",
+ "confirmTitle": "Êtes-vous sûr?",
+ "confirmWarning": "Supprimer \n{{cameraName}}\n ne peut être annulé.",
+ "deleteExports": "Supprimer aussi les exports de cette caméra",
+ "confirmButton": "Suppression permanente",
+ "success": "Caméra {{cameraName}} supprimée avec succès",
+ "error": "Impossible de supprimer la caméra {{cameraName}}"
+ },
+ "profiles": {
+ "selectLabel": "Choisir un profil",
+ "description": "Configurer quelles caméras sont activées ou désactivées quand un profil est activé. Les caméras activées avec \"Inherit\" conservent leur statut de base.",
+ "inherit": "Hériter",
+ "enabled": "Activé",
+ "disabled": "Désactivé"
+ },
+ "clone": {
+ "target": {
+ "newNameLabel": "Nom de la caméra",
+ "newNamePlaceholder": "p.ex., porte_arriere ou Porte arrière",
+ "newNameRequired": "Le nom de la caméra est requis",
+ "newNameInvalid": "Nom de caméra invalide",
+ "newNameCollision": "Une caméra avec ce nom existe déjà",
+ "newStreamsForced": "Les flux sont toujours copiés pour une nouvelle caméra.",
+ "existingCamerasRadio": "Caméras existantes",
+ "allCameras": "Toutes les caméras",
+ "existingPlaceholder": "Sélectionnez au moins une caméra",
+ "existingDisabled": "Aucune autre caméra à copier vers"
+ },
+ "categories": {
+ "legend": "Paramètres à cloner",
+ "description": "Choisissez quels paramètres copier depuis la caméra source.",
+ "selectAll": "Sélectionner tout",
+ "selectNone": "Sélectionner aucun",
+ "resetDefaults": "Rétablir à la configuration d'usine",
+ "general": "Général",
+ "spatial": "Paramètres spatiaux",
+ "streams": "Flux",
+ "spatialWarning": "La résolution détectée ({{srcWidth}}x{{srcHeight}}) de la caméra source {{srcCamera}} diffère de : {{cameras}}. Les polygones peuvent ne pas être alignés sur ces caméras. Ces paramètres sont désactivés ; activer pour copier tel quel.",
+ "restartHint": "Redémarrage requis",
+ "items": {
+ "record": "En cours d'enregistrement",
+ "objects": "Objets",
+ "audio": "Détection audio",
+ "audio_transcription": "Transcription audio",
+ "notifications": "Notifications",
+ "mqtt": "MQTT",
+ "onvif": "ONVIF",
+ "face_recognition": "Reconnaissance faciale",
+ "semantic_search": "Recherche sémantique"
+ }
+ },
+ "footer": {
+ "submit": "Cloner",
+ "submitting": "Clonage…"
+ },
+ "toast": {
+ "success": "Paramètres copiés vers {{cameraName}}",
+ "successWithRestart": "Paramètres copiés vers {{cameraName}}. Redémarrez Frigate afin d'appliquer tous les changements.",
+ "successMulti_one": "Paramètres copiés vers {{count}} caméra",
+ "successMulti_many": "Paramètres copiés vers {{count}} caméras",
+ "successMulti_other": "Paramètres copiés vers {{count}} caméras"
+ }
}
},
"cameraReview": {
@@ -1392,6 +1479,9 @@
"value": {
"label": "Nouvelle valeur",
"reset": "Réinitialiser"
+ },
+ "profile": {
+ "label": "Profil"
}
},
"button": {
@@ -1405,5 +1495,115 @@
"sync": {
"title": "Synchronisation du Média"
}
- }
+ },
+ "configMessages": {
+ "lpr": {
+ "vehicleNotTracked": "La reconnaissance de plaque d'immatriculation requiert que 'voiture' ou 'moto' soit suivi.",
+ "globalDisabled": "La reconnaissance de numéro d'immatriculation n'est pas activée au niveau global. Activez-la dans les paramètres globaux pour que la reconnaissance de plaques fonctionne au niveau caméra."
+ },
+ "review": {
+ "recordDisabled": "L'enregistrement est désactivé, aucune révision ne sera générée.",
+ "detectDisabled": "La détection d'objet est désactivée. Les révisions requièrent que les objets détectés catégorisent les alertes et les détections.",
+ "allNonAlertDetections": "Toutes les activités de non alerte seront incluses en tant que détections."
+ },
+ "audio": {
+ "noAudioRole": "Aucun flux ne possède de rôle audio défini. Vous devez activer le rôle audio afin de faire fonctionner la détection audio."
+ },
+ "audioTranscription": {
+ "audioDetectionDisabled": "La détection audio n'est pas active pour cette caméra. La transcription audio nécessite que la détection audio soit active."
+ },
+ "detect": {
+ "fpsGreaterThanFive": "Il n'est pas recommandé de régler la détection au-delà de 5 FPS."
+ },
+ "faceRecognition": {
+ "globalDisabled": "La reconnaissance faciale n'est pas activée au niveau global. Activez-la dans les paramètres globaux pour que la reconnaissance faciale fonctionne au niveau caméra.",
+ "personNotTracked": "La reconnaissance faciale requiert que l'objet 'person' soit suivie. Assurez-vous que 'person' soit dans la liste d'objets suivis."
+ }
+ },
+ "go2rtcStreams": {
+ "ffmpeg": {
+ "audioMp3": "Transcoder en PM3",
+ "audioExclude": "Exclure",
+ "hardwareNone": "Pas d'accélération matérielle",
+ "hardwareAuto": "Accélération matérielle automatique",
+ "audioCopy": "Copier",
+ "audioAac": "Transcoder en AAC",
+ "audioOpus": "Transcoder vers Opus",
+ "audioPcmu": "Transcoder vers PCM μ-law",
+ "video": "Vidéo",
+ "audio": "Audio",
+ "hardware": "Accélération matérielle",
+ "videoCopy": "Copier",
+ "videoH264": "Transcoder vers H.264",
+ "videoH265": "Transcoder vers H.265",
+ "videoExclude": "Exclure",
+ "useFfmpegModule": "Utiliser le mode de compatibilité (ffmpeg)",
+ "audioPcma": "Transcoder vers PCM A-law",
+ "audioPcm": "Transcoder vers PCM"
+ },
+ "renameStream": "Renommer le flux",
+ "renameStreamDesc": "Saisir un nouveau nom pour ce flux. Le renommage d'un flux peut induire un problème avec les caméras ou les autres flux qui le référence par nom.",
+ "addStream": "Ajouter un flux",
+ "title": "Flux go2rtc",
+ "description": "Gérer les paramètres de flux go2rtc pour la rediffusion de caméra. Chaque flux possède un nom et une ou plusieurs URLs source.",
+ "deleteStream": "Supprimer flux",
+ "deleteStreamConfirm": "Êtes-vous sûr de vouloir supprimer le flux \"{{streamName}}\" ? Les caméras qui référencent ce flux pourraient ne plus fonctionner.",
+ "noStreams": "Aucun flux go2rtc configuré. Ajoutez un flux pour commencer.",
+ "validation": {
+ "nameRequired": "Le nom de flux est obligatoire",
+ "nameDuplicate": "Un flux avec ce nom existe déjà",
+ "nameInvalid": "Le nom de flux ne peut contenir que des lettres, nombres, underscores et tirets",
+ "urlRequired": "Au moins une URL est requise"
+ },
+ "newStreamName": "Nouveau nom de flux",
+ "addUrl": "Ajouter URL",
+ "streamName": "Nom de flux",
+ "streamNamePlaceholder": "p. ex., porte_entree",
+ "streamUrlPlaceholder": "p. ex., rtsp://utilisateur:motpasse@192.168.1.100/flux",
+ "addStreamDesc": "Saisir un nom pour ce nouveau flux. Ce nom sera utilisé pour référencer le flux dans les paramètres de votre caméra."
+ },
+ "onvif": {
+ "profileAuto": "Automatique",
+ "profileLoading": "Chargement des profils..."
+ },
+ "profiles": {
+ "enableSwitch": "Activer les profils",
+ "enabledDescription": "Les profils sont actifs. Créer un nouveau profil ci-dessous, naviguer vers la section de configuration de la caméra afin de faire vos changements, et les sauvegarder afin de les prendre en compte.",
+ "error": {
+ "mustBeAtLeastTwoCharacters": "Doit comporter au moins 2 caractères",
+ "mustNotContainPeriod": "Ne doit pas contenir de points",
+ "alreadyExists": "Un profil avec cet identifiant existe déjà"
+ },
+ "deactivated": "Profil désactivé",
+ "noProfiles": "Aucun profil défini.",
+ "noOverrides": "Aucune surcharge",
+ "cameraCount_one": "{{count}} caméra",
+ "cameraCount_many": "{{count}} caméras",
+ "cameraCount_other": "{{count}} caméras",
+ "columnCamera": "Caméra",
+ "columnOverrides": "Surcharges de profil",
+ "baseConfig": "Configuration de base",
+ "addProfile": "Ajouter un profil",
+ "newProfile": "Nouveau profil",
+ "friendlyNameLabel": "Nom profil",
+ "profileIdLabel": "ID profil",
+ "profileIdDescription": "Identifiant interne utilisé dans la configuration et automatisations",
+ "nameInvalid": "Ne sont autorisés que les lettres minuscules, nombres et underscores",
+ "nameDuplicate": "Un profil avec ce nom existe déjà",
+ "renameProfile": "Renommer profil",
+ "renameSuccess": "Profil renommé en '{{profile}}'",
+ "deleteProfile": "Supprimer Profil",
+ "deleteProfileConfirm": "Supprimer profil \"{{profile}}\" de toutes les caméras ? Ceci ne peut être annulé.",
+ "deleteSuccess": "Le profil '{{profile}}' a été supprimé",
+ "createSuccess": "Le profil '{{profile}}' a été créé",
+ "removeOverride": "Supprimer le profil surchargé",
+ "deleteSection": "Supprimer la section de surcharges",
+ "deleteSectionConfirm": "Supprimer les surcharges de {{section}} pour le profil {{profile}} sur {{camera}} ?",
+ "deleteSectionSuccess": "Surcharges de {{section}} supprimées pour {{profile}}",
+ "disabledDescription": "Les profils vous permettent de définir des ensembles nommés de surcharges de configuration de caméra (p. ex. armé, absent, nuit) qui peuvent être activés à la demande."
+ },
+ "unsavedChanges": "Vous avez des changements non sauvegardés",
+ "confirmReset": "Confirmer réinitialisation",
+ "resetToDefaultDescription": "Cela va réinitialiser les paramètres dans cette section avec les valeurs d'usine. Cette action ne peut être annulée.",
+ "resetToGlobalDescription": "Ceci va réinitialiser les paramètres de cette section vers les paramètres globaux. Cette action ne peut être annulée."
}
diff --git a/web/public/locales/fr/views/system.json b/web/public/locales/fr/views/system.json
index f29b871707..3c4577c212 100644
--- a/web/public/locales/fr/views/system.json
+++ b/web/public/locales/fr/views/system.json
@@ -111,7 +111,8 @@
"description": "Il s'agit d'un bug connu de l'outil de statistiques GPU d'Intel (intel_gpu_top) : il peut afficher à tort une utilisation de 0 %, même lorsque l'accélération matérielle et la détection d'objets fonctionnent correctement sur l'iGPU. Ce problème ne vient pas de Frigate. Vous pouvez redémarrer l'hôte pour rétablir temporairement l'affichage et confirmer le fonctionnement du GPU. Les performances ne sont pas affectées."
},
"gpuTemperature": "Température du GPU",
- "npuTemperature": "Température du NPU"
+ "npuTemperature": "Température du NPU",
+ "gpuCompute": "Calcul GPU / Encodage"
},
"otherProcesses": {
"title": "Autres processus",
@@ -121,7 +122,7 @@
"go2rtc": "go2rtc",
"recording": "enregistrement",
"review_segment": "Segment d'activité",
- "embeddings": "embeddings",
+ "embeddings": "intégrations",
"audio_detector": "détecteur audio"
}
}
@@ -148,7 +149,11 @@
"overview": "Vue d'ensemble",
"shm": {
"title": "Allocation de mémoire partagée SHM",
- "warning": "La taille actuelle de la SHM de {{total}} Mo est trop petite. Augmentez-la au moins à {{min_shm}} Mo."
+ "warning": "La taille actuelle de la SHM de {{total}} Mo est trop petite. Augmentez-la au moins à {{min_shm}} Mo.",
+ "frameLifetime": {
+ "title": "Durée de vie de la trame",
+ "description": "Chaque caméra dispose de {{frames}} emplacements de trames en mémoire partagée. À la fréquence d'images de la caméra la plus rapide, chaque trame est disponible pendant environ {{lifetime}}s avant d'être écrasée."
+ }
}
},
"cameras": {
@@ -168,7 +173,22 @@
"error": "Erreur : {{error}}",
"codec": "Codec :",
"video": "Vidéo :",
- "aspectRatio": "rapport d'aspect"
+ "aspectRatio": "rapport d'aspect",
+ "keyframes": {
+ "title": "Analyse par image clef",
+ "analyzing": "Analyse des images clefs... {{seconds}} secondes restantes",
+ "stillAnalyzing": "Analyse des images clefs toujours en cours ...",
+ "recordStream": "Flux d'enregistrement :",
+ "keyframeCount": "Images clefs observées :",
+ "observedDuration": "Durée observée :",
+ "gap": "Écart en images clefs (min / moy / max) :",
+ "segmentLength": "Durée du segment d'enregistrement :",
+ "ok": "Images clefs chaque ~{{seconds}}s, convient à l'enregistrement et au visionnement.",
+ "warning": "Images clefs fragmentées ou variables (plus grand écart d'environ {{seconds}}s), vraisemblablement un codec intelligent (H.264+/H.265+), non-recommendé.",
+ "error": "L'écart en images clefs (~{{seconds}}) dépasse la longueur du segment d'enregistrement ({{segmentTime}}). Certains segments n'ont peut-être pas de clef, ce qui corrompt la lecture. Désactiver le codec intelligent/+ sur la caméra ou raccourcir son intervalle en images clefs.",
+ "unknown": "Impossible de déterminer l'espacement en images clefs.",
+ "recordDisabled": "L'enregistrement est désactivé pour cette caméra."
+ }
},
"framesAndDetections": "Images / Détections",
"label": {
@@ -185,12 +205,13 @@
"cameraCapture": "{{camName}} capture",
"cameraDetect": "{{camName}} détection",
"cameraFramesPerSecond": "{{camName}} images par seconde",
- "cameraDetectionsPerSecond": "{{camName}} détections par seconde"
+ "cameraDetectionsPerSecond": "{{camName}} détections par seconde",
+ "cameraGpu": "GPU {{camName}}"
},
"overview": "Vue d'ensemble",
"toast": {
"success": {
- "copyToClipboard": "Données de la sonde copiées dans le presse-papiers"
+ "copyToClipboard": "Données de la sonde copiées dans le presse-papiers."
},
"error": {
"unableToProbeCamera": "Impossible d'interroger la caméra : {{errorMessage}}"
@@ -206,6 +227,9 @@
"expectedFps": "IPS attendues",
"reconnectsLastHour": "Reconnexions (dernière heure)",
"stallsLastHour": "Baisses de qualité (dernière heure)"
+ },
+ "noCameras": {
+ "title": "Aucune caméra trouvée"
}
},
"lastRefreshed": "Dernier rafraichissement : ",
@@ -217,7 +241,8 @@
"cameraIsOffline": "{{camera}} est hors ligne",
"detectIsSlow": "{{detect}} est lent ({{speed}} ms)",
"detectIsVerySlow": "{{detect}} est très lent ({{speed}} ms)",
- "shmTooLow": "L'allocation /dev/shm ({{total}} Mo) devrait être augmentée à au moins {{min}} Mo."
+ "shmTooLow": "L'allocation /dev/shm ({{total}} Mo) devrait être augmentée à au moins {{min}} Mo.",
+ "debugReplayActive": "Session de relecture de débogage active"
},
"enrichments": {
"title": "Enrichissements",
diff --git a/web/public/locales/gl/views/chat.json b/web/public/locales/gl/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/gl/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/gl/views/motionSearch.json b/web/public/locales/gl/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/gl/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/gl/views/replay.json b/web/public/locales/gl/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/gl/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/he/common.json b/web/public/locales/he/common.json
index 1059ae300b..d20ee8b530 100644
--- a/web/public/locales/he/common.json
+++ b/web/public/locales/he/common.json
@@ -81,7 +81,8 @@
"thisMonth": "החודש",
"inProgress": "בתהליך",
"invalidStartTime": "זמן התחלה לא תקין",
- "invalidEndTime": "זמן סיום לא תקין"
+ "invalidEndTime": "זמן סיום לא תקין",
+ "never": "אף פעם"
},
"unit": {
"speed": {
@@ -146,7 +147,8 @@
"download": "הורדה",
"info": "מידע",
"next": "הבא",
- "continue": "המשך"
+ "continue": "המשך",
+ "add": "להוסיף"
},
"menu": {
"system": "מערכת",
diff --git a/web/public/locales/he/views/chat.json b/web/public/locales/he/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/he/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/he/views/motionSearch.json b/web/public/locales/he/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/he/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/he/views/replay.json b/web/public/locales/he/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/he/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/hi/views/chat.json b/web/public/locales/hi/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/hi/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/hi/views/motionSearch.json b/web/public/locales/hi/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/hi/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/hi/views/replay.json b/web/public/locales/hi/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/hi/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/hr/audio.json b/web/public/locales/hr/audio.json
index 55d1e5fce5..8531e48d8d 100644
--- a/web/public/locales/hr/audio.json
+++ b/web/public/locales/hr/audio.json
@@ -11,7 +11,7 @@
"laughter": "Smijeh",
"train": "Vlak",
"snicker": "Smješkanje",
- "boat": "ÄŒamac",
+ "boat": "Brod",
"crying": "Plakanje",
"singing": "Pjevanje",
"choir": "Zbor",
@@ -350,7 +350,7 @@
"microwave_oven": "Mikrovalna pećnica",
"water_tap": "Vodovodna slavina",
"bathtub": "Kada",
- "toilet_flush": "Ispiranje WC-a",
+ "toilet_flush": "Ispiranje toaleta",
"electric_toothbrush": "Električna četkica za zube",
"vacuum_cleaner": "Usisavač",
"zipper": "Patentni zatvarač",
diff --git a/web/public/locales/hr/common.json b/web/public/locales/hr/common.json
index 68b1cb41f0..8206a7d3ba 100644
--- a/web/public/locales/hr/common.json
+++ b/web/public/locales/hr/common.json
@@ -22,11 +22,11 @@
"pm": "pm",
"am": "am",
"ago": "prije {{timeAgo}}",
- "yr": "{{time}}g.",
+ "yr": "{{time}}g",
"year_one": "{{time}} godina",
"year_few": "{{time}} godine",
"year_other": "{{time}} godina",
- "mo": "{{time}}mj.",
+ "mo": "{{time}}mj",
"month_one": "{{time}} mjesec",
"month_few": "{{time}} mjeseca",
"month_other": "{{time}} mjeseci",
diff --git a/web/public/locales/hr/config/cameras.json b/web/public/locales/hr/config/cameras.json
index 0967ef424b..1ffab63889 100644
--- a/web/public/locales/hr/config/cameras.json
+++ b/web/public/locales/hr/config/cameras.json
@@ -1 +1,3 @@
-{}
+{
+ "label": "Konfiguracijakamere"
+}
diff --git a/web/public/locales/hr/config/global.json b/web/public/locales/hr/config/global.json
index 0967ef424b..9a3929845d 100644
--- a/web/public/locales/hr/config/global.json
+++ b/web/public/locales/hr/config/global.json
@@ -1 +1,5 @@
-{}
+{
+ "version": {
+ "label": "Trenutna verzija konfiguracije"
+ }
+}
diff --git a/web/public/locales/hr/config/groups.json b/web/public/locales/hr/config/groups.json
index 0967ef424b..fd99a882c1 100644
--- a/web/public/locales/hr/config/groups.json
+++ b/web/public/locales/hr/config/groups.json
@@ -1 +1,7 @@
-{}
+{
+ "audio": {
+ "global": {
+ "detection": "Globalna detekcija"
+ }
+ }
+}
diff --git a/web/public/locales/hr/objects.json b/web/public/locales/hr/objects.json
index 955ebe0cd7..b27b693e1f 100644
--- a/web/public/locales/hr/objects.json
+++ b/web/public/locales/hr/objects.json
@@ -6,7 +6,7 @@
"airplane": "Zrakoplov",
"bus": "Autobus",
"train": "Vlak",
- "boat": "ÄŒamac",
+ "boat": "Brod",
"traffic_light": "Semafor",
"fire_hydrant": "Hidrant",
"street_sign": "Prometni znak",
diff --git a/web/public/locales/hr/views/chat.json b/web/public/locales/hr/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/hr/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/hr/views/live.json b/web/public/locales/hr/views/live.json
index 9fce430f57..a60f87cd51 100644
--- a/web/public/locales/hr/views/live.json
+++ b/web/public/locales/hr/views/live.json
@@ -1,5 +1,7 @@
{
- "documentTitle": "Uživo - Frigate",
+ "documentTitle": {
+ "default": "Uživo - Frigate"
+ },
"documentTitle.withCamera": "{{camera}} - Uživo - Frigate",
"twoWayTalk": {
"enable": "Omogući dvosmjerni razgovor",
diff --git a/web/public/locales/hr/views/motionSearch.json b/web/public/locales/hr/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/hr/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/hr/views/replay.json b/web/public/locales/hr/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/hr/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/hu/audio.json b/web/public/locales/hu/audio.json
index 7f31e04592..c16ebfdfc9 100644
--- a/web/public/locales/hu/audio.json
+++ b/web/public/locales/hu/audio.json
@@ -426,5 +426,12 @@
"chink": "Csörömpölés",
"shatter": "Összetörés",
"field_recording": "Helyszíni felvétel",
- "noise": "Zaj"
+ "noise": "Zaj",
+ "liquid": "Folyadék",
+ "splash": "Csobbanás",
+ "slosh": "Löttyenés",
+ "trickle": "Csörgedezés",
+ "gush": "Folyás",
+ "stir": "Kavarás",
+ "thump": "Puffanás"
}
diff --git a/web/public/locales/hu/common.json b/web/public/locales/hu/common.json
index 6e5df9f1de..fdebbf0e9a 100644
--- a/web/public/locales/hu/common.json
+++ b/web/public/locales/hu/common.json
@@ -178,7 +178,8 @@
"configuration": "Konfiguráció",
"systemLogs": "Rendszer naplók",
"settings": "Beállítások",
- "classification": "Osztályozás"
+ "classification": "Osztályozás",
+ "profiles": "Profilok"
},
"role": {
"viewer": "Néző",
@@ -273,7 +274,8 @@
"export": "Exportálás",
"deleteNow": "Törlés Most",
"next": "Következő",
- "continue": "Tovább"
+ "continue": "Tovább",
+ "add": "Hozzáad"
},
"label": {
"back": "Vissza",
diff --git a/web/public/locales/hu/components/player.json b/web/public/locales/hu/components/player.json
index 31ee991378..1e420ed309 100644
--- a/web/public/locales/hu/components/player.json
+++ b/web/public/locales/hu/components/player.json
@@ -3,7 +3,8 @@
"noPreviewFound": "Nincs elérhető előkép",
"submitFrigatePlus": {
"title": "Elküldi ezt a képet a Frigate+-nak?",
- "submit": "Küldés"
+ "submit": "Küldés",
+ "previewError": "Nem sikerült betölteni a pillanatkép előnézetét. Előfordulhat, hogy a felvétel jelenleg nem elérhető."
},
"noPreviewFoundFor": "Nem található előnézet {{cameraName}}-hoz/-hez/-höz",
"livePlayerRequiredIOSVersion": "iOS 17.1 vagy újabb szükséges ehhez az élő adás típushoz.",
diff --git a/web/public/locales/hu/config/cameras.json b/web/public/locales/hu/config/cameras.json
index e228cd9786..5bc97239a6 100644
--- a/web/public/locales/hu/config/cameras.json
+++ b/web/public/locales/hu/config/cameras.json
@@ -39,6 +39,41 @@
"description": "A Frigate felhasználói felületén használt, könnyen megjegyezhető kamera név"
},
"enabled": {
- "label": "Engedélyezve"
+ "label": "Engedélyezve",
+ "description": "Engedélyezve"
+ },
+ "audio": {
+ "label": "Hangesemények",
+ "description": "Hangalapú eseményérzékelés beállításai ennél a kameránál.",
+ "enabled": {
+ "label": "Hangalapú eseményérzékelés engedélyezése",
+ "description": "A hangalapú eseményérzékelés engedélyezése vagy letiltása ennél a kameránál."
+ },
+ "max_not_heard": {
+ "description": "Ennyi másodperc után fejeződik be a hangesemény, ha a beállított hangtípus nem észlelhető.",
+ "label": "Időtúllépés befejezése"
+ },
+ "min_volume": {
+ "label": "Minimális hangerő",
+ "description": "Minimum RMS hangerő a hangérzékelés futtatásához; az alacsonyabb értékek növelik az érzékenységet (pl: 200 magas, 500 közepes, 1000 alacsony érzékenységet jelent)."
+ },
+ "listen": {
+ "label": "Hallgatási típúsok",
+ "description": "Lista a hangalapú eseményekről amit érzékelni szeretnél (angolul) (például: bark, fire_alarm, scream, speech, yell)."
+ },
+ "filters": {
+ "label": "Audio szűrők (filters)",
+ "description": "Hangtípusonkénti szűrőbeállítások (filter), mint például a téves találatok számát mérsékelő konfidencia-küszöbök."
+ },
+ "enabled_in_config": {
+ "label": "Eredeti audio állapot"
+ }
+ },
+ "audio_transcription": {
+ "label": "Hang Feliratozás",
+ "description": "„Beállítások élő hang és beszéd automatikus szöveggé alakításához, eseményekhez és élő feliratozáshoz.",
+ "enabled": {
+ "label": "Hangról szövegre alakítás engedélyezése"
+ }
}
}
diff --git a/web/public/locales/hu/config/global.json b/web/public/locales/hu/config/global.json
index 8a43985e3f..1389d61a53 100644
--- a/web/public/locales/hu/config/global.json
+++ b/web/public/locales/hu/config/global.json
@@ -40,5 +40,65 @@
"environment_vars": {
"label": "Környezeti változók",
"description": "A Home Assistant OS rendszerben a Frigate folyamat számára beállítandó környezeti változói. A nem HAOS-felhasználóknak helyette a Docker konfigurációját kell használniuk."
+ },
+ "logger": {
+ "label": "Naplózás",
+ "description": "Az alapértelmezett naplózási részletességet és a komponensenkénti naplózási szintek felülírását vezérli.",
+ "default": {
+ "label": "Naplózási részletesség",
+ "description": "Alapértelmezett globális naplórészletesség (debug, info, warning, error)."
+ },
+ "logs": {
+ "label": "Folyamatonkénti naplózási szint",
+ "description": "Összetevőnkénti naplózási szint felülbírálások az egyes modulok részletességének növeléséhez vagy csökkentéséhez."
+ }
+ },
+ "audio": {
+ "label": "Hangesemények",
+ "enabled": {
+ "label": "Hangalapú eseményérzékelés engedélyezése"
+ },
+ "max_not_heard": {
+ "description": "Ennyi másodperc után fejeződik be a hangesemény, ha a beállított hangtípus nem észlelhető.",
+ "label": "Időtúllépés befejezése"
+ },
+ "min_volume": {
+ "label": "Minimális hangerő",
+ "description": "Minimum RMS hangerő a hangérzékelés futtatásához; az alacsonyabb értékek növelik az érzékenységet (pl: 200 magas, 500 közepes, 1000 alacsony érzékenységet jelent)."
+ },
+ "listen": {
+ "label": "Hallgatási típúsok",
+ "description": "Lista a hangalapú eseményekről amit érzékelni szeretnél (angolul) (például: bark, fire_alarm, scream, speech, yell)."
+ },
+ "filters": {
+ "label": "Audio szűrők (filters)",
+ "description": "Hangtípusonkénti szűrőbeállítások (filter), mint például a téves találatok számát mérsékelő konfidencia-küszöbök."
+ },
+ "enabled_in_config": {
+ "label": "Eredeti audio állapot"
+ }
+ },
+ "auth": {
+ "label": "Azonosítás",
+ "description": "Bejelentkezési és munkamenet-beállítások, többek között süti- és lekérdezési korlátok (rate limit) megadásához.",
+ "enabled": {
+ "label": "Bejelentkezés engedélyezése",
+ "description": "Natív bejelentkezés (azonosítás) engedélyezése a Frigate felületén."
+ },
+ "reset_admin_password": {
+ "label": "Admin jelszó visszaállítása",
+ "description": "Ha igaz, akkor visszaállítja az admin felhasználó jelszavát, és induláskor a naplóba írja ki az új jelszót."
+ },
+ "cookie_name": {
+ "label": "JWT süti neve",
+ "description": "A süti neve ami a JWT tokent tárolja a natív bejelentkezéshez."
+ }
+ },
+ "audio_transcription": {
+ "label": "Hang Feliratozás",
+ "description": "„Beállítások élő hang és beszéd automatikus szöveggé alakításához, eseményekhez és élő feliratozáshoz.",
+ "enabled": {
+ "label": "Hangról szövegre alakítás engedélyezése"
+ }
}
}
diff --git a/web/public/locales/hu/config/groups.json b/web/public/locales/hu/config/groups.json
index a50d820663..6d334c5a75 100644
--- a/web/public/locales/hu/config/groups.json
+++ b/web/public/locales/hu/config/groups.json
@@ -16,5 +16,43 @@
"cameras": {
"appearance": "Kinézet"
}
+ },
+ "motion": {
+ "global": {
+ "sensitivity": "Globális érzékenység",
+ "algorithm": "Globális Algoritmus"
+ },
+ "cameras": {
+ "sensitivity": "Érzékenység",
+ "algorithm": "Algoritmus"
+ }
+ },
+ "detect": {
+ "global": {
+ "resolution": "Globális Felbontás",
+ "tracking": "Globális követés"
+ },
+ "cameras": {
+ "resolution": "Felbontás",
+ "tracking": "Követés (tracking)"
+ }
+ },
+ "snapshots": {
+ "global": {
+ "display": "Globális kijelző"
+ },
+ "cameras": {
+ "display": "Kijelző"
+ }
+ },
+ "objects": {
+ "global": {
+ "tracking": "Globális objektumkövetés",
+ "filtering": "Globális szűrés (filtering)"
+ },
+ "cameras": {
+ "tracking": "Követés",
+ "filtering": "Szűrés (filtering)"
+ }
}
}
diff --git a/web/public/locales/hu/config/validation.json b/web/public/locales/hu/config/validation.json
index 7b3ab646b6..2e6fafa925 100644
--- a/web/public/locales/hu/config/validation.json
+++ b/web/public/locales/hu/config/validation.json
@@ -4,5 +4,22 @@
"exclusiveMinimum": "Nagyobbnak kell lennie, mint {{limit}}",
"exclusiveMaximum": "Kevesebbnek kell lennie, mint {{limit}}",
"minLength": "Legalább {{limit}} karaktert kell megadni",
- "maxLength": "Legfeljebb {{limit}} karakter lehet"
+ "maxLength": "Legfeljebb {{limit}} karakter lehet",
+ "minItems": "Legalább {{limit}} elemnek kell lennie",
+ "maxItems": "Legfeljebb {{limit}} elem lehet",
+ "pattern": "Érvénytelen formátum",
+ "required": "Ezt a mezőt kötelező kitölteni",
+ "type": "Érvénytelen értéktípus",
+ "enum": "Az engedélyezett értékek közül legalább egy kell legyen",
+ "const": "Az érték nem egyezik a várt állandóval",
+ "uniqueItems": "Minden elemnek egyedinek kell lennie",
+ "format": "Érvénytelen formátum",
+ "additionalProperties": "Ismeretlen tulajdonság nem engedélyezett",
+ "oneOf": "Pontosan az egyik engedélyezett sémának kell megfelelnie",
+ "anyOf": "Legalább az egyik engedélyezett sémának kell megfelelnie",
+ "ffmpeg": {
+ "inputs": {
+ "rolesUnique": "Mindegyik szerepkör (role) csak egy bemeneti (input) streamhez rendelhető hozzá."
+ }
+ }
}
diff --git a/web/public/locales/hu/views/chat.json b/web/public/locales/hu/views/chat.json
new file mode 100644
index 0000000000..0130564979
--- /dev/null
+++ b/web/public/locales/hu/views/chat.json
@@ -0,0 +1,19 @@
+{
+ "documentTitle": "Chat - Frigate",
+ "title": "Frigate Chat",
+ "subtitle": "A kamerakezeléshez és elemzésekhez szükséges MI asszisztensed",
+ "placeholder": "Kérdezz bármit...",
+ "error": "Hiba történt. Kérjük, próbáld meg újra.",
+ "processing": "Feldolgozás...",
+ "toolsUsed": "Használatban: {{tools}}",
+ "showTools": "Eszközök megjelenítése ({{count}})",
+ "hideTools": "Eszközök elrejtése",
+ "call": "Hívás",
+ "result": "Eredmény",
+ "response": "Válasz:",
+ "attachment_chip_remove": "Melléklet eltávolítása",
+ "open_in_explore": "Megnyitás Böngészőben",
+ "attachment_picker_paste_label": "Vagy illeszd be az esemény ID-t",
+ "attachment_picker_attach": "Melléklet",
+ "arguments": "Argumentumok:"
+}
diff --git a/web/public/locales/hu/views/events.json b/web/public/locales/hu/views/events.json
index 904a013368..82e7b72025 100644
--- a/web/public/locales/hu/views/events.json
+++ b/web/public/locales/hu/views/events.json
@@ -15,7 +15,9 @@
"only": "Csak mozgások"
},
"allCameras": "Összes kamera",
- "timeline": "Idővonal",
+ "timeline": {
+ "label": "Idővonal"
+ },
"detected": "észlelve",
"events": {
"label": "Események",
diff --git a/web/public/locales/hu/views/exports.json b/web/public/locales/hu/views/exports.json
index f1880b1252..ebf428f178 100644
--- a/web/public/locales/hu/views/exports.json
+++ b/web/public/locales/hu/views/exports.json
@@ -3,7 +3,9 @@
"search": "Keresés",
"noExports": "Export nem található",
"deleteExport.desc": "Biztos, hogy törölni akarja {{exportName}}-t?",
- "deleteExport": "Export törlése",
+ "deleteExport": {
+ "label": "Export törlése"
+ },
"editExport": {
"title": "Exportálás átnevezése",
"desc": "Adjon meg egy új nevet ennek az exportnak.",
@@ -23,5 +25,8 @@
"headings": {
"cases": "Esetek",
"uncategorizedExports": "Kategória nélküli exportok"
+ },
+ "toolbar": {
+ "addExport": "Export hozzáadása"
}
}
diff --git a/web/public/locales/hu/views/live.json b/web/public/locales/hu/views/live.json
index b7a5ff9672..a24a0e6bb5 100644
--- a/web/public/locales/hu/views/live.json
+++ b/web/public/locales/hu/views/live.json
@@ -3,7 +3,9 @@
"enable": "Kétirányú kommunikáció engedélyezése",
"disable": "Kétirányú kommunikáció tiltása"
},
- "documentTitle": "Élő - Frigate",
+ "documentTitle": {
+ "default": "Élő - Frigate"
+ },
"lowBandwidthMode": "Alacsony felbontású mód",
"documentTitle.withCamera": "{{camera}} - Élő - Frigate",
"cameraAudio": {
@@ -15,7 +17,8 @@
"clickMove": {
"label": "Kattintson a képre a kamera középre igazításához",
"enable": "Engedélyezze a kattintást a mozgatáshoz",
- "disable": "Kattintással húzás kikapcsolása"
+ "disable": "Kattintással húzás kikapcsolása",
+ "enableWithZoom": "Kattintással történő mozgatás és húzással való nagyítás engedélyezése"
},
"left": {
"label": "PTZ kamera balra mozgatása"
diff --git a/web/public/locales/hu/views/motionSearch.json b/web/public/locales/hu/views/motionSearch.json
new file mode 100644
index 0000000000..e84683d3ba
--- /dev/null
+++ b/web/public/locales/hu/views/motionSearch.json
@@ -0,0 +1,22 @@
+{
+ "documentTitle": "Mozgás Keresés - Frigate",
+ "title": "Mozgás Keresés",
+ "description": "Rajzoljon be egy sokszöget a vizsgálandó terület kijelöléséhez, majd adja meg az időtartományt, amelyen belül a mozgásváltozásokat szeretné keresni ezen a területen.",
+ "selectCamera": "Mozgás Keresés betöltése folyamatban",
+ "startSearch": "Keresés Indítása",
+ "searchStarted": "Megkezdődött a keresés",
+ "searchCancelled": "Keresés megszakítva",
+ "cancelSearch": "Mégse",
+ "searching": "Keresés folyamatban.",
+ "searchComplete": "A keresés befejeződött",
+ "noResultsYet": "Futtass keresést a kijelölt területen a mozgásváltozások felkutatásához",
+ "noChangesFound": "A kijelölt területen nem történt pixelváltozás",
+ "changesFound_one": "Észlelhető {{count}} mozgásváltozás",
+ "changesFound_other": "Észlelhető {{count}} mozgásváltozás",
+ "framesProcessed": "{{count}} képkocka feldolgozva",
+ "jumpToTime": "Ugrás erre az időpontra",
+ "results": "Eredmények",
+ "showSegmentHeatmap": "Hőtérkép",
+ "newSearch": "Új Keresés",
+ "clearResults": "Eredmények törlése"
+}
diff --git a/web/public/locales/hu/views/replay.json b/web/public/locales/hu/views/replay.json
new file mode 100644
index 0000000000..7df3327315
--- /dev/null
+++ b/web/public/locales/hu/views/replay.json
@@ -0,0 +1,25 @@
+{
+ "title": "Hibakeresés visszajátszás",
+ "websocket_messages": "Üzenetek",
+ "dialog": {
+ "title": "Hibakeresés visszajátszásának indítása",
+ "camera": "Forráskamera",
+ "timeRange": "Időtartomány",
+ "preset": {
+ "1m": "Legutóbbi 1 perc",
+ "5m": "Legutóbbi 5 perc",
+ "timeline": "Az Idővonalból",
+ "custom": "Egyedi"
+ },
+ "startButton": "Visszajátszás indítása",
+ "selectFromTimeline": "Válassz",
+ "starting": "Visszajátszás indítása...",
+ "startLabel": "Indítás",
+ "endLabel": "Vége",
+ "toast": {
+ "error": "A hibakeresési visszajátszás elindítása sikertelen: {{error}}",
+ "alreadyActive": "A visszajátszási munkamenet már aktív",
+ "goToReplay": "Ugrás a visszajátszásra"
+ }
+ }
+}
diff --git a/web/public/locales/hu/views/settings.json b/web/public/locales/hu/views/settings.json
index c8bd38614d..3cf386f72b 100644
--- a/web/public/locales/hu/views/settings.json
+++ b/web/public/locales/hu/views/settings.json
@@ -12,7 +12,11 @@
"motionTuner": "Mozgás Hangoló - Frigate",
"enrichments": "Kiegészítés Beállítások - Frigate",
"cameraManagement": "Kamerák kezelése - Frigate",
- "cameraReview": "Kamera beállítások áttekintése – Frigate"
+ "cameraReview": "Kamera beállítások áttekintése – Frigate",
+ "globalConfig": "Globális Konfiguráció - Frigate",
+ "cameraConfig": "Kamera Konfiguráció - Frigate",
+ "maintenance": "Karbantartás - Frigate",
+ "profiles": "Profilok - Frigate"
},
"menu": {
"ui": "UI",
@@ -28,7 +32,24 @@
"triggers": "Triggerek",
"roles": "Szerepkörök",
"cameraManagement": "Menedzsment",
- "cameraReview": "Vizsgálat"
+ "cameraReview": "Vizsgálat",
+ "general": "Általános",
+ "globalConfig": "Globális konfiguráció",
+ "system": "Rendszer",
+ "integrations": "Integrációk",
+ "uiSettings": "UI beállítások",
+ "profiles": "Profilok",
+ "globalDetect": "Tárgy felismerés",
+ "globalRecording": "Felvétel",
+ "globalSnapshots": "Pillanatképek",
+ "globalFfmpeg": "FFmpeg",
+ "globalMotion": "Mozgásérzékelés",
+ "globalObjects": "Tárgyak",
+ "globalReview": "Áttekintés",
+ "globalAudioEvents": "Hangesemények",
+ "cameraAudioEvents": "Hangesemények",
+ "cameraAudioTranscription": "Hang Feliratozás",
+ "integrationAudioTranscription": "Hang Feliratozás"
},
"dialog": {
"unsavedChanges": {
@@ -517,7 +538,7 @@
"all": "Mindem Maszk és Zóna"
},
"motionMaskLabel": "Mozgási Maszk {{number}}",
- "objectMaskLabel": "Tárgy Maszk {{number}} {{label}}",
+ "objectMaskLabel": "Tárgy Maszk {{number}}",
"toast": {
"success": {
"copyCoordinates": "A {{polyName}} koordinátái vágólapra másolva."
@@ -861,5 +882,16 @@
"streamConfiguration": "Stream beállítások",
"validationAndTesting": "Validálás és tesztelés"
}
+ },
+ "button": {
+ "overriddenGlobal": "Felülírt (Globális)",
+ "overriddenGlobalTooltip": "Ez a kamera felülírja a globális konfigurációs beállításokat ebben a részben",
+ "overriddenBaseConfig": "Felülírt (Alapbeállítás)",
+ "overriddenBaseConfigTooltip": "A {{profile}} profil felülírja a konfigurációs beállításokat ebben a részben"
+ },
+ "detectionModel": {
+ "plusActive": {
+ "label": "A jelenlegi modell forrása"
+ }
}
}
diff --git a/web/public/locales/hu/views/system.json b/web/public/locales/hu/views/system.json
index d99cfbcb32..f922f61b99 100644
--- a/web/public/locales/hu/views/system.json
+++ b/web/public/locales/hu/views/system.json
@@ -6,7 +6,8 @@
"logs": {
"frigate": "Frigate naplók - Frigate",
"go2rtc": "Go2RTC naplók - Frigate",
- "nginx": "Nginx naplók - Frigate"
+ "nginx": "Nginx naplók - Frigate",
+ "websocket": "Üzenet naplók - Frigate"
},
"enrichments": "Kiegészítés statisztikák - Frigate"
},
@@ -78,7 +79,22 @@
"download": {
"label": "Naplók letöltése"
},
- "tips": "A naplók a szerverről érkeznek"
+ "tips": "A naplók a szerverről érkeznek",
+ "websocket": {
+ "label": "Üzenetek",
+ "pause": "Szüneteltetés",
+ "resume": "Folytatás",
+ "filter": {
+ "all": "Összes téma (topic)",
+ "topics": "Témák (topics)",
+ "events": "Események",
+ "reviews": "Értékelések (reviews)",
+ "face_recognition": "Arcfelismerés",
+ "classification": "Osztályozás",
+ "lpr": "LPR"
+ },
+ "clear": "Töröl"
+ }
},
"general": {
"title": "Általános",
@@ -173,7 +189,11 @@
"face_recognition": "Arcfelismerés",
"plate_recognition": "Rendszám Felismerés",
"yolov9_plate_detection_speed": "YOLOv9 Rendszám Felismerés Sebesség",
- "yolov9_plate_detection": "YOLOv9 Rendszám Észlelés"
+ "yolov9_plate_detection": "YOLOv9 Rendszám Észlelés",
+ "classification_events_per_second": "{{name}} Osztályozási események másodpercenként",
+ "classification_speed": "{{name}} Osztályozási sebesség",
+ "classification": "{{name}} Osztályozás",
+ "object_description_events_per_second": "Objektum leírás"
},
"infPerSecond": "Inferencia Per Másodperc",
"title": "Kiegészítések"
diff --git a/web/public/locales/hy/views/chat.json b/web/public/locales/hy/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/hy/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/hy/views/motionSearch.json b/web/public/locales/hy/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/hy/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/hy/views/replay.json b/web/public/locales/hy/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/hy/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/id/audio.json b/web/public/locales/id/audio.json
index cb3f2539f2..b1ea21a628 100644
--- a/web/public/locales/id/audio.json
+++ b/web/public/locales/id/audio.json
@@ -1,91 +1,503 @@
{
- "yell": "Teriakan",
- "speech": "Bahasa",
- "babbling": "Ocehan",
- "bellow": "Di bawah",
- "whoop": "Teriakan",
- "whispering": "Bisikan",
- "snicker": "Tertawa",
+ "yell": "Berteriak",
+ "speech": "Ucapan",
+ "babbling": "Mengoceh (berekah)",
+ "bellow": "Begadang / Meraung",
+ "whoop": "Tertawa lepas (whoop)",
+ "whispering": "Berbisik",
+ "snicker": "Tertawa cekikikan",
"crying": "Menangis",
- "sigh": "Mendesah",
- "choir": "Paduan Suara",
- "yodeling": "Bernyanyi Yodel",
- "chant": "Nyanyian",
+ "sigh": "Menghela napas",
+ "choir": "Paduan suara",
+ "yodeling": "Yodel",
+ "chant": "Berzikir / Menyanyi berulang",
"child_singing": "Anak bernyanyi",
- "rapping": "Mengetuk",
- "humming": "Bersenandung",
- "groan": "Mengerang",
+ "rapping": "Rap",
+ "humming": "Berdengung",
+ "groan": "Menggerung",
"grunt": "Mendengus",
- "breathing": "Bernafas",
+ "breathing": "Bernapas",
"laughter": "Tertawa",
- "singing": "Nyanyian",
+ "singing": "Bernyanyi",
"mantra": "Mantra",
- "synthetic_singing": "Nyanyian sintesis",
- "whistling": "Siulan",
+ "synthetic_singing": "Bernyanyi buatan (sintetis)",
+ "whistling": "Mendengung (mencicit / bersiul)",
"car": "Mobil",
- "motorcycle": "Motor",
+ "motorcycle": "Sepeda motor",
"bicycle": "Sepeda",
- "bus": "Bis",
+ "bus": "Bus",
"train": "Kereta",
- "boat": "Kapal",
+ "boat": "Perahu",
"sneeze": "Bersin",
- "run": "Lari",
+ "run": "Berlari",
"footsteps": "Langkah kaki",
"chewing": "Mengunyah",
"biting": "Menggigit",
- "stomach_rumble": "Perut Keroncongan",
+ "stomach_rumble": "Suara perut bergerak",
"burping": "Sendawa",
- "hiccup": "Cegukan",
+ "hiccup": "Cegukan (hikap)",
"fart": "Kentut",
"hands": "Tangan",
- "heartbeat": "Detak Jantung",
- "applause": "Tepuk Tangan",
- "chatter": "Obrolan",
- "children_playing": "Anak-Anak Bermain",
- "animal": "Binatang",
- "pets": "Peliharaan",
+ "heartbeat": "Detak jantung",
+ "applause": "Tepuk tangan massa",
+ "chatter": "Mengobrol",
+ "children_playing": "Anak‑anak bermain",
+ "animal": "Hewan",
+ "pets": "Hewan peliharaan",
"dog": "Anjing",
- "bark": "Gonggongan",
- "howl": "Melolong",
+ "bark": "Kulit kayu",
+ "howl": "Mengaung",
"cat": "Kucing",
- "meow": "Meong",
- "livestock": "Hewan Ternak",
+ "meow": "Mengeong",
+ "livestock": "Hewan ternak",
"horse": "Kuda",
"cattle": "Sapi",
"pig": "Babi",
"goat": "Kambing",
"sheep": "Domba",
"chicken": "Ayam",
- "cluck": "Berkokok",
- "cock_a_doodle_doo": "Kukuruyuk",
+ "cluck": "Menguk / \"cluck\"",
+ "cock_a_doodle_doo": "Berkokok (\"cock‑a‑doodle‑doo\")",
"turkey": "Kalkun",
"duck": "Bebek",
- "quack": "Kwek",
+ "quack": "Menggock (\"quack\")",
"goose": "Angsa",
- "wild_animals": "Hewan Liar",
+ "wild_animals": "Hewan liar",
"bird": "Burung",
"pigeon": "Merpati",
"crow": "Gagak",
- "owl": "Burung Hantu",
- "flapping_wings": "Kepakan Sayap",
- "dogs": "Anjing",
+ "owl": "Burung hantu",
+ "flapping_wings": "Sayap berkibar",
+ "dogs": "Anjing‑anjing",
"insect": "Serangga",
- "cricket": "Jangkrik",
+ "cricket": "Kerik / Kriket",
"mosquito": "Nyamuk",
"fly": "Lalat",
- "frog": "Katak",
+ "frog": "Kodok",
"snake": "Ular",
"music": "Musik",
- "musical_instrument": "Alat Musik",
+ "musical_instrument": "Instrumen musik",
"guitar": "Gitar",
- "electric_guitar": "Gitar Elektrik",
- "acoustic_guitar": "Gitar Akustik",
- "strum": "Genjreng",
+ "electric_guitar": "Gitar listrik",
+ "acoustic_guitar": "Gitar akustik",
+ "strum": "Mengstrum",
"banjo": "Banjo",
- "snoring": "Ngorok",
+ "snoring": "Mendengkur",
"cough": "Batuk",
- "clapping": "Tepukan",
+ "clapping": "Tepuk tangan",
"camera": "Kamera",
- "wheeze": "Nafas",
- "gasp": "Tersedak"
+ "wheeze": "Mengi",
+ "gasp": "Menggigil / Tarik napas tajam",
+ "sound_effect": "Efek suara (sound effect)",
+ "environmental_noise": "Kebisingan lingkungan",
+ "static": "Suara statis",
+ "white_noise": "White noise",
+ "television": "Televisi",
+ "radio": "Radio",
+ "scream": "Teriakan",
+ "pant": "Terengah-engah",
+ "snort": "Mendengus (melalui hidung)",
+ "throat_clearing": "Membersihkan tenggorokan",
+ "sniff": "Mengendus",
+ "shuffle": "Menyeret kaki",
+ "gargling": "Gargling",
+ "finger_snapping": "Mengklik jari",
+ "heart_murmur": "Murmur jantung",
+ "cheering": "Bersorak",
+ "crowd": "Kerumunan orang",
+ "yip": "Menggonggong pendek / ringkik",
+ "bow_wow": "Gonggongan \"bow wow\" khas",
+ "growling": "Menggeram",
+ "whimper_dog": "Rintihan anjing",
+ "purr": "Mendengkur",
+ "hiss": "Mendesis",
+ "caterwaul": "Mengeong nyaring (melolong)",
+ "clip_clop": "Suara kuda berlari (\"clip‑clop\")",
+ "neigh": "Meringkik",
+ "moo": "Mengamuk / \"Moo\"",
+ "cowbell": "Bel sapi",
+ "oink": "Menggonggong \"oink\"",
+ "bleat": "Mengebik",
+ "fowl": "Unggas",
+ "gobble": "Menggobleg",
+ "honk": "Bebek / \"honk\"",
+ "roaring_cats": "Kucing besar mengaung",
+ "roar": "Mengaung (raungan predator)",
+ "chirp": "Cicit / bernyanyi burung kecil",
+ "squawk": "Mengkokok / mendengung keras",
+ "coo": "Mengkuk \"coo\"",
+ "caw": "Menggagak / \"caw\"",
+ "hoot": "Menghoo / \"hoot\"",
+ "rats": "Tikus",
+ "mouse": "Mouse",
+ "patter": "Peluit kaki kecil",
+ "buzz": "Menggema / \"buzz\"",
+ "croak": "Kokok / \"croak\"",
+ "rattle": "Bersuara \"rattle\"",
+ "whale_vocalization": "Suara vokalisasi paus",
+ "plucked_string_instrument": "Instrumen senar dipetik",
+ "bass_guitar": "Bass gitar",
+ "steel_guitar": "Steel gitar",
+ "tapping": "Mengetuk",
+ "sitar": "Sitar",
+ "mandolin": "Mandolin",
+ "zither": "Zither",
+ "ukulele": "Ukulele",
+ "keyboard": "Keyboard",
+ "piano": "Piano",
+ "electric_piano": "Piano elektrik",
+ "organ": "Organ",
+ "electronic_organ": "Organ elektronik",
+ "hammond_organ": "Organ Hammond",
+ "synthesizer": "Synthesizer",
+ "sampler": "Sampler",
+ "harpsichord": "Harpsichord",
+ "percussion": "Percussion",
+ "drum_kit": "Kotak drum (drum kit)",
+ "drum_machine": "Mesin drum (drum machine)",
+ "drum": "Drum",
+ "snare_drum": "Snare drum",
+ "rimshot": "Rimshot",
+ "drum_roll": "Roll drum",
+ "bass_drum": "Bass drum",
+ "timpani": "Timpani",
+ "tabla": "Tabla",
+ "cymbal": "Cymbal",
+ "hi_hat": "Hi‑hat",
+ "wood_block": "Wood block",
+ "tambourine": "Tambourine",
+ "maraca": "Maraca",
+ "gong": "Gong",
+ "tubular_bells": "Tubular bells",
+ "mallet_percussion": "Percussion palu (mallet)",
+ "marimba": "Marimba",
+ "glockenspiel": "Glockenspiel",
+ "vibraphone": "Vibraphone",
+ "steelpan": "Steelpan",
+ "orchestra": "Orchestra",
+ "brass_instrument": "Instrumen tiup logam (brass)",
+ "french_horn": "French horn",
+ "trumpet": "Trumpet",
+ "trombone": "Trombone",
+ "bowed_string_instrument": "Instrumen senar di‑gesek (bowed string)",
+ "string_section": "Seksi biola (string section)",
+ "violin": "Biola (violin)",
+ "pizzicato": "Pizzicato",
+ "cello": "Violoncello (cello)",
+ "double_bass": "Double bass",
+ "wind_instrument": "Instrumen tiup (wind)",
+ "flute": "Flute",
+ "saxophone": "Saxophone",
+ "clarinet": "Clarinet",
+ "harp": "Harp",
+ "bell": "Bel (lonceng)",
+ "church_bell": "Lonceng gereja",
+ "jingle_bell": "Jingle bell",
+ "bicycle_bell": "Bel sepeda",
+ "tuning_fork": "Tuning fork",
+ "chime": "Chime",
+ "wind_chime": "Wind chime",
+ "harmonica": "Harmonika",
+ "accordion": "Akordian",
+ "bagpipes": "Bagpipes",
+ "didgeridoo": "Didgeridoo",
+ "theremin": "Theremin",
+ "singing_bowl": "Singing bowl",
+ "scratching": "Scratching (DJ scratching)",
+ "pop_music": "Musik pop",
+ "hip_hop_music": "Musik hip‑hop",
+ "beatboxing": "Beatboxing",
+ "rock_music": "Musik rock",
+ "heavy_metal": "Heavy metal",
+ "punk_rock": "Punk rock",
+ "grunge": "Grunge",
+ "progressive_rock": "Progressive rock",
+ "rock_and_roll": "Rock and roll",
+ "psychedelic_rock": "Psychedelic rock",
+ "rhythm_and_blues": "Rhythm and blues",
+ "soul_music": "Soul",
+ "reggae": "Reggae",
+ "country": "Country",
+ "swing_music": "Swing",
+ "bluegrass": "Bluegrass",
+ "funk": "Funk",
+ "folk_music": "Folk",
+ "middle_eastern_music": "Musik Timur Tengah",
+ "jazz": "Jazz",
+ "disco": "Disco",
+ "classical_music": "Musik klasik",
+ "opera": "Opera",
+ "electronic_music": "Musik elektronik",
+ "house_music": "House music",
+ "techno": "Tekno",
+ "dubstep": "Dubstep",
+ "drum_and_bass": "Drum and bass",
+ "electronica": "Electronica",
+ "electronic_dance_music": "Electronic dance music (EDM)",
+ "ambient_music": "Musik ambient",
+ "trance_music": "Trance",
+ "music_of_latin_america": "Musik Amerika Latin",
+ "salsa_music": "Salsa",
+ "flamenco": "Flamenco",
+ "blues": "Blues",
+ "music_for_children": "Musik anak‑anak",
+ "new-age_music": "Musik new age",
+ "vocal_music": "Musik vokal",
+ "a_capella": "A cappella",
+ "music_of_africa": "Musik Afrika",
+ "afrobeat": "Afrobeat",
+ "christian_music": "Musik krisitan / Kristen",
+ "gospel_music": "Musik gospel",
+ "music_of_asia": "Musik Asia",
+ "carnatic_music": "Carnatic music",
+ "music_of_bollywood": "Musik Bollywood",
+ "ska": "Ska",
+ "traditional_music": "Musik tradisional",
+ "independent_music": "Independent music",
+ "song": "Lagu",
+ "background_music": "Background music",
+ "theme_music": "Theme music",
+ "jingle": "Jingle (lagu iklan singkat)",
+ "soundtrack_music": "Musik soundtrack",
+ "lullaby": "Lullaby",
+ "video_game_music": "Musik video game",
+ "christmas_music": "Musik Natal",
+ "dance_music": "Musik dansa",
+ "wedding_music": "Musik pernikahan",
+ "happy_music": "Musik bahagia",
+ "sad_music": "Musik sedih",
+ "tender_music": "Musik lembut / romantis",
+ "exciting_music": "Musik mendebarkan",
+ "angry_music": "Musik marah",
+ "scary_music": "Musik menakutkan",
+ "wind": "Angin",
+ "rustling_leaves": "Daun bergesekan",
+ "wind_noise": "Suara angin",
+ "thunderstorm": "Badai petir",
+ "thunder": "Kilat / guruh (guntur)",
+ "water": "Air",
+ "rain": "Hujan",
+ "raindrop": "Tetesan hujan",
+ "rain_on_surface": "Hujan jatuh ke permukaan",
+ "stream": "Aliran sungai kecil",
+ "waterfall": "Air terjun",
+ "ocean": "Laut",
+ "waves": "Ombak",
+ "steam": "Uap",
+ "gurgling": "Menggulung / bergolak (gurgling)",
+ "fire": "Api",
+ "crackle": "Mercekik / berderak (crackle)",
+ "vehicle": "Kendaraan",
+ "sailboat": "Perahu layar",
+ "rowboat": "Perahu dayung",
+ "motorboat": "Perahu bermotor",
+ "ship": "Kapal besar",
+ "motor_vehicle": "Kendaraan bermotor",
+ "toot": "Bunyi klakson kecil",
+ "car_alarm": "Alarm mobil",
+ "power_windows": "Jendela bergerak dengan tenaga listrik",
+ "skidding": "Selipan roda",
+ "tire_squeal": "Roda tergelincir / berdecit",
+ "car_passing_by": "Mobil melintas",
+ "race_car": "Mobil balap",
+ "truck": "Truk",
+ "air_brake": "Rem udara",
+ "air_horn": "Horn udara",
+ "reversing_beeps": "Bunyi beeper mundur",
+ "ice_cream_truck": "Mobil es krim",
+ "emergency_vehicle": "Kendaraan darurat",
+ "police_car": "Mobil patroli polisi",
+ "ambulance": "Ambulans",
+ "fire_engine": "Mobil pemadam kebakaran",
+ "traffic_noise": "Kebisingan lalu lintas",
+ "rail_transport": "Transportasi rel",
+ "train_whistle": "Pelecut kereta",
+ "train_horn": "Klakson kereta api",
+ "railroad_car": "Gerigit kereta api",
+ "train_wheels_squealing": "Rel kereta berdecit",
+ "subway": "Kereta bawah tanah (subway)",
+ "aircraft": "Pesawat udara",
+ "aircraft_engine": "Mesin pesawat",
+ "jet_engine": "Mesin jet",
+ "propeller": "Propeller",
+ "helicopter": "Helikopter",
+ "fixed-wing_aircraft": "Pesawat sayap tetap",
+ "skateboard": "Papan luncur",
+ "engine": "Mesin",
+ "light_engine": "Mesin ringan",
+ "dental_drill's_drill": "Bor gigi",
+ "lawn_mower": "Mesin pemotong rumput",
+ "chainsaw": "Gergaji mesin / chainsaw",
+ "medium_engine": "Mesin menengah",
+ "heavy_engine": "Mesin berat",
+ "engine_knocking": "Mesin berdecit",
+ "engine_starting": "Mesin dihidupkan",
+ "idling": "Mesin diam tetap hidup (idling)",
+ "accelerating": "Percepatan (accelerating)",
+ "door": "Pintu",
+ "doorbell": "Bel pintu",
+ "ding-dong": "Ding‑dong (bunyi bel pintu khas)",
+ "sliding_door": "Pintu geser",
+ "slam": "Menjekat (bunyi pintu ditutup keras)",
+ "knock": "Ketukan",
+ "tap": "Mengetuk",
+ "squeak": "Berderit",
+ "cupboard_open_or_close": "Kupmar terbuka atau tertutup",
+ "drawer_open_or_close": "Laci terbuka atau tertutup",
+ "dishes": "Piring",
+ "cutlery": "Sendok garpu",
+ "chopping": "Mengiris",
+ "frying": "Menggoreng",
+ "microwave_oven": "Oven microwave",
+ "blender": "Blender",
+ "water_tap": "Kran air",
+ "sink": "Wastafel",
+ "bathtub": "Bak mandi",
+ "hair_dryer": "Pengering rambut",
+ "toilet_flush": "Siraman toilet",
+ "toothbrush": "Sikat gigi",
+ "electric_toothbrush": "Sikat gigi elektrik",
+ "vacuum_cleaner": "Vacuum cleaner",
+ "zipper": "Ritsleting",
+ "keys_jangling": "Kunci berdering",
+ "coin": "Koin",
+ "scissors": "Gunting",
+ "electric_shaver": "Alat cukur listrik",
+ "shuffling_cards": "Mengacaukan kartu",
+ "typing": "Ketikan",
+ "typewriter": "Mesin tik",
+ "computer_keyboard": "Keyboard komputer",
+ "writing": "Menulis",
+ "alarm": "Alarm",
+ "telephone": "Telepon",
+ "telephone_bell_ringing": "Bel telepon berdering",
+ "ringtone": "Nada dering",
+ "telephone_dialing": "Menelepon dengan dial",
+ "dial_tone": "Nada tunggu (dial tone)",
+ "busy_signal": "Suara sibuk",
+ "alarm_clock": "Alarm jam",
+ "siren": "Sirine",
+ "civil_defense_siren": "Sirine perlindungan sipil",
+ "buzzer": "Buzzer",
+ "smoke_detector": "Detektor asap",
+ "fire_alarm": "Alarm kebakaran",
+ "foghorn": "Foghorn (bunyi peluit kabut laut)",
+ "whistle": "Peluit",
+ "steam_whistle": "Peluit uap",
+ "mechanisms": "Mekanisme",
+ "ratchet": "Ratchet",
+ "clock": "Jam",
+ "tick": "Detak (tick)",
+ "tick-tock": "Tick‑tock",
+ "gears": "Roda gigi (gears)",
+ "pulleys": "Katrol",
+ "sewing_machine": "Mesin jahit",
+ "mechanical_fan": "Kipas baling‑baling mekanik",
+ "air_conditioning": "Pendingin ruangan / AC",
+ "cash_register": "Mesin kasir",
+ "printer": "Printer",
+ "single-lens_reflex_camera": "Kamera single‑lens reflex",
+ "tools": "Perkakas",
+ "hammer": "Palu",
+ "jackhammer": "Jackhammer",
+ "sawing": "Menggergaji",
+ "filing": "Mengasah",
+ "sanding": "Mengampelas",
+ "power_tool": "Power tool (perkakas bermotor)",
+ "drill": "Bor",
+ "explosion": "Ledakan",
+ "gunshot": "Tembakan senjata api",
+ "machine_gun": "Senapan mesin",
+ "fusillade": "Fusillade (banyak tembakan sekaligus)",
+ "artillery_fire": "Tembakan artileri",
+ "cap_gun": "Senapan mainan (cap gun)",
+ "fireworks": "Kembang api",
+ "firecracker": "Petasan kembang api",
+ "burst": "Ledakan pecah (burst)",
+ "eruption": "Letusan (eruption)",
+ "boom": "Boom (bunyi ledakan berat)",
+ "wood": "Kayu",
+ "chop": "Menebang (chop)",
+ "splinter": "Bercerai (splinter)",
+ "crack": "Retak / pecah (crack)",
+ "glass": "Kaca",
+ "chink": "Bunyi kaca berdenting (chink)",
+ "shatter": "Hancur / pecah (shatter)",
+ "silence": "Diam / tidak ada suara (silence)",
+ "pink_noise": "Pink noise",
+ "field_recording": "Rekaman lapangan (field recording)",
+ "sodeling": "Menangis tertahan",
+ "chird": "Derit / suara aneh",
+ "change_ringing": "Lantunan lonceng bergantian (change ringing)",
+ "shofar": "Shofar",
+ "liquid": "Cairan",
+ "splash": "Cipratan (splash)",
+ "slosh": "Slosh (suara cairan bergoyang)",
+ "squish": "Squish (bunyi renyah basah)",
+ "drip": "Tetes (drip)",
+ "pour": "Tuang (pour)",
+ "trickle": "Menetes (trickle)",
+ "gush": "Mengalir deras (gush)",
+ "fill": "Mengisi (fill)",
+ "spray": "Semprot (spray)",
+ "pump": "Pompa",
+ "stir": "Aduk (stir)",
+ "boiling": "Mendidih",
+ "sonar": "Sonar",
+ "arrow": "Panah",
+ "whoosh": "Whoosh (bunyi melesat cepat)",
+ "thump": "Thump (bantingan)",
+ "thunk": "Thunk (bunyi tebal tumpul)",
+ "electronic_tuner": "Tuner elektronik",
+ "effects_unit": "Effects unit (efek audio)",
+ "chorus_effect": "Efek chorus",
+ "basketball_bounce": "Pantulan bola basket",
+ "bang": "Benturan keras (bang)",
+ "slap": "Plak (pukulan telapak)",
+ "whack": "Whack (pukulan keras)",
+ "smash": "Smash (hancurkan keras)",
+ "breaking": "Memecahkan",
+ "bouncing": "Memantul",
+ "whip": "Cambuk",
+ "flap": "Flap (sayap / lembaran berkibar)",
+ "scratch": "Gores (scratch)",
+ "scrape": "Gesekan kasar (scrape)",
+ "rub": "Menggosok (rub)",
+ "roll": "Gulung (roll)",
+ "crushing": "Menghancurkan (crushing)",
+ "crumpling": "Menggumpalkan (crumpling)",
+ "tearing": "Merosak (tearing)",
+ "beep": "Beep",
+ "ping": "Ping",
+ "ding": "Ding",
+ "clang": "Clang",
+ "squeal": "Squeal (mengerang)",
+ "creak": "Creak (berderit pelan)",
+ "rustle": "Rustle (menggerut)",
+ "whir": "Whir (menderu putaran cepat)",
+ "clatter": "Kerincing / benturan berantai (clatter)",
+ "sizzle": "Sizzle (menggoreng / bersiul)",
+ "clicking": "Clicking (bunyi kunci)",
+ "clickety_clack": "Clickety clack (bunyi kaki atau rel)",
+ "rumble": "Rumble (gempuran / gemuruh)",
+ "plop": "Plop (bunyi jatuh lembut ke air)",
+ "hum": "Hum (mendengung)",
+ "zing": "Zing (bunyi gesek tipis cepat)",
+ "boing": "Boing (bunyi pegas)",
+ "crunch": "Crunch (remas keras)",
+ "sine_wave": "Gelombang sinus (sine wave)",
+ "harmonic": "Harmonik",
+ "chirp_tone": "Tone chirp",
+ "pulse": "Pulse (detak / pulsa)",
+ "inside": "Di dalam ruangan",
+ "outside": "Di luar ruangan",
+ "reverberation": "Gema ruang (reverberation)",
+ "echo": "Gema (echo)",
+ "noise": "Kebisingan",
+ "mains_hum": "Mains hum (dengungan listrik arus utama)",
+ "distortion": "Distorsi",
+ "sidetone": "Sidetone (suara sendiri saat menelepon)",
+ "cacophony": "Kecemasan suara (cacophony)",
+ "throbbing": "Berdebar / gemuruh (throbbing)",
+ "vibration": "Vibrasi"
}
diff --git a/web/public/locales/id/common.json b/web/public/locales/id/common.json
index b1498de076..cdeb4ab24b 100644
--- a/web/public/locales/id/common.json
+++ b/web/public/locales/id/common.json
@@ -1,17 +1,17 @@
{
"time": {
- "untilForRestart": "Hingga Frigate memulai ulang.",
- "untilRestart": "Sampai memulai ulang",
- "ago": "{{timeAgo}} Lalu",
- "justNow": "Sekarang",
+ "untilForRestart": "Sampai Frigate dimulai ulang.",
+ "untilRestart": "Sampai dimulai ulang",
+ "ago": "{{timeAgo}} yang lalu",
+ "justNow": "Baru saja",
"today": "Hari ini",
"yesterday": "Kemarin",
- "untilForTime": "Hingga {{time}}",
+ "untilForTime": "Sampai {{time}}",
"last7": "7 hari terakhir",
"last14": "14 hari terakhir",
"last30": "30 hari terakhir",
"thisWeek": "Minggu Ini",
- "never": "Tidak Pernah",
+ "never": "Tidak pernah",
"lastWeek": "Minggu Lalu",
"thisMonth": "Bulan Ini",
"lastMonth": "Bulan Lalu",
@@ -21,11 +21,297 @@
"1hour": "1 jam",
"12hours": "12 jam",
"24hours": "24 jam",
- "pm": "pm",
- "am": "am",
- "yr": "{{time}} tahun",
+ "pm": "PM",
+ "am": "AM",
+ "yr": "{{time}} th",
"year_other": "{{time}} tahun",
- "mo": "{{time}} bulan"
+ "mo": "{{time}} bln",
+ "month_other": "{{time}} bulan",
+ "d": "{{time}} hr",
+ "day_other": "{{time}} hari",
+ "h": "{{time}} jam",
+ "hour_other": "{{time}} jam",
+ "m": "{{time}} mnt",
+ "minute_other": "{{time}} menit",
+ "s": "{{time}} dtk",
+ "second_other": "{{time}} detik",
+ "formattedTimestamp": {
+ "12hour": "MMM d, h:mm:ss aaa",
+ "24hour": "MMM d, HH:mm:ss"
+ },
+ "formattedTimestamp2": {
+ "12hour": "MM/dd h:mm:ssa",
+ "24hour": "d MMM HH:mm:ss"
+ },
+ "formattedTimestampHourMinute": {
+ "12hour": "h:mm aaa",
+ "24hour": "HH:mm"
+ },
+ "formattedTimestampHourMinuteSecond": {
+ "12hour": "h:mm:ss aaa",
+ "24hour": "HH:mm:ss"
+ },
+ "formattedTimestampMonthDayHourMinute": {
+ "12hour": "MMM d, h:mm aaa",
+ "24hour": "MMM d, HH:mm"
+ },
+ "formattedTimestampMonthDayYear": {
+ "12hour": "MMM d, yyyy",
+ "24hour": "MMM d, yyyy"
+ },
+ "formattedTimestampMonthDayYearHourMinute": {
+ "12hour": "MMM d yyyy, h:mm aaa",
+ "24hour": "MMM d yyyy, HH:mm"
+ },
+ "formattedTimestampMonthDay": "MMM d",
+ "formattedTimestampFilename": {
+ "12hour": "MM-dd-yy-h-mm-ss-a",
+ "24hour": "MM-dd-yy-HH-mm-ss"
+ },
+ "inProgress": "Sedang berlangsung",
+ "invalidStartTime": "Waktu mulai tidak valid",
+ "invalidEndTime": "Waktu selesai tidak valid"
},
- "readTheDocumentation": "Baca dokumentasi"
+ "readTheDocumentation": "Baca dokumentasi",
+ "menu": {
+ "system": "Sistem",
+ "profiles": "Profil",
+ "systemMetrics": "Metrik sistem",
+ "configuration": "Konfigurasi",
+ "systemLogs": "Log sistem",
+ "settings": "Pengaturan",
+ "configurationEditor": "Editor Konfigurasi",
+ "languages": "Bahasa",
+ "language": {
+ "en": "English (Inggris)",
+ "es": "Español (Spanyol)",
+ "zhCN": "简体中文 (Tionghoa Sederhana)",
+ "hi": "हिन्दी (Hindi)",
+ "fr": "Français (Prancis)",
+ "ar": "العربية (Arab)",
+ "pt": "Português (Portugis)",
+ "ptBR": "Português brasileiro (Portugis Brasil)",
+ "ru": "Русский (Rusia)",
+ "de": "Deutsch (Jerman)",
+ "ja": "日本語 (Jepang)",
+ "tr": "Türkçe (Turki)",
+ "it": "Italiano (Italia)",
+ "nl": "Nederlands (Belanda)",
+ "sv": "Svenska (Swedia)",
+ "cs": "Čeština (Ceko)",
+ "nb": "Norsk Bokmål (Norwegia Bokmål)",
+ "ko": "한국어 (Korea)",
+ "vi": "Tiếng Việt (Vietnam)",
+ "fa": "فارسی (Persia)",
+ "pl": "Polski (Polandia)",
+ "uk": "Українська (Ukraina)",
+ "he": "עברית (Ibrani)",
+ "el": "Ελληνικά (Yunani)",
+ "ro": "Română (Rumania)",
+ "hu": "Magyar (Hungaria)",
+ "fi": "Suomi (Finlandia)",
+ "da": "Dansk (Denmark)",
+ "sk": "Slovenčina (Slovakia)",
+ "yue": "粵語 (Kanton)",
+ "th": "ไทย (Thai)",
+ "ca": "Català (Katalan)",
+ "hr": "Hrvatski (Kroasia)",
+ "bs": "Bosanski (Bosnia)",
+ "sr": "Српски (Serbia)",
+ "sl": "Slovenščina (Slovenia)",
+ "lt": "Lietuvių (Lituania)",
+ "bg": "Български (Bulgaria)",
+ "gl": "Galego (Galisia)",
+ "id": "Bahasa Indonesia (Indonesia)",
+ "ur": "اردو (Urdu)",
+ "withSystem": {
+ "label": "Gunakan pengaturan sistem untuk bahasa"
+ },
+ "zhHant": "繁體中文 (Tionghoa Tradisional)"
+ },
+ "appearance": "Tampilan",
+ "darkMode": {
+ "label": "Mode Gelap",
+ "light": "Terang",
+ "dark": "Gelap",
+ "withSystem": {
+ "label": "Gunakan pengaturan sistem untuk mode terang atau gelap"
+ }
+ },
+ "withSystem": "Sistem",
+ "theme": {
+ "label": "Tema",
+ "blue": "Biru",
+ "green": "Hijau",
+ "nord": "Nord",
+ "red": "Merah",
+ "highcontrast": "Kontras Tinggi",
+ "default": "Default"
+ },
+ "help": "Bantuan",
+ "documentation": {
+ "title": "Dokumentasi",
+ "label": "Dokumentasi Frigate"
+ },
+ "restart": "Mulai Ulang Frigate",
+ "live": {
+ "title": "Live",
+ "allCameras": "Semua Kamera",
+ "cameras": {
+ "title": "Kamera",
+ "count_other": "{{count}} Kamera"
+ }
+ },
+ "review": "Tinjauan",
+ "explore": "Jelajah",
+ "export": "Ekspor",
+ "actions": "Tindakan",
+ "uiPlayground": "UI Playground",
+ "features": "Fitur",
+ "faceLibrary": "Pustaka Wajah",
+ "classification": "Klasifikasi",
+ "chat": "Chat",
+ "user": {
+ "title": "Pengguna",
+ "account": "Akun",
+ "current": "Pengguna Saat Ini: {{user}}",
+ "anonymous": "anonim",
+ "logout": "Keluar",
+ "setPassword": "Atur Kata Sandi"
+ }
+ },
+ "unit": {
+ "speed": {
+ "mph": "mph",
+ "kph": "kph"
+ },
+ "length": {
+ "feet": "kaki",
+ "meters": "meter"
+ },
+ "data": {
+ "kbps": "kB/dtk",
+ "mbps": "MB/dtk",
+ "gbps": "GB/dtk",
+ "kbph": "kB/jam",
+ "mbph": "MB/jam",
+ "gbph": "GB/jam"
+ }
+ },
+ "label": {
+ "back": "Kembali",
+ "hide": "Sembunyikan {{item}}",
+ "show": "Tampilkan {{item}}",
+ "ID": "ID",
+ "none": "Tidak ada",
+ "all": "Semua",
+ "other": "Lainnya"
+ },
+ "list": {
+ "two": "{{0}} dan {{1}}",
+ "many": "{{items}}, dan {{last}}",
+ "separatorWithSpace": ", "
+ },
+ "field": {
+ "optional": "Opsional",
+ "internalID": "ID Internal yang digunakan Frigate dalam konfigurasi dan basis data"
+ },
+ "button": {
+ "add": "Tambah",
+ "apply": "Terapkan",
+ "applying": "Menerapkan…",
+ "reset": "Atur Ulang",
+ "undo": "Urungkan",
+ "done": "Selesai",
+ "enabled": "Diaktifkan",
+ "enable": "Aktifkan",
+ "disabled": "Dinonaktifkan",
+ "disable": "Nonaktifkan",
+ "save": "Simpan",
+ "saving": "Menyimpan…",
+ "cancel": "Batal",
+ "close": "Tutup",
+ "copy": "Salin",
+ "copiedToClipboard": "Disalin ke papan klip",
+ "back": "Kembali",
+ "history": "Riwayat",
+ "fullscreen": "Layar Penuh",
+ "exitFullscreen": "Keluar dari Layar Penuh",
+ "pictureInPicture": "Gambar dalam Gambar",
+ "twoWayTalk": "Audio Dua Arah",
+ "cameraAudio": "Audio Kamera",
+ "on": "AKTIF",
+ "off": "NONAKTIF",
+ "edit": "Edit",
+ "copyCoordinates": "Salin koordinat",
+ "delete": "Hapus",
+ "yes": "Ya",
+ "no": "Tidak",
+ "download": "Unduh",
+ "info": "Info",
+ "suspended": "Ditangguhkan",
+ "unsuspended": "Batalkan penangguhan",
+ "play": "Putar",
+ "unselect": "Batalkan pilihan",
+ "export": "Ekspor",
+ "deleteNow": "Hapus Sekarang",
+ "next": "Berikutnya",
+ "continue": "Lanjutkan",
+ "modified": "Diubah",
+ "overridden": "Ditimpa",
+ "resetToGlobal": "Atur Ulang ke Global",
+ "resetToDefault": "Atur Ulang ke Default",
+ "saveAll": "Simpan Semua",
+ "savingAll": "Menyimpan Semua…",
+ "undoAll": "Urungkan Semua",
+ "retry": "Coba Lagi"
+ },
+ "toast": {
+ "copyUrlToClipboard": "URL disalin ke papan klip.",
+ "save": {
+ "title": "Simpan",
+ "error": {
+ "title": "Gagal menyimpan perubahan konfigurasi: {{errorMessage}}",
+ "noMessage": "Gagal menyimpan perubahan konfigurasi"
+ },
+ "success": "Berhasil menyimpan perubahan konfigurasi."
+ }
+ },
+ "role": {
+ "title": "Peran",
+ "admin": "Admin",
+ "viewer": "Penampil",
+ "desc": "Admin memiliki akses penuh ke semua fitur di UI Frigate. Penampil terbatas hanya untuk melihat kamera, item tinjauan, dan rekaman historis di UI."
+ },
+ "pagination": {
+ "label": "paginasi",
+ "previous": {
+ "title": "Sebelumnya",
+ "label": "Buka halaman sebelumnya"
+ },
+ "next": {
+ "title": "Berikutnya",
+ "label": "Buka halaman berikutnya"
+ },
+ "more": "Halaman lainnya"
+ },
+ "accessDenied": {
+ "documentTitle": "Akses Ditolak - Frigate",
+ "title": "Akses Ditolak",
+ "desc": "Anda tidak memiliki izin untuk melihat halaman ini."
+ },
+ "notFound": {
+ "documentTitle": "Tidak Ditemukan - Frigate",
+ "title": "404",
+ "desc": "Halaman tidak ditemukan"
+ },
+ "selectItem": "Pilih {{item}}",
+ "information": {
+ "pixels": "{{area}}px"
+ },
+ "no_items": "Tidak ada item",
+ "validation_errors": "Kesalahan Validasi",
+ "credentialField": {
+ "savedPlaceholder": "Tersimpan — biarkan kosong untuk mempertahankan yang saat ini"
+ }
}
diff --git a/web/public/locales/id/components/camera.json b/web/public/locales/id/components/camera.json
index de7759f34d..61ca36bf21 100644
--- a/web/public/locales/id/components/camera.json
+++ b/web/public/locales/id/components/camera.json
@@ -42,12 +42,49 @@
"stream": "Siaran",
"placeholder": "Pilih stream",
"streamMethod": {
- "label": "Metode Streaming"
+ "label": "Metode Streaming",
+ "placeholder": "Pilih satu metode streaming",
+ "method": {
+ "noStreaming": {
+ "label": "Tidak Streaming",
+ "desc": "Gambar kamera hanya diperbarui sekali per menit dan tidak akan ada live streaming."
+ },
+ "smartStreaming": {
+ "label": "Streaming cerdas (disarankan)",
+ "desc": "Streaming cerdas akan memperbarui gambar kamera sekali per menit saat tidak ada aktivitas yang terdeteksi untuk menghemat bandwidth dan sumber daya. Ketika aktivitas terdeteksi, gambar akan otomatis beralih ke live stream."
+ },
+ "continuousStreaming": {
+ "desc": {
+ "title": "Kamera akan selalu live stream selama muncul di dashboard, meskipun tidak ada aktivitas yang terdeteksi.",
+ "warning": "Streaming Terus Menerus dapat menyebabkan penggunaan bandwidth yang tinggi dan masalah performa. Gunakan dengan hati-hati."
+ },
+ "label": "Streaming Terus Menerus"
+ }
+ }
+ },
+ "compatibilityMode": {
+ "label": "Mode Kompatibilitas",
+ "desc": "Aktifkan opsi ini hanya jika live stream kamera Anda menampilkan artefak warna dan memiliki garis diagonal di sisi kanan gambar."
}
}
- }
+ },
+ "showAll": "Tampilkan semua grup kamera",
+ "showLess": "Lebih sedikit",
+ "editGroups": "Sunting Grup Kamera"
},
"debug": {
- "boundingBox": "Batas Kotak"
+ "boundingBox": "Batas Kotak",
+ "options": {
+ "label": "Pengaturan",
+ "title": "Opsi",
+ "showOptions": "Lihat Opsi",
+ "hideOptions": "Sembunyikan Opsi"
+ },
+ "zones": "Zona",
+ "motion": "Gerakan",
+ "regions": "Wilayah",
+ "timestamp": "Timestamp",
+ "mask": "Masker",
+ "paths": "Path"
}
}
diff --git a/web/public/locales/id/components/dialog.json b/web/public/locales/id/components/dialog.json
index 35d87b07ca..6ec42c5759 100644
--- a/web/public/locales/id/components/dialog.json
+++ b/web/public/locales/id/components/dialog.json
@@ -52,12 +52,73 @@
"export": "Ekspor",
"selectOrExport": "Pilih atau Ekspor",
"toast": {
- "success": "Berhasil memulai ekspor. Lihat file pada halaman ekspor."
- }
+ "success": "Berhasil memulai ekspor. Lihat file pada halaman ekspor.",
+ "queued": "Ekspor diantrekan. Lihat progres di halaman ekspor.",
+ "view": "Melihat",
+ "batchSuccess_other": "{{count}} Ekspor dimulai. Membuka kasusnya sekarang.",
+ "batchPartial": "Ekspor berhasil dimulai sebanyak {{successful}} dari total {{total}} ekspor. Kamera yang gagal: {{failedCameras}}",
+ "batchFailed": "Gagal memulai ekspor sebanyak {{total}}. Kamera yang gagal: {{failedCameras}}"
+ },
+ "case": {
+ "newCaseOption": "Membuat Kasus Baru",
+ "newCaseNamePlaceholder": "Nama Kasus Baru",
+ "newCaseDescriptionPlaceholder": "Deskripsi Kasus",
+ "label": "Kasus",
+ "nonAdminHelp": "Kasus Baru akan Dibuat untuk Ekspor ini.",
+ "placeholder": "Pilih Kasus"
+ },
+ "tabs": {
+ "export": "Kamera Tunggal",
+ "multiCamera": "Multi Kamera"
+ },
+ "multiCamera": {
+ "timeRange": "Rentang Waktu",
+ "selectFromTimeline": "Pilih dari Timeline",
+ "cameraSelection": "Kamera",
+ "checkingActivity": "Memeriksa Aktivitas Kamera...",
+ "noCameras": "Tidak ada Kamera Tersedia",
+ "detectionCount_other": "{{count}} Objek terlacak",
+ "nameLabel": "Nama Ekspor",
+ "exportButton_other": "Ekspor {{count}} kamera",
+ "cameraSelectionHelp": "Kamera dengan objek yang dilacak dalam rentang waktu ini telah dipilih sebelumnya",
+ "queueingButton": "Mengantrekan ekspor...",
+ "clearSelection": "Hapus pilihan",
+ "selectWithActivity": "Kamera dengan objek yang dilacak",
+ "selectGroup": "Pilih grup",
+ "noMatchingCameras": "Tidak ada kamera yang sesuai dengan pencarian Anda",
+ "selectedCount": "{{terpilih}} / {{total}} terpilih",
+ "namePlaceholder": "Nama dasar opsional untuk ekspor ini"
+ },
+ "multi": {
+ "title_other": "Ekspor {{count}} Ulasan",
+ "toast": {
+ "startedNoCase_other": "{{count}} Ekspor Dimulai.",
+ "started_other": "{{count}} ekspor dimulai. Membuka kasusnya sekarang.",
+ "partial": "Memulai {{successful}} dari {{total}} ekspor. Gagal: {{failedItems}}",
+ "failed": "Gagal memulai {{total}} ekspor. Gagal: {{failedItems}}"
+ },
+ "description": "Ekspor setiap ulasan yang dipilih. Semua Ekspor akan dikelompokkan dalam satu kasus.",
+ "descriptionNoCase": "Ekspor setiap ulasan yang dipilih.",
+ "exportButton_other": "Ekspor {{count}} ulasan",
+ "exportingButton": "Mengekspor...",
+ "caseNamePlaceholder": "Ekspor tinjauan - {{tanggal}}"
+ },
+ "queueing": "Mengantrekan Ekspor..."
},
"search": {
"saveSearch": {
"overwrite": "{{searchName}} sudah ada. Menyimpan akan menimpa file yang sudah ada."
}
+ },
+ "streaming": {
+ "restreaming": {
+ "disabled": "Fitur restreaming tidak diaktifkan untuk kamera ini.",
+ "desc": {
+ "title": "Instal go2rtc untuk opsi tampilan langsung dan audio tambahan untuk kamera ini."
+ }
+ },
+ "showStats": {
+ "label": "Tampilkan statistik streaming"
+ }
}
}
diff --git a/web/public/locales/id/components/player.json b/web/public/locales/id/components/player.json
index 0372a797c7..0adef26797 100644
--- a/web/public/locales/id/components/player.json
+++ b/web/public/locales/id/components/player.json
@@ -5,7 +5,7 @@
"submit": "Kirim",
"title": "Kirim frame ini ke Frigate+?"
},
- "noRecordingsFoundForThisTime": "Tidak ada Rekaman pada waktu ini",
+ "noRecordingsFoundForThisTime": "Tidak ada rekaman ditemukan pada waktu ini",
"livePlayerRequiredIOSVersion": "iOS 17.1 atau yang lebih tinggi diperlukan untuk tipe siaran langsung ini.",
"streamOffline": {
"title": "Stream Tidak Aktif",
diff --git a/web/public/locales/id/config/cameras.json b/web/public/locales/id/config/cameras.json
index 0967ef424b..3b80196174 100644
--- a/web/public/locales/id/config/cameras.json
+++ b/web/public/locales/id/config/cameras.json
@@ -1 +1,62 @@
-{}
+{
+ "label": "Pengaturan Kamera",
+ "name": {
+ "label": "Nama Kamera",
+ "description": "Nama Kamera diwajibkan"
+ },
+ "friendly_name": {
+ "label": "Nama Singkat",
+ "description": "Nama Singkat kamera digunakan pada tampilan UI Frigate"
+ },
+ "audio": {
+ "label": "Deteksi Suara",
+ "description": "Pengaturan untuk Deteksi Kejadian berdasarkan Suara pada kamera ini.",
+ "enabled": {
+ "label": "Nyalakan Deteksi Suara",
+ "description": "Nyalakan atau matikan deteksi kejadian suara pada kamera ini."
+ },
+ "filters": {
+ "threshold": {
+ "label": "Keyakinan-Suara Minimum"
+ }
+ },
+ "min_volume": {
+ "label": "Volume-Suara Minimum"
+ }
+ },
+ "audio_transcription": {
+ "label": "Transkripsi Suara",
+ "enabled": {
+ "label": "Nyalakan Transkripsi"
+ },
+ "live_enabled": {
+ "label": "Transkripsi Langsung (Live)"
+ }
+ },
+ "detect": {
+ "label": "Deteksi Objek",
+ "enabled": {
+ "label": "Nyalakan Deteksi Objek"
+ },
+ "stationary": {
+ "classifier": {
+ "label": "Nyalakan Klasifikasi-Visual",
+ "description": "Menggunakan pengklasifikasi visual untuk membedakan objek-objek diam (benar-benar tidak bergerak), meskipun bounding-box kurang stabil atau bergetar (jitter)."
+ }
+ },
+ "fps": {
+ "label": "Kecepatan (FPS) Deteksi",
+ "description": "Kecepatan yang ditargetkan untuk menjalankan Deteksi Objek, dalam satuan frame per second (FPS); nilai lebih rendah mengurangi intensitas proses dan dapat meringangkan beban kerja CPU. Nilai 5 direkomendasikan, sedangakan nilai 10 dianggap sangat tinggi dan hanya digunakan untuk pelacakan (tracking) benda yang bergerak dengan benar-benar cepat."
+ }
+ },
+ "enabled": {
+ "label": "Dinyalakan",
+ "description": "Dinyalakan (Enabled)"
+ },
+ "birdseye": {
+ "enabled": {
+ "description": "Nyalakan atau matikan fitur Penglihatan Atas (Birdseye View).",
+ "label": "Nyalakan Birdseye"
+ }
+ }
+}
diff --git a/web/public/locales/id/config/global.json b/web/public/locales/id/config/global.json
index 0967ef424b..9b8409655f 100644
--- a/web/public/locales/id/config/global.json
+++ b/web/public/locales/id/config/global.json
@@ -1 +1,47 @@
-{}
+{
+ "version": {
+ "label": "Versi konfigurasi"
+ },
+ "audio": {
+ "label": "Deteksi Suara",
+ "enabled": {
+ "label": "Nyalakan Deteksi Suara"
+ },
+ "filters": {
+ "threshold": {
+ "label": "Keyakinan-Suara Minimum"
+ }
+ },
+ "min_volume": {
+ "label": "Volume-Suara Minimum"
+ }
+ },
+ "audio_transcription": {
+ "label": "Transkripsi Suara",
+ "live_enabled": {
+ "label": "Transkripsi Langsung (Live)"
+ }
+ },
+ "detect": {
+ "label": "Deteksi Objek",
+ "enabled": {
+ "label": "Nyalakan Deteksi Objek"
+ },
+ "stationary": {
+ "classifier": {
+ "label": "Nyalakan Klasifikasi-Visual",
+ "description": "Menggunakan pengklasifikasi visual untuk membedakan objek-objek diam (benar-benar tidak bergerak), meskipun bounding-box kurang stabil atau bergetar (jitter)."
+ }
+ },
+ "fps": {
+ "label": "Kecepatan (FPS) Deteksi",
+ "description": "Kecepatan yang ditargetkan untuk menjalankan Deteksi Objek, dalam satuan frame per second (FPS); nilai lebih rendah mengurangi intensitas proses dan dapat meringangkan beban kerja CPU. Nilai 5 direkomendasikan, sedangakan nilai 10 dianggap sangat tinggi dan hanya digunakan untuk pelacakan (tracking) benda yang bergerak dengan benar-benar cepat."
+ }
+ },
+ "birdseye": {
+ "enabled": {
+ "description": "Nyalakan atau matikan fitur Penglihatan Atas (Birdseye View).",
+ "label": "Nyalakan Birdseye"
+ }
+ }
+}
diff --git a/web/public/locales/id/config/groups.json b/web/public/locales/id/config/groups.json
index 0967ef424b..7e1fd9f862 100644
--- a/web/public/locales/id/config/groups.json
+++ b/web/public/locales/id/config/groups.json
@@ -1 +1,7 @@
-{}
+{
+ "audio": {
+ "global": {
+ "detection": "Deteksi Global"
+ }
+ }
+}
diff --git a/web/public/locales/id/config/validation.json b/web/public/locales/id/config/validation.json
index 0967ef424b..3be9f0d2f8 100644
--- a/web/public/locales/id/config/validation.json
+++ b/web/public/locales/id/config/validation.json
@@ -1 +1,3 @@
-{}
+{
+ "minimum": "Nilai harus lebih besar dari {{limit}}"
+}
diff --git a/web/public/locales/id/objects.json b/web/public/locales/id/objects.json
index e56f051d01..82f56b05c8 100644
--- a/web/public/locales/id/objects.json
+++ b/web/public/locales/id/objects.json
@@ -2,30 +2,128 @@
"person": "Orang",
"bicycle": "Sepeda",
"car": "Mobil",
- "motorcycle": "Motor",
+ "motorcycle": "Sepeda motor",
"airplane": "Pesawat",
- "bus": "Bis",
+ "bus": "Bus",
"train": "Kereta",
- "boat": "Kapal",
- "traffic_light": "Lampu Lalu Lintas",
- "fire_hydrant": "Hidran Kebakaran",
- "animal": "Binatang",
+ "boat": "Perahu",
+ "traffic_light": "Lampu lalu lintas",
+ "fire_hydrant": "Hidran kebakaran",
+ "animal": "Hewan",
"dog": "Anjing",
- "bark": "Gonggongan",
+ "bark": "Kulit kayu",
"cat": "Kucing",
"horse": "Kuda",
"goat": "Kambing",
"sheep": "Domba",
"bird": "Burung",
- "street_sign": "Rambu Jalan",
- "stop_sign": "Tanda Stop",
- "parking_meter": "Parkir Meter",
- "bench": "Kursi",
+ "street_sign": "Rambu jalan",
+ "stop_sign": "Rambu berhenti",
+ "parking_meter": "Meter parkir",
+ "bench": "Bangku",
"cow": "Sapi",
"elephant": "Gajah",
"bear": "Beruang",
"zebra": "Zebra",
"giraffe": "Jerapah",
"hat": "Topi",
- "backpack": "Tas"
+ "backpack": "Ransel",
+ "mouse": "Mouse",
+ "keyboard": "Keyboard",
+ "vehicle": "Kendaraan",
+ "skateboard": "Papan luncur",
+ "door": "Pintu",
+ "blender": "Blender",
+ "sink": "Wastafel",
+ "hair_dryer": "Pengering rambut",
+ "toothbrush": "Sikat gigi",
+ "scissors": "Gunting",
+ "clock": "Jam",
+ "umbrella": "Payung",
+ "shoe": "Sepatu",
+ "eye_glasses": "Kacamata",
+ "handbag": "Tas tangan",
+ "tie": "Dasi",
+ "suitcase": "Koper",
+ "frisbee": "Frisbee",
+ "skis": "Ski",
+ "snowboard": "Papan seluncur salju",
+ "sports_ball": "Bola olahraga",
+ "kite": "Layang-layang",
+ "baseball_bat": "Tongkat bisbol",
+ "baseball_glove": "Sarung tangan bisbol",
+ "surfboard": "Papan selancar",
+ "tennis_racket": "Raket tenis",
+ "bottle": "Botol",
+ "plate": "Piring",
+ "wine_glass": "Gelas anggur",
+ "cup": "Cangkir",
+ "fork": "Garpu",
+ "knife": "Pisau",
+ "spoon": "Sendok",
+ "bowl": "Mangkuk",
+ "banana": "Pisang",
+ "apple": "Apel",
+ "sandwich": "Sandwich",
+ "orange": "Jeruk",
+ "broccoli": "Brokoli",
+ "carrot": "Wortel",
+ "hot_dog": "Hot dog",
+ "pizza": "Pizza",
+ "donut": "Donat",
+ "cake": "Kue",
+ "chair": "Kursi",
+ "couch": "Sofa",
+ "potted_plant": "Tanaman dalam pot",
+ "bed": "Tempat tidur",
+ "mirror": "Cermin",
+ "dining_table": "Meja makan",
+ "window": "Jendela",
+ "desk": "Meja tulis",
+ "toilet": "Toilet",
+ "tv": "TV",
+ "laptop": "Laptop",
+ "remote": "Remote",
+ "cell_phone": "Ponsel",
+ "microwave": "Microwave",
+ "oven": "Oven",
+ "toaster": "Pemanggang roti",
+ "refrigerator": "Kulkas",
+ "book": "Buku",
+ "vase": "Vas",
+ "teddy_bear": "Boneka beruang",
+ "hair_brush": "Sikat rambut",
+ "squirrel": "Tupai",
+ "deer": "Rusa",
+ "fox": "Rubah",
+ "rabbit": "Kelinci",
+ "raccoon": "Rakuns",
+ "robot_lawnmower": "Mesin pemotong rumput robot",
+ "waste_bin": "Tempat sampah",
+ "on_demand": "Sesuai permintaan",
+ "face": "Wajah",
+ "license_plate": "Pelat nomor",
+ "package": "Paket",
+ "bbq_grill": "Panggangan BBQ",
+ "amazon": "Amazon",
+ "usps": "USPS",
+ "ups": "UPS",
+ "fedex": "FedEx",
+ "dhl": "DHL",
+ "an_post": "An Post",
+ "purolator": "Purolator",
+ "postnl": "PostNL",
+ "nzpost": "NZPost",
+ "postnord": "PostNord",
+ "gls": "GLS",
+ "dpd": "DPD",
+ "canada_post": "Canada Post",
+ "royal_mail": "Royal Mail",
+ "school_bus": "Bus sekolah",
+ "skunk": "Sigung",
+ "kangaroo": "Kanguru",
+ "baby": "Bayi",
+ "baby_stroller": "Kereta dorong bayi",
+ "rickshaw": "Becak",
+ "rodent": "Hewan pengerat"
}
diff --git a/web/public/locales/id/views/chat.json b/web/public/locales/id/views/chat.json
new file mode 100644
index 0000000000..4fc251afdb
--- /dev/null
+++ b/web/public/locales/id/views/chat.json
@@ -0,0 +1,3 @@
+{
+ "documentTitle": "Chat - Frigate"
+}
diff --git a/web/public/locales/id/views/classificationModel.json b/web/public/locales/id/views/classificationModel.json
index 55ca9051dd..9ea38ad20d 100644
--- a/web/public/locales/id/views/classificationModel.json
+++ b/web/public/locales/id/views/classificationModel.json
@@ -2,7 +2,7 @@
"documentTitle": "Klasifikasi Model - Frigate",
"details": {
"scoreInfo": "Skor tersebut mewakili rata-rata kepercayaan klasifikasi di seluruh deteksi objek ini.",
- "none": "Tidak ada",
+ "none": "Tidak Ada",
"unknown": "Tidak diketahui"
},
"button": {
diff --git a/web/public/locales/id/views/events.json b/web/public/locales/id/views/events.json
index 19a85885f5..bb78ea70aa 100644
--- a/web/public/locales/id/views/events.json
+++ b/web/public/locales/id/views/events.json
@@ -16,7 +16,9 @@
}
},
"timeline.aria": "Pilih timeline",
- "timeline": "Linimasa",
+ "timeline": {
+ "label": "Linimasa"
+ },
"zoomIn": "Perbesar",
"zoomOut": "Perkecil",
"events": {
@@ -43,7 +45,9 @@
},
"documentTitle": "Tinjauan - Frigate",
"recordings": {
- "documentTitle": "Rekaman - Frigate"
+ "documentTitle": "Rekaman - Frigate",
+ "invalidSharedLink": "Tidak dapat membuka tautan rekaman bertanda waktu karena kesalahan penguraian.",
+ "invalidSharedCamera": "Tidak dapat membuka tautan rekaman bertanda waktu karena kamera tidak dikenal atau tidak berwenang."
},
"calendarFilter": {
"last24Hours": "24 Jam Terakhir"
@@ -54,10 +58,37 @@
"button": "Item Batu Untuk Ditinjau",
"label": "Lihat item ulasan baru"
},
- "selected_one": "{{count}} terpilih",
- "selected_other": "{{count}} terpilih",
+ "selected_one": "{{count}} dipilih",
+ "selected_other": "{{count}} dipilih",
"camera": "Kamera",
"detected": "terdeteksi",
"suspiciousActivity": "Aktivitas Mencurigakan",
- "threateningActivity": "Aktivitas yang Mengancam"
+ "threateningActivity": "Aktivitas yang Mengancam",
+ "select_all": "Semua",
+ "normalActivity": "Normal",
+ "needsReview": "Perlu ditinjau",
+ "securityConcern": "Kendala keamanan",
+ "motionSearch": {
+ "menuItem": "Pencarian gerakan",
+ "openMenu": "Opsi kamera"
+ },
+ "motionPreviews": {
+ "menuItem": "Lihat pratinjau gerakan",
+ "title": "Pratinjau gerakan: {{camera}}",
+ "mobileSettingsTitle": "Setelan Pratinjau Gerakan",
+ "mobileSettingsDesc": "Sesuaikan kecepatan pemutaran dan peredupan, serta pilih tanggal untuk meninjau klip hanya gerakan.",
+ "dim": "Redup",
+ "dimAria": "Sesuaikan intensitas peredupan",
+ "dimDesc": "Tingkatkan peredupan untuk meningkatkan visibilitas area gerakan.",
+ "speed": "Kecepatan",
+ "speedAria": "Pilih kecepatan pemutaran pratinjau",
+ "speedDesc": "Pilih seberapa cepat klip pratinjau diputar.",
+ "back": "Kembali",
+ "empty": "Tidak ada pratinjau tersedia",
+ "noPreview": "Pratinjau tidak tersedia",
+ "seekAria": "Pindahkan {{camera}} pemain ke {{time}}",
+ "filter": "Filter",
+ "filterDesc": "Pilih area untuk hanya menampilkan klip dengan gerakan di wilayah tersebut.",
+ "filterClear": "Hapus"
+ }
}
diff --git a/web/public/locales/id/views/explore.json b/web/public/locales/id/views/explore.json
index b93d4bf617..726548db3b 100644
--- a/web/public/locales/id/views/explore.json
+++ b/web/public/locales/id/views/explore.json
@@ -2,45 +2,262 @@
"documentTitle": "Jelajahi - Frigate",
"generativeAI": "AI Generatif",
"exploreIsUnavailable": {
- "title": "Penelusuran tidak tersedia",
+ "title": "Jelajah Tidak Tersedia",
"embeddingsReindexing": {
- "context": "Jelajahi dapat digunakan setelah embedding objek yang dilacak selesai di-reindex.",
+ "context": "Jelajah dapat digunakan setelah embedding objek terlacak selesai diindeks ulang.",
"startingUp": "Sedang memulai…",
"estimatedTime": "Perkiraan waktu tersisa:",
- "finishingShortly": "Selesai sesaat lagi",
+ "finishingShortly": "Segera selesai",
"step": {
- "thumbnailsEmbedded": "Keluku dilampirkan ",
- "descriptionsEmbedded": "Deskripsi terlampir: ",
- "trackedObjectsProcessed": "Objek yang dilacak diproses: "
+ "thumbnailsEmbedded": "Thumbnail yang disematkan: ",
+ "descriptionsEmbedded": "Deskripsi yang disematkan: ",
+ "trackedObjectsProcessed": "Objek terlacak yang diproses: "
}
},
"downloadingModels": {
- "context": "Frigate sedang mengunduh model embedding yang diperlukan untuk mendukung fitur Pencarian Semantik. Proses ini mungkin memakan waktu beberapa menit tergantung pada kecepatan koneksi jaringan Anda.",
+ "context": "Frigate sedang mengunduh model embedding yang diperlukan untuk mendukung fitur Pencarian Semantik. Ini mungkin memerlukan beberapa menit tergantung pada kecepatan koneksi jaringan Anda.",
"setup": {
- "visionModel": "Model vision",
- "visionModelFeatureExtractor": "Ekstraktor fitur model visi",
+ "visionModel": "Model visi",
+ "visionModelFeatureExtractor": "Pengekstrak fitur model visi",
"textModel": "Model teks",
- "textTokenizer": "Teks tokenizer"
+ "textTokenizer": "Tokenizer teks"
},
"tips": {
- "context": "Anda mungkin ingin mengindeks ulang embeddings dari objek yang Anda lacak setelah model-model tersebut diunduh."
+ "context": "Anda mungkin ingin mengindeks ulang embedding objek terlacak Anda setelah model selesai diunduh."
},
- "error": "Terjadi eror. Periksa log Frigate."
+ "error": "Terjadi kesalahan. Periksa log Frigate."
}
},
"details": {
- "timestamp": "Stempel waktu"
+ "timestamp": "Cap waktu",
+ "item": {
+ "title": "Detail Item Tinjauan",
+ "desc": "Detail item tinjauan",
+ "button": {
+ "share": "Bagikan item tinjauan ini",
+ "viewInExplore": "Lihat di Jelajah"
+ },
+ "tips": {
+ "mismatch_other": "{{count}} objek yang tidak tersedia terdeteksi dan disertakan dalam item tinjauan ini. Objek-objek tersebut tidak memenuhi syarat sebagai peringatan atau deteksi, atau sudah dibersihkan/dihapus.",
+ "hasMissingObjects": "Sesuaikan konfigurasi Anda jika Anda ingin Frigate menyimpan objek terlacak untuk label berikut: {{objects}} "
+ },
+ "toast": {
+ "success": {
+ "regenerate": "Deskripsi baru telah diminta dari {{provider}}. Tergantung pada kecepatan penyedia Anda, deskripsi baru mungkin memerlukan waktu untuk dibuat ulang.",
+ "updatedSublabel": "Berhasil memperbarui sublabel.",
+ "updatedLPR": "Berhasil memperbarui pelat nomor.",
+ "updatedAttributes": "Berhasil memperbarui atribut.",
+ "audioTranscription": "Berhasil meminta transkripsi audio. Tergantung pada kecepatan server Frigate Anda, transkripsi mungkin memerlukan waktu untuk selesai."
+ },
+ "error": {
+ "regenerate": "Gagal memanggil {{provider}} untuk deskripsi baru: {{errorMessage}}",
+ "updatedSublabelFailed": "Gagal memperbarui sublabel: {{errorMessage}}",
+ "updatedLPRFailed": "Gagal memperbarui pelat nomor: {{errorMessage}}",
+ "updatedAttributesFailed": "Gagal memperbarui atribut: {{errorMessage}}",
+ "audioTranscription": "Gagal meminta transkripsi audio: {{errorMessage}}"
+ }
+ }
+ },
+ "label": "Label",
+ "editSubLabel": {
+ "title": "Edit sublabel",
+ "desc": "Masukkan sublabel baru untuk {{label}} ini",
+ "descNoLabel": "Masukkan sublabel baru untuk objek terlacak ini"
+ },
+ "editLPR": {
+ "title": "Edit pelat nomor",
+ "desc": "Masukkan nilai pelat nomor baru untuk {{label}} ini",
+ "descNoLabel": "Masukkan nilai pelat nomor baru untuk objek terlacak ini"
+ },
+ "editAttributes": {
+ "title": "Edit atribut",
+ "desc": "Pilih atribut klasifikasi untuk {{label}} ini"
+ },
+ "snapshotScore": {
+ "label": "Skor Snapshot"
+ },
+ "topScore": {
+ "label": "Skor Tertinggi",
+ "info": "Skor tertinggi adalah skor median tertinggi untuk objek terlacak, jadi ini mungkin berbeda dari skor yang ditampilkan pada thumbnail hasil pencarian."
+ },
+ "score": {
+ "label": "Skor"
+ },
+ "recognizedLicensePlate": "Pelat Nomor yang Diakui",
+ "attributes": "Atribut Klasifikasi",
+ "estimatedSpeed": "Perkiraan Kecepatan",
+ "objects": "Objek",
+ "camera": "Kamera",
+ "zones": "Zona",
+ "button": {
+ "findSimilar": "Cari yang Serupa",
+ "regenerate": {
+ "title": "Buat Ulang",
+ "label": "Buat ulang deskripsi objek terlacak"
+ }
+ },
+ "description": {
+ "label": "Deskripsi",
+ "placeholder": "Deskripsi objek terlacak",
+ "aiTips": "Frigate tidak akan meminta deskripsi dari penyedia AI Generatif Anda sampai siklus hidup objek terlacak berakhir."
+ },
+ "expandRegenerationMenu": "Perluas menu pembuatan ulang",
+ "regenerateFromSnapshot": "Buat Ulang dari Snapshot",
+ "regenerateFromThumbnails": "Buat Ulang dari Thumbnail",
+ "tips": {
+ "descriptionSaved": "Berhasil menyimpan deskripsi",
+ "saveDescriptionFailed": "Gagal memperbarui deskripsi: {{errorMessage}}"
+ },
+ "title": {
+ "label": "Judul"
+ },
+ "scoreInfo": "Informasi Skor"
},
- "exploreMore": "Eksplor lebih jauh objek-objek {{label}}",
+ "exploreMore": "Jelajahi lebih banyak objek {{label}}",
"trackedObjectDetails": "Detail Objek Terlacak",
"type": {
"details": "detail",
- "snapshot": "tangkapan layar",
+ "snapshot": "snapshot",
"thumbnail": "thumbnail",
"video": "video",
"tracking_details": "detail pelacakan"
},
"trackingDetails": {
- "title": "Detail Pelacakan"
+ "title": "Detail Pelacakan",
+ "noImageFound": "Tidak ada gambar yang ditemukan untuk cap waktu ini.",
+ "createObjectMask": "Buat Masker Objek",
+ "adjustAnnotationSettings": "Sesuaikan pengaturan anotasi",
+ "scrollViewTips": "Klik untuk melihat momen-momen penting dalam siklus hidup objek ini.",
+ "autoTrackingTips": "Posisi kotak pembatas tidak akan akurat untuk kamera dengan pelacakan otomatis.",
+ "count": "{{first}} dari {{second}}",
+ "trackedPoint": "Titik Terlacak",
+ "lifecycleItemDesc": {
+ "visible": "{{label}} terdeteksi",
+ "entered_zone": "{{label}} memasuki {{zones}}",
+ "active": "{{label}} menjadi aktif",
+ "stationary": "{{label}} menjadi diam",
+ "attribute": {
+ "faceOrLicense_plate": "{{attribute}} terdeteksi untuk {{label}}",
+ "other": "{{label}} dikenali sebagai {{attribute}}"
+ },
+ "gone": "{{label}} pergi",
+ "heard": "{{label}} terdengar",
+ "external": "{{label}} terdeteksi",
+ "header": {
+ "zones": "Zona",
+ "ratio": "Rasio",
+ "area": "Area",
+ "score": "Skor",
+ "computedScore": "Skor Terhitung",
+ "topScore": "Skor Tertinggi",
+ "toggleAdvancedScores": "Alihkan skor lanjutan"
+ }
+ },
+ "annotationSettings": {
+ "title": "Pengaturan Anotasi",
+ "showAllZones": {
+ "title": "Tampilkan Semua Zona",
+ "desc": "Selalu tampilkan zona pada frame tempat objek telah memasuki suatu zona."
+ },
+ "offset": {
+ "label": "Offset Anotasi",
+ "desc": "Data ini berasal dari feed deteksi kamera Anda tetapi ditumpangkan pada gambar dari feed rekaman. Sangat mungkin kedua stream tersebut tidak sinkron sepenuhnya. Akibatnya, kotak pembatas dan rekaman tidak akan sejajar dengan sempurna. Anda dapat menggunakan pengaturan ini untuk menggeser anotasi maju atau mundur dalam waktu agar lebih selaras dengan rekaman video.",
+ "millisecondsToOffset": "Milidetik untuk menggeser anotasi deteksi. Default: 0 ",
+ "tips": "Turunkan nilainya jika pemutaran video berada di depan kotak dan titik jalur, dan naikkan nilainya jika pemutaran video berada di belakangnya. Nilai ini bisa negatif.",
+ "toast": {
+ "success": "Offset anotasi untuk {{camera}} telah disimpan ke file konfigurasi."
+ }
+ }
+ },
+ "carousel": {
+ "previous": "Slide sebelumnya",
+ "next": "Slide berikutnya"
+ }
+ },
+ "itemMenu": {
+ "downloadVideo": {
+ "label": "Unduh video",
+ "aria": "Unduh video"
+ },
+ "downloadSnapshot": {
+ "label": "Unduh snapshot",
+ "aria": "Unduh snapshot"
+ },
+ "downloadCleanSnapshot": {
+ "label": "Unduh snapshot bersih",
+ "aria": "Unduh snapshot bersih"
+ },
+ "viewTrackingDetails": {
+ "label": "Lihat detail pelacakan",
+ "aria": "Tampilkan detail pelacakan"
+ },
+ "findSimilar": {
+ "label": "Cari yang serupa",
+ "aria": "Cari objek terlacak yang serupa"
+ },
+ "addTrigger": {
+ "label": "Tambahkan pemicu",
+ "aria": "Tambahkan pemicu untuk objek terlacak ini"
+ },
+ "audioTranscription": {
+ "label": "Transkripsikan",
+ "aria": "Minta transkripsi audio"
+ },
+ "submitToPlus": {
+ "label": "Kirim ke Frigate+",
+ "aria": "Kirim ke Frigate Plus"
+ },
+ "viewInHistory": {
+ "label": "Lihat di Riwayat",
+ "aria": "Lihat di Riwayat"
+ },
+ "deleteTrackedObject": {
+ "label": "Hapus objek terlacak ini"
+ },
+ "showObjectDetails": {
+ "label": "Tampilkan jalur objek"
+ },
+ "hideObjectDetails": {
+ "label": "Sembunyikan jalur objek"
+ },
+ "debugReplay": {
+ "label": "Pemutaran Ulang Debug",
+ "aria": "Lihat objek terlacak ini dalam tampilan pemutaran ulang debug"
+ },
+ "more": {
+ "aria": "Lainnya"
+ }
+ },
+ "dialog": {
+ "confirmDelete": {
+ "title": "Konfirmasi Hapus",
+ "desc": "Menghapus objek terlacak ini akan menghapus snapshot, embedding yang tersimpan, dan entri detail pelacakan terkait. Rekaman video dari objek terlacak ini di tampilan Riwayat TIDAK akan dihapus. Anda yakin ingin melanjutkan?"
+ },
+ "toast": {
+ "error": "Kesalahan saat menghapus objek terlacak ini: {{errorMessage}}"
+ }
+ },
+ "noTrackedObjects": "Tidak Ada Objek Terlacak Ditemukan",
+ "fetchingTrackedObjectsFailed": "Kesalahan saat mengambil objek terlacak: {{errorMessage}}",
+ "trackedObjectsCount_other": "{{count}} objek terlacak ",
+ "searchResult": {
+ "tooltip": "Cocok dengan {{type}} pada {{confidence}}%",
+ "previousTrackedObject": "Objek terlacak sebelumnya",
+ "nextTrackedObject": "Objek terlacak berikutnya",
+ "deleteTrackedObject": {
+ "toast": {
+ "success": "Objek terlacak berhasil dihapus.",
+ "error": "Gagal menghapus objek terlacak: {{errorMessage}}"
+ }
+ }
+ },
+ "aiAnalysis": {
+ "title": "Analisis AI"
+ },
+ "concerns": {
+ "label": "Kekhawatiran"
+ },
+ "objectLifecycle": {
+ "noImageFound": "Tidak ada gambar yang ditemukan untuk objek terlacak ini."
}
}
diff --git a/web/public/locales/id/views/exports.json b/web/public/locales/id/views/exports.json
index 043c313de1..bee59ce903 100644
--- a/web/public/locales/id/views/exports.json
+++ b/web/public/locales/id/views/exports.json
@@ -1,23 +1,128 @@
{
- "documentTitle": "Expor - Frigate",
- "search": "Cari",
- "noExports": "Ekspor tidak ditemukan",
- "deleteExport": "Hapus Ekspor",
- "deleteExport.desc": "Apakah Anda yakin ingin menghapus {{exportName}}?",
+ "documentTitle": "Ekspor - Frigate",
+ "search": "Pencarian",
+ "noExports": "Tidak ada ekspor ditemukan",
+ "deleteExport": {
+ "label": "Hapus Ekspor"
+ },
+ "deleteExport.desc": "Anda yakin ingin menghapus {{exportName}}?",
"editExport": {
- "title": "Ganti Nama Ekspor",
+ "title": "Ubah Nama Ekspor",
"desc": "Masukkan nama baru untuk ekspor ini.",
"saveExport": "Simpan Ekspor"
},
"toast": {
"error": {
- "renameExportFailed": "Gagal mengganti nama ekspor: {{errorMessage}}"
+ "renameExportFailed": "Gagal mengubah nama ekspor: {{errorMessage}}",
+ "assignCaseFailed": "Gagal memperbarui penetapan kasus: {{errorMessage}}",
+ "caseSaveFailed": "Gagal menyimpan kasus: {{errorMessage}}",
+ "caseDeleteFailed": "Gagal menghapus kasus: {{errorMessage}}"
}
},
"tooltip": {
- "shareExport": "Bagikan Ekspor",
- "downloadVideo": "Unduh Video",
+ "shareExport": "Bagikan ekspor",
+ "downloadVideo": "Unduh video",
"editName": "Ubah nama",
- "deleteExport": "Hapus ekspor"
+ "deleteExport": "Hapus ekspor",
+ "assignToCase": "Tambahkan ke kasus",
+ "removeFromCase": "Hapus dari kasus"
+ },
+ "headings": {
+ "cases": "Kasus",
+ "uncategorizedExports": "Ekspor Tanpa Kategori"
+ },
+ "toolbar": {
+ "newCase": "Kasus Baru",
+ "addExport": "Tambahkan Ekspor",
+ "editCase": "Edit Kasus",
+ "deleteCase": "Hapus Kasus"
+ },
+ "deleteCase": {
+ "label": "Hapus Kasus",
+ "desc": "Anda yakin ingin menghapus {{caseName}}?",
+ "descKeepExports": "Ekspor akan tetap tersedia sebagai ekspor tanpa kategori.",
+ "descDeleteExports": "Semua ekspor dalam kasus ini akan dihapus secara permanen.",
+ "deleteExports": "Hapus juga ekspor"
+ },
+ "caseDialog": {
+ "title": "Tambahkan ke kasus",
+ "description": "Pilih kasus yang sudah ada atau buat yang baru.",
+ "selectLabel": "Kasus",
+ "newCaseOption": "Buat kasus baru",
+ "nameLabel": "Nama kasus",
+ "descriptionLabel": "Deskripsi"
+ },
+ "caseCard": {
+ "emptyCase": "Belum ada ekspor"
+ },
+ "jobCard": {
+ "defaultName": "Ekspor {{camera}}",
+ "queued": "Dalam antrean",
+ "running": "Sedang berjalan",
+ "preparing": "Menyiapkan",
+ "copying": "Menyalin",
+ "encoding": "Menyandi",
+ "encodingRetry": "Menyandi (coba lagi)",
+ "finalizing": "Menyelesaikan"
+ },
+ "caseView": {
+ "noDescription": "Tidak ada deskripsi",
+ "createdAt": "Dibuat {{value}}",
+ "exportCount_one": "1 ekspor",
+ "exportCount_other": "{{count}} ekspor",
+ "cameraCount_one": "1 kamera",
+ "cameraCount_other": "{{count}} kamera",
+ "showMore": "Tampilkan lebih banyak",
+ "showLess": "Tampilkan lebih sedikit",
+ "emptyTitle": "Kasus ini kosong",
+ "emptyDescription": "Tambahkan ekspor tanpa kategori yang sudah ada agar kasus tetap terorganisasi.",
+ "emptyDescriptionNoExports": "Belum ada ekspor tanpa kategori yang tersedia untuk ditambahkan."
+ },
+ "caseEditor": {
+ "createTitle": "Buat Kasus",
+ "editTitle": "Edit Kasus",
+ "namePlaceholder": "Nama kasus",
+ "descriptionPlaceholder": "Tambahkan catatan atau konteks untuk kasus ini"
+ },
+ "addExportDialog": {
+ "title": "Tambahkan Ekspor ke {{caseName}}",
+ "searchPlaceholder": "Cari ekspor tanpa kategori",
+ "empty": "Tidak ada ekspor tanpa kategori yang cocok dengan pencarian ini.",
+ "addButton_one": "Tambahkan 1 Ekspor",
+ "addButton_other": "Tambahkan {{count}} Ekspor",
+ "adding": "Menambahkan..."
+ },
+ "selected_one": "{{count}} dipilih",
+ "selected_other": "{{count}} dipilih",
+ "bulkActions": {
+ "addToCase": "Tambahkan ke Kasus",
+ "moveToCase": "Pindahkan ke Kasus",
+ "removeFromCase": "Hapus dari Kasus",
+ "delete": "Hapus",
+ "deleteNow": "Hapus Sekarang"
+ },
+ "bulkDelete": {
+ "title": "Hapus Ekspor",
+ "desc_one": "Anda yakin ingin menghapus {{count}} ekspor?",
+ "desc_other": "Anda yakin ingin menghapus {{count}} ekspor?"
+ },
+ "bulkRemoveFromCase": {
+ "title": "Hapus dari Kasus",
+ "desc_one": "Hapus {{count}} ekspor dari kasus ini?",
+ "desc_other": "Hapus {{count}} ekspor dari kasus ini?",
+ "descKeepExports": "Ekspor akan dipindahkan ke tanpa kategori.",
+ "descDeleteExports": "Ekspor akan dihapus secara permanen.",
+ "deleteExports": "Hapus ekspor saja"
+ },
+ "bulkToast": {
+ "success": {
+ "delete": "Berhasil menghapus ekspor",
+ "reassign": "Berhasil memperbarui penetapan kasus",
+ "remove": "Berhasil menghapus ekspor dari kasus"
+ },
+ "error": {
+ "deleteFailed": "Gagal menghapus ekspor: {{errorMessage}}",
+ "reassignFailed": "Gagal memperbarui penetapan kasus: {{errorMessage}}"
+ }
}
}
diff --git a/web/public/locales/id/views/faceLibrary.json b/web/public/locales/id/views/faceLibrary.json
index 70b2a419a6..ec0c700d6f 100644
--- a/web/public/locales/id/views/faceLibrary.json
+++ b/web/public/locales/id/views/faceLibrary.json
@@ -9,7 +9,7 @@
"subLabelScore": "Skor Sub Label",
"face": "Detail Wajah",
"scoreInfo": "Skor sub label adalah nilai gabungan dari tingkat keyakinan sistem dalam mengenali wajah. Nilai ini bisa berbeda dengan skor yang terlihat pada gambar cuplikan.",
- "timestamp": "Stempel waktu",
+ "timestamp": "Cap waktu",
"unknown": "Tidak diketahui",
"faceDesc": "Detail objek terlacak yang menghasilkan wajah ini"
},
diff --git a/web/public/locales/id/views/live.json b/web/public/locales/id/views/live.json
index 96e4575230..416d688964 100644
--- a/web/public/locales/id/views/live.json
+++ b/web/public/locales/id/views/live.json
@@ -1,54 +1,57 @@
{
- "documentTitle.withCamera": "{{camera}} - Langsung - Frigate",
- "documentTitle": "Langsung - Frigate",
- "lowBandwidthMode": "Mode Bandwith-Rendah",
+ "documentTitle.withCamera": "{{camera}} - Live - Frigate",
+ "documentTitle": {
+ "default": "Live - Frigate"
+ },
+ "lowBandwidthMode": "Mode bandwidth rendah",
"twoWayTalk": {
- "enable": "Nyalakan Komunikasi dua arah",
- "disable": "Nonaktifkan Komunikasi Dua Arah"
+ "enable": "Aktifkan Audio Dua Arah",
+ "disable": "Nonaktifkan Audio Dua Arah"
},
"cameraAudio": {
- "enable": "Nyalakan Audio Kamera",
- "disable": "Matikan Audio Kamera"
+ "enable": "Aktifkan Audio Kamera",
+ "disable": "Nonaktifkan Audio Kamera"
},
"ptz": {
"move": {
"clickMove": {
- "label": "Klik kotak ini untuk menengahkan kamera",
- "enable": "Aktifkan klik untuk bergerak",
- "disable": "Non-aktifkan klik untuk bergerak"
+ "label": "Klik pada frame untuk memusatkan kamera",
+ "enable": "Aktifkan klik untuk memindahkan",
+ "disable": "Nonaktifkan klik untuk memindahkan",
+ "enableWithZoom": "Aktifkan klik untuk memindahkan / seret untuk memperbesar"
},
"left": {
- "label": "Geser kamera PTZ ke kiri"
+ "label": "Gerakkan kamera PTZ ke kiri"
},
"up": {
- "label": "Geser kamera PTZ keatas"
+ "label": "Gerakkan kamera PTZ ke atas"
},
"down": {
- "label": "Geser kamera PTZ kebawah"
+ "label": "Gerakkan kamera PTZ ke bawah"
},
"right": {
- "label": "Geser kamera PTZ ke kanan"
+ "label": "Gerakkan kamera PTZ ke kanan"
}
},
"zoom": {
"in": {
- "label": "Perbesar kamera PTZ"
+ "label": "Perbesar zoom kamera PTZ"
},
"out": {
- "label": "Perkecil kamera PTZ"
+ "label": "Perkecil zoom kamera PTZ"
}
},
"focus": {
"in": {
- "label": "Fokus kamera PTZ kedalam"
+ "label": "Fokuskan kamera PTZ ke dalam"
},
"out": {
- "label": "Fokus kamera PTZ keluar"
+ "label": "Fokuskan kamera PTZ ke luar"
}
},
"frame": {
"center": {
- "label": "Klik pada frame untuk menengahkan kamera PTZ"
+ "label": "Klik pada frame untuk memusatkan kamera PTZ"
}
},
"presets": "Preset kamera PTZ"
@@ -59,10 +62,139 @@
},
"muteCameras": {
"enable": "Bisukan Semua Kamera",
- "disable": "Bunyikan Semua Kamera"
+ "disable": "Suarakan Semua Kamera"
},
"detect": {
- "enable": "Aktifkan Pendeteksi",
- "disable": "Nonaktifkan Pendeteksi"
+ "enable": "Aktifkan Deteksi",
+ "disable": "Nonaktifkan Deteksi"
+ },
+ "recording": {
+ "enable": "Aktifkan Perekaman",
+ "disable": "Nonaktifkan Perekaman",
+ "disabledInConfig": "Perekaman harus terlebih dahulu diaktifkan di Pengaturan untuk kamera ini."
+ },
+ "snapshots": {
+ "enable": "Aktifkan Snapshot",
+ "disable": "Nonaktifkan Snapshot"
+ },
+ "snapshot": {
+ "takeSnapshot": "Unduh snapshot instan",
+ "noVideoSource": "Tidak ada sumber video yang tersedia untuk snapshot.",
+ "captureFailed": "Gagal mengambil snapshot.",
+ "downloadStarted": "Pengunduhan snapshot dimulai."
+ },
+ "audioDetect": {
+ "enable": "Aktifkan Deteksi Audio",
+ "disable": "Nonaktifkan Deteksi Audio"
+ },
+ "transcription": {
+ "enable": "Aktifkan Transkripsi Audio Langsung",
+ "disable": "Nonaktifkan Transkripsi Audio Langsung"
+ },
+ "autotracking": {
+ "enable": "Aktifkan Pelacakan Otomatis",
+ "disable": "Nonaktifkan Pelacakan Otomatis"
+ },
+ "streamStats": {
+ "enable": "Tampilkan Statistik Stream",
+ "disable": "Sembunyikan Statistik Stream"
+ },
+ "manualRecording": {
+ "title": "Sesuai Permintaan",
+ "tips": "Unduh snapshot instan atau mulai event manual berdasarkan pengaturan retensi rekaman kamera ini.",
+ "playInBackground": {
+ "label": "Putar di latar belakang",
+ "desc": "Aktifkan opsi ini untuk melanjutkan streaming saat pemutar disembunyikan."
+ },
+ "showStats": {
+ "label": "Tampilkan Statistik",
+ "desc": "Aktifkan opsi ini untuk menampilkan statistik stream sebagai overlay pada umpan kamera."
+ },
+ "debugView": "Tampilan Debug",
+ "start": "Mulai perekaman sesuai permintaan",
+ "started": "Perekaman manual sesuai permintaan dimulai.",
+ "failedToStart": "Gagal memulai perekaman manual sesuai permintaan.",
+ "recordDisabledTips": "Karena perekaman dinonaktifkan atau dibatasi dalam konfigurasi untuk kamera ini, hanya snapshot yang akan disimpan.",
+ "end": "Akhiri perekaman sesuai permintaan",
+ "ended": "Perekaman manual sesuai permintaan diakhiri.",
+ "failedToEnd": "Gagal mengakhiri perekaman manual sesuai permintaan."
+ },
+ "streamingSettings": "Pengaturan Streaming",
+ "notifications": "Notifikasi",
+ "audio": "Audio",
+ "suspend": {
+ "forTime": "Tangguhkan selama: "
+ },
+ "stream": {
+ "title": "Stream",
+ "audio": {
+ "tips": {
+ "title": "Audio harus dikeluarkan oleh kamera Anda dan dikonfigurasi di go2rtc untuk stream ini."
+ },
+ "available": "Audio tersedia untuk stream ini",
+ "unavailable": "Audio tidak tersedia untuk stream ini"
+ },
+ "debug": {
+ "picker": "Pemilihan stream tidak tersedia dalam mode debug. Tampilan debug selalu menggunakan stream yang ditetapkan ke peran detect."
+ },
+ "twoWayTalk": {
+ "tips": "Perangkat Anda harus mendukung fitur ini dan WebRTC harus dikonfigurasi untuk audio dua arah.",
+ "available": "Audio dua arah tersedia untuk stream ini",
+ "unavailable": "Audio dua arah tidak tersedia untuk stream ini"
+ },
+ "lowBandwidth": {
+ "tips": "Tampilan live berada dalam mode bandwidth rendah karena buffering atau kesalahan stream.",
+ "resetStream": "Atur ulang stream"
+ },
+ "playInBackground": {
+ "label": "Putar di latar belakang",
+ "tips": "Aktifkan opsi ini untuk melanjutkan streaming saat pemutar disembunyikan."
+ }
+ },
+ "cameraSettings": {
+ "title": "Pengaturan {{camera}}",
+ "cameraEnabled": "Kamera Diaktifkan",
+ "objectDetection": "Deteksi Objek",
+ "recording": "Perekaman",
+ "snapshots": "Snapshot",
+ "audioDetection": "Deteksi Audio",
+ "transcription": "Transkripsi Audio",
+ "autotracking": "Pelacakan Otomatis"
+ },
+ "history": {
+ "label": "Tampilkan rekaman historis"
+ },
+ "effectiveRetainMode": {
+ "modes": {
+ "all": "Semua",
+ "motion": "Gerakan",
+ "active_objects": "Objek Aktif"
+ }
+ },
+ "editLayout": {
+ "label": "Edit Tata Letak",
+ "group": {
+ "label": "Edit Grup Kamera"
+ },
+ "exitEdit": "Keluar dari Mode Edit"
+ },
+ "noCameras": {
+ "title": "Tidak Ada Kamera yang Dikonfigurasi",
+ "description": "Mulai dengan menghubungkan kamera ke Frigate.",
+ "buttonText": "Tambahkan Kamera",
+ "restricted": {
+ "title": "Tidak Ada Kamera yang Tersedia",
+ "description": "Anda tidak memiliki izin untuk melihat kamera apa pun di grup ini."
+ },
+ "default": {
+ "title": "Tidak Ada Kamera yang Dikonfigurasi",
+ "description": "Mulai dengan menghubungkan kamera ke Frigate.",
+ "buttonText": "Tambahkan Kamera"
+ },
+ "group": {
+ "title": "Tidak Ada Kamera dalam Grup",
+ "description": "Grup kamera ini tidak memiliki kamera yang ditetapkan atau diaktifkan.",
+ "buttonText": "Kelola Grup"
+ }
}
}
diff --git a/web/public/locales/id/views/motionSearch.json b/web/public/locales/id/views/motionSearch.json
new file mode 100644
index 0000000000..3a098d03ee
--- /dev/null
+++ b/web/public/locales/id/views/motionSearch.json
@@ -0,0 +1,73 @@
+{
+ "documentTitle": "Pencarian Gerakan - Frigate",
+ "title": "Pencarian Gerakan",
+ "description": "Gambar poligon untuk menentukan wilayah yang diminati, lalu tentukan rentang waktu untuk mencari perubahan gerakan di dalam wilayah tersebut.",
+ "selectCamera": "Pencarian Gerakan sedang dimuat",
+ "startSearch": "Mulai Pencarian",
+ "searchStarted": "Pencarian dimulai",
+ "searchCancelled": "Pencarian dibatalkan",
+ "cancelSearch": "Batal",
+ "searching": "Pencarian sedang berlangsung.",
+ "searchComplete": "Pencarian selesai",
+ "noResultsYet": "Jalankan pencarian untuk menemukan perubahan gerakan di wilayah yang dipilih",
+ "noChangesFound": "Tidak ada perubahan piksel yang terdeteksi di wilayah yang dipilih",
+ "changesFound_other": "Ditemukan {{count}} perubahan gerakan",
+ "framesProcessed": "{{count}} frame diproses",
+ "jumpToTime": "Lompat ke waktu ini",
+ "results": "Hasil",
+ "showSegmentHeatmap": "Peta panas",
+ "newSearch": "Pencarian Baru",
+ "clearResults": "Hapus Hasil",
+ "clearROI": "Hapus poligon",
+ "polygonControls": {
+ "points_other": "{{count}} titik",
+ "undo": "Urungkan titik terakhir",
+ "reset": "Atur ulang poligon"
+ },
+ "motionHeatmapLabel": "Peta Panas Gerakan",
+ "dialog": {
+ "title": "Pencarian Gerakan",
+ "cameraLabel": "Kamera",
+ "previewAlt": "Pratinjau kamera untuk {{camera}}"
+ },
+ "timeRange": {
+ "title": "Rentang Pencarian",
+ "start": "Waktu mulai",
+ "end": "Waktu selesai"
+ },
+ "settings": {
+ "title": "Pengaturan Pencarian",
+ "parallelMode": "Mode paralel",
+ "parallelModeDesc": "Pindai beberapa segmen rekaman secara bersamaan (lebih cepat, tetapi penggunaan CPU jauh lebih tinggi)",
+ "threshold": "Ambang Sensitivitas",
+ "thresholdDesc": "Nilai yang lebih rendah mendeteksi perubahan yang lebih kecil (1-255)",
+ "minArea": "Luas Perubahan Minimum",
+ "minAreaDesc": "Persentase minimum dari wilayah yang diminati yang harus berubah agar dianggap signifikan",
+ "frameSkip": "Lewati Frame",
+ "frameSkipDesc": "Proses setiap frame ke-N. Atur ini ke frame rate kamera Anda untuk memproses satu frame per detik (misalnya 5 untuk kamera 5 FPS, 30 untuk kamera 30 FPS). Nilai yang lebih tinggi akan lebih cepat, tetapi bisa melewatkan kejadian gerakan singkat.",
+ "maxResults": "Jumlah Hasil Maksimum",
+ "maxResultsDesc": "Berhenti setelah sebanyak ini cap waktu yang cocok"
+ },
+ "errors": {
+ "noCamera": "Silakan pilih kamera",
+ "noROI": "Silakan gambar wilayah yang diminati",
+ "noTimeRange": "Silakan pilih rentang waktu",
+ "invalidTimeRange": "Waktu selesai harus setelah waktu mulai",
+ "searchFailed": "Pencarian gagal: {{message}}",
+ "polygonTooSmall": "Poligon harus memiliki setidaknya 3 titik",
+ "unknown": "Kesalahan tidak diketahui"
+ },
+ "changePercentage": "{{percentage}}% berubah",
+ "metrics": {
+ "title": "Metrik Pencarian",
+ "segmentsScanned": "Segmen dipindai",
+ "segmentsProcessed": "Diproses",
+ "segmentsSkippedInactive": "Dilewati (tidak ada aktivitas)",
+ "segmentsSkippedHeatmap": "Dilewati (tidak ada tumpang tindih ROI)",
+ "fallbackFullRange": "Pemindaian rentang penuh cadangan",
+ "framesDecoded": "Frame didekode",
+ "wallTime": "Waktu pencarian",
+ "segmentErrors": "Kesalahan segmen",
+ "seconds": "{{seconds}} dtk"
+ }
+}
diff --git a/web/public/locales/id/views/replay.json b/web/public/locales/id/views/replay.json
new file mode 100644
index 0000000000..d47dbd0c90
--- /dev/null
+++ b/web/public/locales/id/views/replay.json
@@ -0,0 +1,59 @@
+{
+ "title": "Pemutaran Ulang Debug",
+ "description": "Putar ulang rekaman kamera untuk debugging. Daftar objek menampilkan ringkasan objek terdeteksi yang tertunda waktu, dan tab Pesan menampilkan aliran pesan internal Frigate dari rekaman pemutaran ulang.",
+ "websocket_messages": "Pesan",
+ "dialog": {
+ "title": "Mulai Pemutaran Ulang Debug",
+ "description": "Buat kamera pemutaran ulang sementara yang memutar berulang rekaman historis untuk men-debug masalah deteksi dan pelacakan objek. Kamera pemutaran ulang akan memiliki konfigurasi deteksi yang sama dengan kamera sumber. Pilih rentang waktu untuk memulai.",
+ "camera": "Kamera Sumber",
+ "timeRange": "Rentang Waktu",
+ "preset": {
+ "1m": "1 Menit Terakhir",
+ "5m": "5 Menit Terakhir",
+ "timeline": "Dari Linimasa",
+ "custom": "Kustom"
+ },
+ "startButton": "Mulai Pemutaran Ulang",
+ "selectFromTimeline": "Pilih",
+ "starting": "Memulai pemutaran ulang...",
+ "startLabel": "Mulai",
+ "endLabel": "Selesai",
+ "toast": {
+ "error": "Gagal memulai pemutaran ulang debug: {{error}}",
+ "alreadyActive": "Sesi pemutaran ulang sudah aktif",
+ "stopError": "Gagal menghentikan pemutaran ulang debug: {{error}}",
+ "goToReplay": "Buka Pemutaran Ulang"
+ }
+ },
+ "page": {
+ "noSession": "Tidak Ada Sesi Pemutaran Ulang Debug Aktif",
+ "noSessionDesc": "Mulai Pemutaran Ulang Debug dari tampilan Riwayat dengan mengeklik tombol Aksi di bilah alat dan memilih Pemutaran Ulang Debug.",
+ "goToRecordings": "Buka Riwayat",
+ "preparingClip": "Menyiapkan klip…",
+ "preparingClipDesc": "Frigate sedang menggabungkan rekaman untuk rentang waktu yang dipilih. Ini dapat memakan waktu satu menit untuk rentang yang lebih panjang.",
+ "startingCamera": "Memulai Pemutaran Ulang Debug…",
+ "startError": {
+ "title": "Gagal memulai Pemutaran Ulang Debug",
+ "back": "Kembali ke Riwayat"
+ },
+ "sourceCamera": "Kamera Sumber",
+ "replayCamera": "Kamera Pemutaran Ulang",
+ "initializingReplay": "Menginisialisasi Pemutaran Ulang Debug...",
+ "stoppingReplay": "Menghentikan Pemutaran Ulang Debug...",
+ "stopReplay": "Hentikan Pemutaran Ulang",
+ "confirmStop": {
+ "title": "Hentikan Pemutaran Ulang Debug?",
+ "description": "Ini akan menghentikan sesi dan membersihkan semua data sementara. Anda yakin?",
+ "confirm": "Hentikan Pemutaran Ulang",
+ "cancel": "Batal"
+ },
+ "activity": "Aktivitas",
+ "objects": "Daftar Objek",
+ "audioDetections": "Deteksi Audio",
+ "noActivity": "Tidak ada aktivitas terdeteksi",
+ "activeTracking": "Pelacakan aktif",
+ "noActiveTracking": "Tidak ada pelacakan aktif",
+ "configuration": "Konfigurasi",
+ "configurationDesc": "Sesuaikan secara halus pengaturan deteksi gerakan dan pelacakan objek untuk kamera Pemutaran Ulang Debug. Tidak ada perubahan yang disimpan ke file konfigurasi Frigate Anda."
+ }
+}
diff --git a/web/public/locales/id/views/search.json b/web/public/locales/id/views/search.json
index 724b2b2d6c..9a4aa09baa 100644
--- a/web/public/locales/id/views/search.json
+++ b/web/public/locales/id/views/search.json
@@ -1,36 +1,73 @@
{
- "search": "Cari",
- "savedSearches": "Simpan Pencarian",
+ "search": "Pencarian",
+ "savedSearches": "Pencarian Tersimpan",
"searchFor": "Cari untuk {{inputValue}}",
"button": {
- "clear": "Bersihkan pencarian",
- "save": "Simpan Pencarian",
+ "clear": "Hapus pencarian",
+ "save": "Simpan pencarian",
"delete": "Hapus pencarian yang disimpan",
- "filterInformation": "Saring Informasi",
+ "filterInformation": "Informasi filter",
"filterActive": "Filter aktif"
},
- "trackedObjectId": "Tracked Object ID",
+ "trackedObjectId": "ID Objek yang Dilacak",
"filter": {
"label": {
"cameras": "Kamera",
"labels": "Label",
"zones": "Zona",
- "sub_labels": "Sublabel",
+ "sub_labels": "Sub Label",
"attributes": "Atribut",
- "search_type": "Tipe pencarian",
+ "search_type": "Jenis Pencarian",
"time_range": "Rentang Waktu",
"before": "Sebelum",
"after": "Sesudah",
- "min_score": "Minimal Skor",
- "max_score": "Maks Skor",
- "min_speed": "Kecepatan Min",
- "max_speed": "Kecepatan Maks",
- "recognized_license_plate": "Plat Kendaraan Dikenali",
- "has_clip": "Memiliki Klip",
- "has_snapshot": "Memiliki tangkapan layar"
+ "min_score": "Skor Minimum",
+ "max_score": "Skor Maksimum",
+ "min_speed": "Kecepatan Minimum",
+ "max_speed": "Kecepatan Maksimum",
+ "recognized_license_plate": "Pelat Nomor yang Diakui",
+ "has_clip": "Memiliki Video Klip",
+ "has_snapshot": "Memiliki Snapshot"
},
"searchType": {
- "thumbnail": "Tumbnail"
+ "thumbnail": "Gambar Mini",
+ "description": "Deskripsi"
+ },
+ "toast": {
+ "error": {
+ "beforeDateBeLaterAfter": "Tanggal 'before' harus lebih akhir daripada tanggal 'after'.",
+ "afterDatebeEarlierBefore": "Tanggal 'after' harus lebih awal daripada tanggal 'before'.",
+ "minScoreMustBeLessOrEqualMaxScore": "'min_score' harus lebih kecil dari atau sama dengan 'max_score'.",
+ "maxScoreMustBeGreaterOrEqualMinScore": "'max_score' harus lebih besar dari atau sama dengan 'min_score'.",
+ "minSpeedMustBeLessOrEqualMaxSpeed": "'min_speed' harus lebih kecil dari atau sama dengan 'max_speed'.",
+ "maxSpeedMustBeGreaterOrEqualMinSpeed": "'max_speed' harus lebih besar dari atau sama dengan 'min_speed'."
+ }
+ },
+ "tips": {
+ "title": "Cara menggunakan filter teks",
+ "desc": {
+ "text": "Filter membantu Anda mempersempit hasil pencarian. Berikut cara menggunakannya di kolom input:",
+ "step1": "Ketik nama kunci filter diikuti tanda titik dua (misalnya, \"cameras:\").",
+ "step2": "Pilih nilai dari saran atau ketik nilai Anda sendiri.",
+ "step3": "Gunakan beberapa filter dengan menambahkannya satu per satu dengan spasi di antaranya.",
+ "step4": "Filter tanggal (before: dan after:) menggunakan format {{DateFormat}}.",
+ "step5": "Filter rentang waktu menggunakan format {{exampleTime}}.",
+ "step6": "Hapus filter dengan mengklik tanda 'x' di sebelahnya.",
+ "exampleLabel": "Contoh:"
+ }
+ },
+ "header": {
+ "currentFilterType": "Nilai Filter",
+ "noFilters": "Filter",
+ "activeFilters": "Filter Aktif"
}
+ },
+ "similaritySearch": {
+ "title": "Pencarian Kemiripan",
+ "active": "Pencarian kemiripan aktif",
+ "clear": "Hapus pencarian kemiripan"
+ },
+ "placeholder": {
+ "search": "Pencarian…"
}
}
diff --git a/web/public/locales/id/views/settings.json b/web/public/locales/id/views/settings.json
index 1a74776d59..d738cdd080 100644
--- a/web/public/locales/id/views/settings.json
+++ b/web/public/locales/id/views/settings.json
@@ -4,48 +4,1950 @@
"camera": "Pengaturan Kamera - Frigate",
"classification": "Pengaturan Klasifikasi - Frigate",
"authentication": "Pengaturan Autentikasi - Frigate",
- "masksAndZones": "Editor Mask dan Zona - Frigate",
+ "masksAndZones": "Editor Masker dan Zona - Frigate",
"motionTuner": "Penyetel Gerakan - Frigate",
- "general": "Frigate - Pengaturan Umum",
+ "general": "Pengaturan UI - Frigate",
"object": "Debug - Frigate",
- "enrichments": "Frigate - Pengaturan Pengayaan",
- "cameraManagement": "Pengaturan Kamera - Frigate",
- "cameraReview": "Pengaturan Ulasan Kamera - Frigate",
+ "enrichments": "Pengaturan Pengayaan - Frigate",
+ "cameraManagement": "Kelola Kamera - Frigate",
+ "cameraReview": "Pengaturan Tinjauan Kamera - Frigate",
"frigatePlus": "Pengaturan Frigate+ - Frigate",
- "notifications": "Pengaturan Notifikasi - Frigate"
+ "notifications": "Pengaturan Notifikasi - Frigate",
+ "globalConfig": "Konfigurasi Global - Frigate",
+ "cameraConfig": "Konfigurasi Kamera - Frigate",
+ "detectorsAndModel": "Detektor dan model - Frigate",
+ "maintenance": "Pemeliharaan - Frigate",
+ "profiles": "Profil - Frigate"
},
"menu": {
- "cameraManagement": "Manajemen",
+ "cameraManagement": "Manajemen kamera",
"notifications": "Notifikasi",
- "ui": "Antarmuka Pengguna",
- "enrichments": "Peningkatan",
- "cameraReview": "Ulasan",
- "motionTuner": "Pengatur Gerak",
+ "ui": "UI",
+ "enrichments": "Pengayaan",
+ "cameraReview": "Tinjauan",
+ "motionTuner": "Penyetel gerakan",
"triggers": "Pemicu",
"users": "Pengguna",
"roles": "Peran",
"frigateplus": "Frigate+",
- "masksAndZones": "Mask / Zona",
- "debug": "Debug"
+ "masksAndZones": "Masker / Zona",
+ "debug": "Debug",
+ "general": "Umum",
+ "globalConfig": "Konfigurasi global",
+ "system": "Sistem",
+ "integrations": "Integrasi",
+ "cameras": "Konfigurasi kamera",
+ "uiSettings": "Pengaturan UI",
+ "profiles": "Profil",
+ "globalDetect": "Deteksi objek",
+ "globalRecording": "Perekaman",
+ "globalSnapshots": "Cuplikan",
+ "globalFfmpeg": "FFmpeg",
+ "globalMotion": "Deteksi gerakan",
+ "globalObjects": "Objek",
+ "globalReview": "Tinjauan",
+ "globalAudioEvents": "Deteksi audio",
+ "globalLivePlayback": "Pemutaran langsung",
+ "globalTimestampStyle": "Gaya stempel waktu",
+ "systemDatabase": "Basis data",
+ "systemTls": "TLS",
+ "systemAuthentication": "Autentikasi",
+ "systemNetworking": "Jaringan",
+ "systemProxy": "Proksi",
+ "systemUi": "UI",
+ "systemLogging": "Pencatatan log",
+ "systemEnvironmentVariables": "Variabel lingkungan",
+ "systemTelemetry": "Telemetri",
+ "systemBirdseye": "Birdseye",
+ "systemFfmpeg": "FFmpeg",
+ "systemDetectorsAndModel": "Detektor dan model",
+ "systemMqtt": "MQTT",
+ "systemGo2rtcStreams": "Stream go2rtc",
+ "integrationSemanticSearch": "Pencarian semantik",
+ "integrationGenerativeAi": "AI generatif",
+ "integrationFaceRecognition": "Pengenalan wajah",
+ "integrationLpr": "Pengenalan pelat nomor",
+ "integrationObjectClassification": "Klasifikasi objek",
+ "integrationAudioTranscription": "Transkripsi audio",
+ "cameraDetect": "Deteksi objek",
+ "cameraFfmpeg": "FFmpeg",
+ "cameraRecording": "Perekaman",
+ "cameraSnapshots": "Cuplikan",
+ "cameraMotion": "Deteksi gerakan",
+ "cameraObjects": "Objek",
+ "cameraConfigReview": "Tinjauan",
+ "cameraAudioEvents": "Deteksi audio",
+ "cameraAudioTranscription": "Transkripsi audio",
+ "cameraNotifications": "Notifikasi",
+ "cameraLivePlayback": "Pemutaran langsung",
+ "cameraBirdseye": "Birdseye",
+ "cameraFaceRecognition": "Pengenalan wajah",
+ "cameraLpr": "Pengenalan pelat nomor",
+ "cameraMqttConfig": "MQTT",
+ "cameraOnvif": "ONVIF",
+ "cameraUi": "UI Kamera",
+ "cameraTimestampStyle": "Gaya stempel waktu",
+ "cameraMqtt": "MQTT Kamera",
+ "maintenance": "Pemeliharaan",
+ "mediaSync": "Sinkronisasi media",
+ "regionGrid": "Grid wilayah"
},
"dialog": {
"unsavedChanges": {
"title": "Anda memiliki perubahan yang belum disimpan.",
- "desc": "Apakah Anda ingin menyimpan perubahan Anda sebelum melanjutkan?"
+ "desc": "Apakah Anda ingin menyimpan perubahan sebelum melanjutkan?"
}
},
"cameraSetting": {
"camera": "Kamera",
- "noCamera": "Tidak Ada Kamera"
+ "noCamera": "Tidak ada kamera"
},
"general": {
- "title": "Pengaturan Antarmuka Pengguna",
+ "title": "Pengaturan UI",
"liveDashboard": {
- "title": "Dashboard Langsung",
+ "title": "Dasbor Langsung",
"automaticLiveView": {
"label": "Tampilan Langsung Otomatis",
- "desc": "Secara otomatis beralih ke tampilan langsung kamera saat aktivitas terdeteksi. Menonaktifkan opsi ini menyebabkan gambar statis kamera di dasbor langsung hanya diperbarui sekali per menit."
+ "desc": "Secara otomatis beralih ke tampilan langsung kamera saat aktivitas terdeteksi. Menonaktifkan opsi ini menyebabkan gambar kamera statis pada dasbor Langsung hanya diperbarui sekali per menit."
+ },
+ "playAlertVideos": {
+ "label": "Putar Video Peringatan",
+ "desc": "Secara default, peringatan terbaru di dasbor Langsung diputar sebagai video kecil yang berulang. Nonaktifkan opsi ini untuk hanya menampilkan gambar statis dari peringatan terbaru pada perangkat/browser ini."
+ },
+ "displayCameraNames": {
+ "label": "Selalu Tampilkan Nama Kamera",
+ "desc": "Selalu tampilkan nama kamera dalam chip pada dasbor tampilan langsung multi-kamera."
+ },
+ "liveFallbackTimeout": {
+ "label": "Batas Waktu Fallback Pemutar Langsung",
+ "desc": "Saat stream langsung kualitas tinggi kamera tidak tersedia, alihkan ke mode bandwidth rendah setelah sekian detik. Default: 3."
+ }
+ },
+ "storedLayouts": {
+ "title": "Tata Letak Tersimpan",
+ "desc": "Tata letak kamera dalam grup kamera dapat diseret/diubah ukurannya. Posisi disimpan di penyimpanan lokal browser Anda.",
+ "clearAll": "Hapus Semua Tata Letak"
+ },
+ "cameraGroupStreaming": {
+ "title": "Pengaturan Streaming Grup Kamera",
+ "desc": "Pengaturan streaming untuk setiap grup kamera disimpan di penyimpanan lokal browser Anda.",
+ "clearAll": "Hapus Semua Pengaturan Streaming"
+ },
+ "recordingsViewer": {
+ "title": "Penampil Rekaman",
+ "defaultPlaybackRate": {
+ "label": "Kecepatan Pemutaran Default",
+ "desc": "Kecepatan pemutaran default untuk pemutaran rekaman."
+ }
+ },
+ "calendar": {
+ "title": "Kalender",
+ "firstWeekday": {
+ "label": "Hari Pertama dalam Minggu",
+ "desc": "Hari saat minggu pada kalender tinjauan dimulai.",
+ "sunday": "Minggu",
+ "monday": "Senin"
+ }
+ },
+ "toast": {
+ "success": {
+ "clearStoredLayout": "Tata letak tersimpan untuk {{cameraName}} telah dihapus",
+ "clearStreamingSettings": "Pengaturan streaming untuk semua grup kamera telah dihapus."
+ },
+ "error": {
+ "clearStoredLayoutFailed": "Gagal menghapus tata letak tersimpan: {{errorMessage}}",
+ "clearStreamingSettingsFailed": "Gagal menghapus pengaturan streaming: {{errorMessage}}"
}
}
+ },
+ "configMessages": {
+ "audioTranscription": {
+ "audioDetectionDisabled": "Deteksi audio tidak diaktifkan untuk kamera ini. Transkripsi audio memerlukan deteksi audio yang aktif."
+ },
+ "detect": {
+ "fpsGreaterThanFive": "Mengatur FPS deteksi lebih dari 5 tidak direkomendasikan. Nilai yang lebih tinggi dapat menyebabkan masalah performa dan tidak akan memberikan manfaat apa pun.",
+ "disabled": "Deteksi objek dinonaktifkan. Cuplikan, item tinjauan, dan pengayaan seperti pengenalan wajah, pengenalan pelat nomor, dan AI Generatif tidak akan berfungsi.",
+ "resolutionShouldBeMultipleOfFour": "Untuk hasil terbaik, lebar dan tinggi deteksi sebaiknya merupakan kelipatan 4. Nilai genap lainnya dapat menyebabkan artefak visual atau distorsi ringan pada stream deteksi.",
+ "aspectRatioMismatch": "Lebar dan tinggi yang Anda masukkan tidak cocok dengan rasio aspek resolusi deteksi saat ini. Ini dapat menghasilkan gambar yang meregang atau terdistorsi.",
+ "maxFramesSet": "Menetapkan frame maksimum akan menimpa perilaku default dan menonaktifkan pelacakan objek diam. Hanya sedikit situasi yang memerlukan ini, gunakan dengan hati-hati.",
+ "squareResolution": "Resolusi deteksi berbentuk persegi tidak lazim. Lebar dan tinggi deteksi harus sesuai dengan rasio aspek kamera Anda, misalnya 16:9, bukan dimensi model deteksi objek. Rasio aspek yang tidak sesuai dapat meregangkan gambar dan mengurangi akurasi deteksi.",
+ "resolutionHigh": "Resolusi deteksi ini lebih tinggi daripada yang direkomendasikan dan dapat meningkatkan penggunaan sumber daya tanpa meningkatkan akurasi deteksi. Resolusi deteksi 1080p atau lebih rendah direkomendasikan untuk sebagian besar kamera.",
+ "globalResolutionMultipleCameras": "Resolusi deteksi global ditetapkan saat beberapa kamera dikonfigurasi. Kecuali semua kamera memiliki resolusi dan rasio aspek yang sama, lebar dan tinggi deteksi sebaiknya ditentukan per kamera agar sesuai dengan rasio aspek asli masing-masing kamera."
+ },
+ "faceRecognition": {
+ "globalDisabled": "Pengayaan pengenalan wajah harus diaktifkan agar fitur pengenalan wajah berfungsi pada kamera ini.",
+ "personNotTracked": "Pengenalan wajah memerlukan objek 'person' untuk dilacak. Aktifkan 'person' di Objek untuk kamera ini.",
+ "modelSizeLarge": "Model 'large' memerlukan GPU atau NPU untuk performa yang wajar. Gunakan 'small' pada sistem yang hanya menggunakan CPU."
+ },
+ "lpr": {
+ "globalDisabled": "Pengayaan pengenalan pelat nomor harus diaktifkan agar fitur LPR berfungsi pada kamera ini.",
+ "vehicleNotTracked": "Pengenalan pelat nomor memerlukan 'car' atau 'motorcycle' untuk dilacak. Aktifkan 'car' atau 'motorcycle' di Objek untuk kamera ini.",
+ "modelSizeLarge": "Model 'large' dioptimalkan untuk pelat nomor multi-baris. Model 'small' memberikan performa lebih baik daripada 'large' dan sebaiknya digunakan kecuali wilayah Anda menggunakan format pelat multi-baris."
+ },
+ "review": {
+ "recordDisabled": "Perekaman dinonaktifkan, item tinjauan tidak akan dibuat.",
+ "detectDisabled": "Deteksi objek dinonaktifkan. Item tinjauan memerlukan objek yang terdeteksi untuk mengategorikan alert dan deteksi.",
+ "allNonAlertDetections": "Semua aktivitas non-alert akan disertakan sebagai deteksi.",
+ "genaiImageSourceRecordingsRecordDisabled": "Sumber gambar disetel ke 'rekaman', tetapi perekaman dinonaktifkan. Frigate akan kembali ke gambar pratinjau."
+ },
+ "audio": {
+ "noAudioRole": "Tidak ada stream yang memiliki peran audio yang didefinisikan. Anda harus mengaktifkan peran audio agar deteksi audio berfungsi."
+ },
+ "objects": {
+ "genaiNoDescriptionsProvider": "Anda harus mengonfigurasi penyedia GenAI dengan peran 'deskripsi' agar deskripsi dapat dibuat."
+ },
+ "record": {
+ "noRecordRole": "Tidak ada stream yang memiliki peran record yang didefinisikan. Perekaman tidak akan berfungsi."
+ },
+ "birdseye": {
+ "objectsModeDetectDisabled": "Birdseye disetel ke mode 'objects', tetapi deteksi objek dinonaktifkan untuk kamera ini. Kamera tidak akan muncul di Birdseye."
+ },
+ "snapshots": {
+ "detectDisabled": "Deteksi objek dinonaktifkan. Cuplikan dihasilkan dari objek yang terlacak dan tidak akan dibuat."
+ },
+ "detectors": {
+ "mixedTypes": "Semua detektor harus menggunakan tipe yang sama. Hapus detektor yang ada untuk menggunakan tipe yang berbeda.",
+ "mixedTypesSuggestion": "Semua detektor harus menggunakan tipe yang sama. Hapus detektor yang ada atau pilih {{type}}."
+ },
+ "semanticSearch": {
+ "jinav2SmallModelSize": "Ukuran 'small' dengan model Jina V2 memiliki RAM tinggi dan biaya inferensi. Model 'large' dengan GPU diskrit direkomendasikan."
+ },
+ "onvif": {
+ "autotrackingNoZones": "Pelacakan otomatis memerlukan setidaknya satu zona. Tentukan zona untuk kamera ini di Masker / Zona, lalu tetapkan sebagai zona wajib di bawah."
+ }
+ },
+ "button": {
+ "overriddenGlobal": "Ditimpa (Global)",
+ "overriddenGlobalTooltip": "Kamera ini menimpa pengaturan konfigurasi global di bagian ini",
+ "overriddenGlobalHeading_other": "Kamera ini menimpa {{count}} bidang dari konfigurasi global:",
+ "overriddenGlobalNoDeltas": "Kamera ini menimpa konfigurasi global, tetapi tidak ada nilai bidang yang berbeda.",
+ "overriddenBaseConfig": "Ditimpa (Konfigurasi Dasar)",
+ "overriddenBaseConfigTooltip": "Profil {{profile}} menimpa pengaturan konfigurasi di bagian ini",
+ "overriddenBaseConfigHeading_other": "Profil {{profile}} menimpa {{count}} bidang dari konfigurasi dasar:",
+ "overriddenBaseConfigNoDeltas": "Profil {{profile}} menimpa bagian ini, tetapi tidak ada nilai bidang yang berbeda dari konfigurasi dasar.",
+ "overriddenInCameras": {
+ "label_other": "Ditimpa di {{count}} kamera",
+ "tooltip_other": "{{count}} kamera menimpa nilai di bagian ini. Klik untuk melihat detail.",
+ "heading_other": "Bagian global ini memiliki bidang yang ditimpa di {{count}} kamera.",
+ "othersField_other": "{{count}} lainnya",
+ "profilePrefix": "Profil {{profile}}: {{fields}}"
+ }
+ },
+ "menuDot": {
+ "overrideGlobal": "Bagian ini menimpa konfigurasi global",
+ "overrideProfile": "Bagian ini ditimpa oleh profil {{profile}}",
+ "unsaved": "Bagian ini memiliki perubahan yang belum disimpan"
+ },
+ "saveAllPreview": {
+ "title": "Perubahan yang akan disimpan",
+ "triggerLabel": "Tinjau perubahan yang tertunda",
+ "empty": "Tidak ada perubahan yang tertunda.",
+ "scope": {
+ "label": "Cakupan",
+ "global": "Global",
+ "camera": "Kamera: {{cameraName}}"
+ },
+ "profile": {
+ "label": "Profil"
+ },
+ "field": {
+ "label": "Bidang"
+ },
+ "value": {
+ "label": "Nilai baru",
+ "reset": "Atur ulang"
+ }
+ },
+ "enrichments": {
+ "title": "Pengaturan Pengayaan",
+ "unsavedChanges": "Perubahan pengaturan Pengayaan yang belum disimpan",
+ "birdClassification": {
+ "title": "Klasifikasi Burung",
+ "desc": "Klasifikasi burung mengidentifikasi burung yang dikenal menggunakan model Tensorflow terkuantisasi. Saat burung yang dikenal dikenali, nama umumnya akan ditambahkan sebagai sub_label. Informasi ini disertakan dalam UI, filter, serta notifikasi."
+ },
+ "semanticSearch": {
+ "title": "Pencarian Semantik",
+ "desc": "Pencarian Semantik di Frigate memungkinkan Anda menemukan objek yang terlacak dalam item tinjauan menggunakan gambar itu sendiri, deskripsi teks yang ditentukan pengguna, atau deskripsi yang dibuat secara otomatis.",
+ "reindexNow": {
+ "label": "Indeks Ulang Sekarang",
+ "desc": "Pengindeksan ulang akan membuat ulang embedding untuk semua objek yang terlacak. Proses ini berjalan di latar belakang dan dapat memaksimalkan penggunaan CPU Anda serta memerlukan waktu cukup lama tergantung pada jumlah objek yang terlacak yang Anda miliki.",
+ "confirmTitle": "Konfirmasi Pengindeksan Ulang",
+ "confirmDesc": "Apakah Anda yakin ingin mengindeks ulang semua embedding objek yang terlacak? Proses ini akan berjalan di latar belakang, tetapi dapat memaksimalkan penggunaan CPU Anda dan memerlukan waktu cukup lama. Anda dapat memantau progresnya di halaman Jelajahi.",
+ "confirmButton": "Indeks Ulang",
+ "success": "Pengindeksan ulang berhasil dimulai.",
+ "alreadyInProgress": "Pengindeksan ulang sudah sedang berlangsung.",
+ "error": "Gagal memulai pengindeksan ulang: {{errorMessage}}"
+ },
+ "modelSize": {
+ "label": "Ukuran Model",
+ "desc": "Ukuran model yang digunakan untuk embedding pencarian semantik.",
+ "small": {
+ "title": "kecil",
+ "desc": "Menggunakan small akan memakai versi model terkuantisasi yang menggunakan RAM lebih sedikit dan berjalan lebih cepat pada CPU dengan perbedaan kualitas embedding yang sangat kecil."
+ },
+ "large": {
+ "title": "besar",
+ "desc": "Menggunakan large memakai model Jina penuh dan akan otomatis berjalan pada GPU jika memungkinkan."
+ }
+ }
+ },
+ "faceRecognition": {
+ "title": "Pengenalan Wajah",
+ "desc": "Pengenalan wajah memungkinkan orang diberi nama, dan saat wajah mereka dikenali Frigate akan menetapkan nama orang tersebut sebagai sub_label. Informasi ini disertakan dalam UI, filter, serta notifikasi.",
+ "modelSize": {
+ "label": "Ukuran Model",
+ "desc": "Ukuran model yang digunakan untuk pengenalan wajah.",
+ "small": {
+ "title": "kecil",
+ "desc": "Menggunakan small memakai model embedding wajah FaceNet yang berjalan efisien pada sebagian besar CPU."
+ },
+ "large": {
+ "title": "besar",
+ "desc": "Menggunakan large memakai model embedding wajah ArcFace dan akan otomatis berjalan pada GPU jika memungkinkan."
+ }
+ }
+ },
+ "licensePlateRecognition": {
+ "title": "Pengenalan Pelat Nomor",
+ "desc": "Frigate dapat mengenali pelat nomor pada kendaraan dan secara otomatis menambahkan karakter yang terdeteksi ke field recognized_license_plate atau nama yang dikenal sebagai sub_label pada objek bertipe mobil. Kasus penggunaan yang umum adalah membaca pelat nomor mobil yang masuk ke jalan masuk rumah atau mobil yang melintas di jalan."
+ },
+ "restart_required": "Perlu mulai ulang (pengaturan Pengayaan berubah)",
+ "toast": {
+ "success": "Pengaturan Pengayaan telah disimpan. Mulai ulang Frigate untuk menerapkan perubahan Anda.",
+ "error": "Gagal menyimpan perubahan konfigurasi: {{errorMessage}}"
+ }
+ },
+ "cameraWizard": {
+ "title": "Tambah Kamera",
+ "description": "Ikuti langkah-langkah di bawah ini untuk menambahkan kamera baru ke instalasi Frigate Anda.",
+ "steps": {
+ "nameAndConnection": "Nama & Koneksi",
+ "probeOrSnapshot": "Probe atau Cuplikan",
+ "streamConfiguration": "Konfigurasi Stream",
+ "validationAndTesting": "Validasi & Pengujian"
+ },
+ "save": {
+ "success": "Berhasil menyimpan kamera baru {{cameraName}}.",
+ "failure": "Kesalahan saat menyimpan {{cameraName}}."
+ },
+ "testResultLabels": {
+ "resolution": "Resolusi",
+ "video": "Video",
+ "audio": "Audio",
+ "fps": "FPS"
+ },
+ "commonErrors": {
+ "noUrl": "Harap berikan URL stream yang valid",
+ "testFailed": "Pengujian stream gagal: {{error}}"
+ },
+ "step1": {
+ "description": "Masukkan detail kamera Anda dan pilih untuk memeriksa kamera atau memilih merek secara manual.",
+ "cameraName": "Nama Kamera",
+ "cameraNamePlaceholder": "mis., front_door atau Back Yard Overview",
+ "host": "Alamat Host/IP",
+ "port": "Port",
+ "username": "Nama Pengguna",
+ "usernamePlaceholder": "Opsional",
+ "password": "Kata Sandi",
+ "passwordPlaceholder": "Opsional",
+ "selectTransport": "Pilih protokol transport",
+ "cameraBrand": "Merek Kamera",
+ "selectBrand": "Pilih merek kamera untuk template URL",
+ "customUrl": "URL Stream Kustom",
+ "brandInformation": "Informasi merek",
+ "brandUrlFormat": "Untuk kamera dengan format URL RTSP seperti: {{exampleUrl}}",
+ "customUrlPlaceholder": "rtsp://username:password@host:port/path",
+ "connectionSettings": "Pengaturan Koneksi",
+ "detectionMethod": "Metode Deteksi Stream",
+ "onvifPort": "Port ONVIF",
+ "probeMode": "Periksa kamera",
+ "manualMode": "Pilihan manual",
+ "detectionMethodDescription": "Periksa kamera dengan ONVIF (jika didukung) untuk menemukan URL stream kamera, atau pilih merek kamera secara manual untuk menggunakan URL yang telah ditentukan sebelumnya. Untuk memasukkan URL RTSP kustom, pilih metode manual dan pilih \"Lainnya\".",
+ "onvifPortDescription": "Untuk kamera yang mendukung ONVIF, biasanya ini adalah 80 atau 8080.",
+ "useDigestAuth": "Gunakan autentikasi digest",
+ "useDigestAuthDescription": "Gunakan autentikasi digest HTTP untuk ONVIF. Beberapa kamera mungkin memerlukan nama pengguna/kata sandi ONVIF khusus alih-alih pengguna admin standar.",
+ "errors": {
+ "brandOrCustomUrlRequired": "Pilih merek kamera dengan host/IP atau pilih 'Lainnya' dengan URL kustom",
+ "nameRequired": "Nama kamera wajib diisi",
+ "nameLength": "Nama kamera harus 64 karakter atau kurang",
+ "invalidCharacters": "Nama kamera mengandung karakter yang tidak valid",
+ "nameExists": "Nama kamera sudah ada",
+ "customUrlRtspRequired": "URL kustom harus diawali dengan \"rtsp://\" atau \"rtsps://\". Konfigurasi manual diperlukan untuk stream kamera non-RTSP."
+ }
+ },
+ "step2": {
+ "description": "Periksa kamera untuk stream yang tersedia atau konfigurasi pengaturan manual berdasarkan metode deteksi yang Anda pilih.",
+ "testSuccess": "Pengujian koneksi berhasil!",
+ "testFailed": "Pengujian koneksi gagal. Harap periksa input Anda dan coba lagi.",
+ "testFailedTitle": "Pengujian Gagal",
+ "streamDetails": "Detail Stream",
+ "probing": "Sedang memeriksa kamera...",
+ "retry": "Coba lagi",
+ "testing": {
+ "probingMetadata": "Sedang memeriksa metadata kamera...",
+ "fetchingSnapshot": "Sedang mengambil cuplikan kamera..."
+ },
+ "probeFailed": "Gagal memeriksa kamera: {{error}}",
+ "probingDevice": "Sedang memeriksa perangkat...",
+ "probeSuccessful": "Pemeriksaan berhasil",
+ "probeError": "Kesalahan Pemeriksaan",
+ "probeNoSuccess": "Pemeriksaan tidak berhasil",
+ "deviceInfo": "Informasi Perangkat",
+ "manufacturer": "Produsen",
+ "model": "Model",
+ "firmware": "Firmware",
+ "profiles": "Profil",
+ "ptzSupport": "Dukungan PTZ",
+ "autotrackingSupport": "Dukungan Pelacakan Otomatis",
+ "presets": "Preset",
+ "rtspCandidates": "Kandidat RTSP",
+ "rtspCandidatesDescription": "URL RTSP berikut ditemukan dari pemeriksaan kamera. Uji koneksi untuk melihat metadata stream.",
+ "noRtspCandidates": "Tidak ada URL RTSP yang ditemukan dari kamera. Kredensial Anda mungkin salah, atau kamera mungkin tidak mendukung ONVIF atau metode yang digunakan untuk mengambil URL RTSP. Kembali dan masukkan URL RTSP secara manual.",
+ "candidateStreamTitle": "Kandidat {{number}}",
+ "useCandidate": "Gunakan",
+ "uriCopy": "Salin",
+ "uriCopied": "URI disalin ke clipboard",
+ "testConnection": "Uji Koneksi",
+ "toggleUriView": "Klik untuk mengalihkan tampilan URI penuh",
+ "connected": "Terhubung",
+ "notConnected": "Tidak Terhubung",
+ "errors": {
+ "hostRequired": "Alamat host/IP wajib diisi"
+ }
+ },
+ "step3": {
+ "description": "Konfigurasikan peran stream dan tambahkan stream tambahan untuk kamera Anda.",
+ "streamsTitle": "Stream Kamera",
+ "addStream": "Tambah Stream",
+ "addAnotherStream": "Tambah Stream Lain",
+ "streamTitle": "Stream {{number}}",
+ "streamUrl": "URL Stream",
+ "streamUrlPlaceholder": "rtsp://username:password@host:port/path",
+ "selectStream": "Pilih sebuah stream",
+ "searchCandidates": "Cari kandidat...",
+ "noStreamFound": "Stream tidak ditemukan",
+ "url": "URL",
+ "resolution": "Resolusi",
+ "selectResolution": "Pilih resolusi",
+ "quality": "Kualitas",
+ "selectQuality": "Pilih kualitas",
+ "roles": "Peran",
+ "roleLabels": {
+ "detect": "Deteksi Objek",
+ "record": "Perekaman",
+ "audio": "Audio"
+ },
+ "testStream": "Uji Koneksi",
+ "testSuccess": "Pengujian stream berhasil!",
+ "testFailed": "Pengujian stream gagal",
+ "testFailedTitle": "Pengujian Gagal",
+ "connected": "Terhubung",
+ "notConnected": "Tidak Terhubung",
+ "featuresTitle": "Fitur",
+ "go2rtc": "Kurangi koneksi ke kamera",
+ "detectRoleWarning": "Setidaknya satu stream harus memiliki peran \"detect\" untuk melanjutkan.",
+ "rolesPopover": {
+ "title": "Peran Stream",
+ "detect": "Umpan utama untuk deteksi objek.",
+ "record": "Menyimpan segmen umpan video berdasarkan pengaturan konfigurasi.",
+ "audio": "Umpan untuk deteksi berbasis audio."
+ },
+ "featuresPopover": {
+ "title": "Fitur Stream",
+ "description": "Gunakan restreaming go2rtc untuk mengurangi koneksi ke kamera Anda."
+ }
+ },
+ "step4": {
+ "description": "Validasi dan analisis akhir sebelum menyimpan kamera baru Anda. Hubungkan setiap stream sebelum menyimpan.",
+ "validationTitle": "Validasi Stream",
+ "connectAllStreams": "Hubungkan Semua Stream",
+ "reconnectionSuccess": "Penyambungan ulang berhasil.",
+ "reconnectionPartial": "Beberapa stream gagal disambungkan ulang.",
+ "streamUnavailable": "Pratinjau stream tidak tersedia",
+ "reload": "Muat ulang",
+ "connecting": "Menghubungkan...",
+ "streamTitle": "Stream {{number}}",
+ "valid": "Valid",
+ "failed": "Gagal",
+ "notTested": "Belum diuji",
+ "connectStream": "Hubungkan",
+ "connectingStream": "Sedang menghubungkan",
+ "disconnectStream": "Putuskan",
+ "estimatedBandwidth": "Perkiraan Bandwidth",
+ "roles": "Peran",
+ "ffmpegModule": "Gunakan mode kompatibilitas stream",
+ "ffmpegModuleDescription": "Jika stream tidak dimuat setelah beberapa kali percobaan, coba aktifkan ini. Saat diaktifkan, Frigate akan menggunakan modul ffmpeg dengan go2rtc. Ini dapat memberikan kompatibilitas yang lebih baik dengan beberapa stream kamera.",
+ "none": "Tidak ada",
+ "error": "Kesalahan",
+ "streamValidated": "Stream {{number}} berhasil divalidasi",
+ "streamValidationFailed": "Validasi stream {{number}} gagal",
+ "saveAndApply": "Simpan Kamera Baru",
+ "saveError": "Konfigurasi tidak valid. Harap periksa pengaturan Anda.",
+ "issues": {
+ "title": "Validasi Stream",
+ "videoCodecGood": "Codec video adalah {{codec}}.",
+ "audioCodecGood": "Codec audio adalah {{codec}}.",
+ "resolutionHigh": "Resolusi {{resolution}} dapat menyebabkan peningkatan penggunaan sumber daya.",
+ "resolutionLow": "Resolusi {{resolution}} mungkin terlalu rendah untuk deteksi objek kecil yang andal.",
+ "resolutionUnknown": "Resolusi stream ini tidak dapat diperiksa. Anda harus menetapkan resolusi detect secara manual di Pengaturan atau konfigurasi Anda.",
+ "noAudioWarning": "Tidak ada audio yang terdeteksi untuk stream ini, rekaman tidak akan memiliki audio.",
+ "audioCodecRecordError": "Codec audio AAC diperlukan untuk mendukung audio dalam rekaman.",
+ "audioCodecRequired": "Stream audio diperlukan untuk mendukung deteksi audio.",
+ "restreamingWarning": "Mengurangi koneksi ke kamera untuk stream record dapat sedikit meningkatkan penggunaan CPU.",
+ "brands": {
+ "reolink-rtsp": "RTSP Reolink tidak direkomendasikan. Aktifkan HTTP di pengaturan firmware kamera dan mulai ulang wizard.",
+ "reolink-http": "Stream HTTP Reolink sebaiknya menggunakan FFmpeg untuk kompatibilitas yang lebih baik. Aktifkan 'Gunakan mode kompatibilitas stream' untuk stream ini."
+ },
+ "dahua": {
+ "substreamWarning": "Substream 1 terkunci pada resolusi rendah. Banyak kamera Dahua / Amcrest / EmpireTech mendukung substream tambahan yang perlu diaktifkan di pengaturan kamera. Disarankan untuk memeriksa dan menggunakan stream tersebut jika tersedia."
+ },
+ "hikvision": {
+ "substreamWarning": "Substream 1 terkunci pada resolusi rendah. Banyak kamera Hikvision mendukung substream tambahan yang perlu diaktifkan di pengaturan kamera. Disarankan untuk memeriksa dan menggunakan stream tersebut jika tersedia."
+ }
+ }
+ }
+ },
+ "cameraManagement": {
+ "title": "Kelola Kamera",
+ "description": "Tambahkan, edit, dan hapus kamera, kendalikan status setiap kamera, serta atur penimpaan per profil dan jenis kamera. Untuk mengonfigurasi stream, deteksi, gerakan, dan pengaturan khusus kamera lainnya, pilih bagian terkait di bawah Konfigurasi Kamera.",
+ "addCamera": "Tambah Kamera Baru",
+ "deleteCamera": "Hapus Kamera",
+ "deleteCameraDialog": {
+ "title": "Hapus Kamera",
+ "description": "Menghapus kamera akan menghapus secara permanen semua rekaman, objek terlacak, dan konfigurasi untuk kamera tersebut. Semua stream go2rtc yang terkait dengan kamera ini mungkin masih perlu dihapus secara manual.",
+ "selectPlaceholder": "Pilih kamera...",
+ "confirmTitle": "Apakah Anda yakin?",
+ "confirmWarning": "Menghapus {{cameraName}} tidak dapat dibatalkan.",
+ "deleteExports": "Juga hapus ekspor untuk kamera ini",
+ "confirmButton": "Hapus Permanen",
+ "success": "Kamera {{cameraName}} berhasil dihapus",
+ "error": "Gagal menghapus kamera {{cameraName}}"
+ },
+ "editCamera": "Edit Kamera:",
+ "selectCamera": "Pilih Kamera",
+ "backToSettings": "Kembali ke Pengaturan Kamera",
+ "streams": {
+ "title": "Status dan Detail Kamera",
+ "enableLabel": "Kamera yang diaktifkan",
+ "enableDesc": "Nonaktifkan sementara kamera yang aktif hingga Frigate dimulai ulang. Menonaktifkan kamera sepenuhnya akan menghentikan seluruh pemrosesan stream kamera ini oleh Frigate. Deteksi, perekaman, dan debugging tidak akan tersedia. Catatan: Ini tidak menonaktifkan restream go2rtc. Seret pegangan untuk mengubah urutan kamera sebagaimana ditampilkan di UI. Urutan kamera yang aktif akan tercermin di seluruh UI termasuk dasbor Langsung dan dropdown pemilihan kamera.",
+ "disableLabel": "Kamera yang dinonaktifkan",
+ "disableDesc": "Aktifkan kamera yang saat ini tidak terlihat di UI dan dinonaktifkan dalam konfigurasi. Mulai ulang Frigate diperlukan setelah mengaktifkan.",
+ "enableSuccess": "{{cameraName}} diaktifkan. Restart Frigate untuk menerapkan.",
+ "reorderHandle": "Seret untuk mengubah urutan",
+ "saving": "Menyimpan…",
+ "saved": "Tersimpan",
+ "friendlyName": {
+ "edit": "Edit nama tampilan kamera",
+ "title": "Edit Nama Tampilan",
+ "description": "Tetapkan nama ramah yang ditampilkan untuk kamera ini di seluruh UI Frigate. Biarkan kosong untuk menggunakan ID kamera.",
+ "rename": "Ubah Nama"
+ },
+ "label": "Status kamera",
+ "description": "Atur status operasi untuk setiap kamera.Nyala : stream diproses secara normal.Mati : mem jeda pemrosesan untuk sementara. Tidak tetap setelah Frigate dimulai ulang.Nonaktif : menghentikan pemrosesan dan menyimpan perubahan ke konfigurasi Anda. Restart diperlukan untuk mengaktifkan kembali kamera yang dinonaktifkan.Catatan: Menonaktifkan tidak memengaruhi restream go2rtc. Seret pegangan untuk mengubah urutan kamera aktif sebagaimana tampil di seluruh UI, termasuk dasbor Live dan menu dropdown pemilihan kamera.",
+ "disabledSubheading": "Dinonaktifkan dalam konfigurasi",
+ "status": {
+ "on": "Nyala",
+ "off": "Mati",
+ "disabled": "Nonaktif"
+ },
+ "disableSuccess": "{{cameraName}} dinonaktifkan dan disimpan ke konfigurasi.",
+ "details": {
+ "edit": "Edit detail kamera",
+ "title": "Edit Detail Kamera",
+ "description": "Perbarui nama tampilan, URL eksternal, dan visibilitas yang digunakan untuk kamera ini di seluruh UI Frigate.",
+ "friendlyNameLabel": "Nama Tampilan",
+ "friendlyNameHelp": "Nama ramah yang ditampilkan untuk kamera ini di seluruh UI Frigate. Biarkan kosong untuk menggunakan ID kamera.",
+ "webuiUrlLabel": "URL Web UI Kamera",
+ "webuiUrlHelp": "URL untuk membuka web UI kamera langsung dari tampilan Debug. Biarkan kosong untuk menonaktifkan tautan.",
+ "webuiUrlInvalid": "Harus berupa URL yang valid (misalnya, https://example.com).",
+ "dashboardLabel": "Tampilkan di dasbor Live",
+ "dashboardHelp": "Tampilkan kamera ini di dasbor Live.",
+ "reviewLabel": "Tampilkan di Review",
+ "reviewHelp": "Tampilkan kamera ini di Review, termasuk filter kamera, review gerakan, dan tampilan riwayat."
+ }
+ },
+ "cameraConfig": {
+ "add": "Tambah Kamera",
+ "edit": "Edit Kamera",
+ "description": "Konfigurasikan pengaturan kamera termasuk input stream dan perannya.",
+ "name": "Nama Kamera",
+ "nameRequired": "Nama kamera wajib diisi",
+ "nameLength": "Nama kamera harus kurang dari 64 karakter.",
+ "namePlaceholder": "mis., front_door atau Back Yard Overview",
+ "enabled": "Diaktifkan",
+ "ffmpeg": {
+ "inputs": "Input Stream",
+ "path": "Path Stream",
+ "pathRequired": "Path stream wajib diisi",
+ "pathPlaceholder": "rtsp://...",
+ "roles": "Peran",
+ "rolesRequired": "Setidaknya satu peran wajib diisi",
+ "rolesUnique": "Setiap peran (audio, detect, record) hanya dapat ditetapkan ke satu stream",
+ "addInput": "Tambah Input Stream",
+ "removeInput": "Hapus Input Stream",
+ "inputsRequired": "Setidaknya satu input stream wajib diisi"
+ },
+ "go2rtcStreams": "Stream go2rtc",
+ "streamUrls": "URL Stream",
+ "addUrl": "Tambah URL",
+ "addGo2rtcStream": "Tambah Stream go2rtc",
+ "toast": {
+ "success": "Kamera {{cameraName}} berhasil disimpan"
+ }
+ },
+ "profiles": {
+ "title": "Penimpaan Kamera per Profil",
+ "selectLabel": "Pilih profil",
+ "description": "Atur kamera mana yang dinyalakan atau dimatikan saat suatu profil diaktifkan. Kamera yang disetel ke \"Warisi\" akan mempertahankan status default-nya.",
+ "inherit": "Warisi",
+ "enabled": "Diaktifkan",
+ "disabled": "Dinonaktifkan",
+ "on": "Nyala",
+ "off": "Mati"
+ },
+ "cameraType": {
+ "title": "Tipe Kamera",
+ "label": "Tipe kamera",
+ "description": "Tetapkan tipe untuk setiap kamera. Kamera LPR khusus adalah kamera satu fungsi dengan zoom optik kuat untuk menangkap pelat nomor pada kendaraan yang jauh. Sebagian besar kamera sebaiknya menggunakan tipe kamera normal kecuali kamera tersebut memang khusus untuk LPR dan memiliki tampilan yang sangat terfokus pada pelat nomor.",
+ "normal": "Normal",
+ "dedicatedLpr": "LPR Khusus",
+ "saveSuccess": "Tipe kamera untuk {{cameraName}} telah diperbarui. Mulai ulang Frigate untuk menerapkan perubahan."
+ },
+ "clone": {
+ "sectionTitle": "Kloning pengaturan",
+ "sectionDescription": "Salin konfigurasi dari satu kamera ke kamera lain atau ke kamera baru.",
+ "button": "Kloning pengaturan",
+ "title": "Kloning pengaturan kamera",
+ "description": "Salin konfigurasi sebuah kamera ke satu atau lebih kamera lain atau ke kamera baru. Identitas (nama, nama ramah, URL web UI, urutan tampilan) tidak pernah disalin.",
+ "source": {
+ "label": "Kamera sumber",
+ "placeholder": "Pilih kamera sumber",
+ "required": "Pilih kamera sumber"
+ },
+ "target": {
+ "legend": "Target",
+ "newRadio": "Kamera baru",
+ "newNameLabel": "Nama kamera",
+ "newNamePlaceholder": "misalnya, pintu_belakang atau Pintu Belakang",
+ "newNameRequired": "Nama kamera wajib diisi",
+ "newNameInvalid": "Nama kamera tidak valid",
+ "newNameCollision": "Kamera dengan nama ini sudah ada",
+ "newStreamsForced": "Stream selalu disalin untuk kamera baru.",
+ "existingCamerasRadio": "Kamera yang sudah ada",
+ "allCameras": "Semua kamera",
+ "existingPlaceholder": "Pilih setidaknya satu kamera",
+ "existingDisabled": "Tidak ada kamera lain untuk disalin ke"
+ },
+ "categories": {
+ "legend": "Pengaturan untuk dikloning",
+ "description": "Pilih pengaturan mana yang akan disalin dari kamera sumber.",
+ "selectAll": "Pilih semua",
+ "selectNone": "Jangan pilih apa pun",
+ "resetDefaults": "Reset ke default",
+ "general": "Umum",
+ "spatial": "Pengaturan spasial",
+ "streams": "Stream",
+ "spatialWarningTitle": "Ketidakcocokan resolusi",
+ "spatialWarning": "Resolusi deteksi kamera sumber {{srcCamera}} ({{srcWidth}}×{{srcHeight}}) berbeda dengan: {{cameras}}. Poligon mungkin tidak sejajar pada kamera-kamera tersebut. Default untuk ini adalah nonaktif; aktifkan untuk menyalin apa adanya.",
+ "restartHint": "Perlu restart",
+ "items": {
+ "record": "Perekaman",
+ "snapshots": "Snapshot",
+ "review": "Review",
+ "motion": "Deteksi gerakan",
+ "objects": "Objek",
+ "audio": "Deteksi audio",
+ "audio_transcription": "Transkripsi audio",
+ "notifications": "Notifikasi",
+ "birdseye": "Birdseye",
+ "mqtt": "MQTT",
+ "timestamp_style": "Gaya stempel waktu",
+ "onvif": "ONVIF",
+ "lpr": "Pengenalan pelat nomor",
+ "face_recognition": "Pengenalan wajah",
+ "semantic_search": "Pencarian semantik",
+ "genai": "AI generatif",
+ "type": "Jenis kamera (normal / LPR khusus)",
+ "profiles": "Profil",
+ "detect": "Dimensi deteksi",
+ "zones": "Zona",
+ "motion_mask": "Masker gerakan",
+ "object_masks": "Masker objek",
+ "ffmpeg_live": "URL dan peran stream"
+ }
+ },
+ "footer": {
+ "changeCount_other": "{{count}} perubahan akan diterapkan",
+ "restartNeeded": "Restart akan diperlukan untuk beberapa perubahan.",
+ "liveOnly": "Semua perubahan akan diterapkan langsung tanpa restart.",
+ "submit": "Kloning",
+ "submitting": "Mengkloning…"
+ },
+ "toast": {
+ "success": "Pengaturan disalin ke {{cameraName}}",
+ "successWithRestart": "Pengaturan disalin ke {{cameraName}}. Restart Frigate untuk menerapkan semua perubahan.",
+ "successMulti_other": "Pengaturan disalin ke {{count}} kamera",
+ "successMultiWithRestart_other": "Pengaturan disalin ke {{count}} kamera. Restart Frigate untuk menerapkan semua perubahan.",
+ "partialFailure": "{{successCount}} bagian diterapkan; '{{failedSection}}' gagal: {{errorMessage}}",
+ "partialFailureMulti": "Disalin ke {{successCount}} kamera; gagal untuk {{failed}}: {{errorMessage}}",
+ "newCameraPartialFailure": "Kamera {{cameraName}} berhasil dibuat, tetapi beberapa pengaturan gagal disalin: {{errorMessage}}",
+ "sourceMissing": "Kamera sumber sudah tidak ada",
+ "submitError": "Gagal mengkloning kamera: {{errorMessage}}"
+ }
+ }
+ },
+ "cameraReview": {
+ "title": "Pengaturan Tinjauan Kamera",
+ "object_descriptions": {
+ "title": "Deskripsi Objek AI Generatif",
+ "desc": "Aktifkan/nonaktifkan sementara deskripsi objek AI Generatif untuk kamera ini hingga Frigate dimulai ulang. Saat dinonaktifkan, deskripsi yang dihasilkan AI tidak akan diminta untuk objek terlacak pada kamera ini."
+ },
+ "review_descriptions": {
+ "title": "Deskripsi Tinjauan AI Generatif",
+ "desc": "Aktifkan/nonaktifkan sementara deskripsi tinjauan AI Generatif untuk kamera ini hingga Frigate dimulai ulang. Saat dinonaktifkan, deskripsi yang dihasilkan AI tidak akan diminta untuk item tinjauan pada kamera ini."
+ },
+ "review": {
+ "title": "Tinjauan",
+ "desc": "Aktifkan/nonaktifkan sementara alert dan deteksi untuk kamera ini hingga Frigate dimulai ulang. Saat dinonaktifkan, tidak ada item tinjauan baru yang akan dibuat. ",
+ "alerts": "Alert ",
+ "detections": "Deteksi "
+ },
+ "reviewClassification": {
+ "title": "Klasifikasi Tinjauan",
+ "desc": "Frigate mengategorikan item tinjauan sebagai Alert dan Deteksi. Secara default, semua objek person dan car dianggap sebagai Alert. Anda dapat menyempurnakan kategorisasi item tinjauan Anda dengan mengonfigurasi zona yang diwajibkan untuk item tersebut.",
+ "noDefinedZones": "Tidak ada zona yang didefinisikan untuk kamera ini.",
+ "objectAlertsTips": "Semua objek {{alertsLabels}} pada {{cameraName}} akan ditampilkan sebagai Alert.",
+ "zoneObjectAlertsTips": "Semua objek {{alertsLabels}} yang terdeteksi di {{zone}} pada {{cameraName}} akan ditampilkan sebagai Alert.",
+ "objectDetectionsTips": "Semua objek {{detectionsLabels}} yang tidak dikategorikan pada {{cameraName}} akan ditampilkan sebagai Deteksi, terlepas dari zona mana mereka berada.",
+ "zoneObjectDetectionsTips": {
+ "text": "Semua objek {{detectionsLabels}} yang tidak dikategorikan di {{zone}} pada {{cameraName}} akan ditampilkan sebagai Deteksi.",
+ "notSelectDetections": "Semua objek {{detectionsLabels}} yang terdeteksi di {{zone}} pada {{cameraName}} dan tidak dikategorikan sebagai Alert akan ditampilkan sebagai Deteksi, terlepas dari zona mana mereka berada.",
+ "regardlessOfZoneObjectDetectionsTips": "Semua objek {{detectionsLabels}} yang tidak dikategorikan pada {{cameraName}} akan ditampilkan sebagai Deteksi, terlepas dari zona mana mereka berada."
+ },
+ "unsavedChanges": "Pengaturan Klasifikasi Tinjauan yang belum disimpan untuk {{camera}}",
+ "selectAlertsZones": "Pilih zona untuk Alert",
+ "selectDetectionsZones": "Pilih zona untuk Deteksi",
+ "limitDetections": "Batasi deteksi ke zona tertentu",
+ "toast": {
+ "success": "Konfigurasi Klasifikasi Tinjauan telah disimpan. Mulai ulang Frigate untuk menerapkan perubahan."
+ }
+ }
+ },
+ "masksAndZones": {
+ "filter": {
+ "all": "Semua Masker dan Zona"
+ },
+ "restart_required": "Perlu mulai ulang (masker/zona berubah)",
+ "disabledInConfig": "Item dinonaktifkan dalam file konfigurasi",
+ "addDisabledProfile": "Tambahkan ke konfigurasi dasar terlebih dahulu, lalu timpa di profil",
+ "profileBase": "(dasar)",
+ "profileOverride": "(timpa)",
+ "toast": {
+ "success": {
+ "copyCoordinates": "Koordinat untuk {{polyName}} telah disalin ke clipboard."
+ },
+ "error": {
+ "copyCoordinatesFailed": "Tidak dapat menyalin koordinat ke clipboard."
+ }
+ },
+ "motionMaskLabel": "Masker Gerakan {{number}}",
+ "objectMaskLabel": "Masker Objek {{number}}",
+ "form": {
+ "id": {
+ "error": {
+ "mustNotBeEmpty": "ID tidak boleh kosong.",
+ "alreadyExists": "Masker dengan ID ini sudah ada untuk kamera ini."
+ }
+ },
+ "name": {
+ "error": {
+ "mustNotBeEmpty": "Nama tidak boleh kosong."
+ }
+ },
+ "zoneName": {
+ "error": {
+ "mustBeAtLeastTwoCharacters": "Nama zona harus minimal 2 karakter.",
+ "mustNotBeSameWithCamera": "Nama zona tidak boleh sama dengan nama kamera.",
+ "alreadyExists": "Zona dengan nama ini sudah ada untuk kamera ini.",
+ "mustNotContainPeriod": "Nama zona tidak boleh mengandung titik.",
+ "hasIllegalCharacter": "Nama zona mengandung karakter yang tidak valid.",
+ "mustHaveAtLeastOneLetter": "Nama zona harus memiliki setidaknya satu huruf."
+ }
+ },
+ "distance": {
+ "error": {
+ "text": "Jarak harus lebih besar dari atau sama dengan 0.1.",
+ "mustBeFilled": "Semua field jarak harus diisi untuk menggunakan estimasi kecepatan."
+ }
+ },
+ "inertia": {
+ "error": {
+ "mustBeAboveZero": "Inersia harus lebih besar dari 0."
+ }
+ },
+ "loiteringTime": {
+ "error": {
+ "mustBeGreaterOrEqualZero": "Waktu loitering harus lebih besar dari atau sama dengan 0."
+ }
+ },
+ "speed": {
+ "error": {
+ "mustBeGreaterOrEqualTo": "Ambang kecepatan harus lebih besar dari atau sama dengan 0.1."
+ }
+ },
+ "polygonDrawing": {
+ "type": {
+ "zone": "zona",
+ "motion_mask": "masker gerakan",
+ "object_mask": "masker objek"
+ },
+ "removeLastPoint": "Hapus titik terakhir",
+ "reset": {
+ "label": "Hapus semua titik"
+ },
+ "snapPoints": {
+ "true": "Kaitkan titik",
+ "false": "Jangan kaitkan titik"
+ },
+ "delete": {
+ "title": "Konfirmasi Hapus",
+ "desc": "Apakah Anda yakin ingin menghapus {{type}} {{name}} ?",
+ "success": "{{name}} telah dihapus."
+ },
+ "revertOverride": {
+ "title": "Kembalikan ke Konfigurasi Dasar",
+ "desc": "Ini akan menghapus penimpaan profil untuk {{type}} {{name}} dan mengembalikannya ke konfigurasi dasar."
+ },
+ "error": {
+ "mustBeFinished": "Gambar poligon harus diselesaikan sebelum menyimpan."
+ }
+ }
+ },
+ "zones": {
+ "label": "Zona",
+ "documentTitle": "Edit Zona - Frigate",
+ "desc": {
+ "title": "Zona memungkinkan Anda menentukan area tertentu pada frame sehingga Anda dapat menentukan apakah suatu objek berada di dalam area tertentu atau tidak.",
+ "documentation": "Dokumentasi"
+ },
+ "add": "Tambah Zona",
+ "edit": "Edit Zona",
+ "point_other": "{{count}} titik",
+ "clickDrawPolygon": "Klik untuk menggambar poligon pada gambar.",
+ "name": {
+ "title": "Nama",
+ "inputPlaceHolder": "Masukkan nama…",
+ "tips": "Nama harus minimal 2 karakter, harus memiliki setidaknya satu huruf, dan tidak boleh sama dengan nama kamera atau zona lain pada kamera ini."
+ },
+ "enabled": {
+ "title": "Diaktifkan",
+ "description": "Menentukan apakah zona ini aktif dan diaktifkan dalam file konfigurasi. Jika dinonaktifkan, zona ini tidak dapat diaktifkan melalui MQTT. Zona yang dinonaktifkan diabaikan saat runtime."
+ },
+ "inertia": {
+ "title": "Inersia",
+ "desc": "Menentukan berapa banyak frame suatu objek harus berada di dalam zona sebelum dianggap berada di zona tersebut. Default: 3 "
+ },
+ "loiteringTime": {
+ "title": "Waktu Loitering",
+ "desc": "Menetapkan jumlah waktu minimum dalam detik yang harus dilalui objek di dalam zona agar zona aktif. Default: 0 "
+ },
+ "objects": {
+ "title": "Objek",
+ "desc": "Daftar objek yang berlaku untuk zona ini."
+ },
+ "allObjects": "Semua Objek",
+ "speedEstimation": {
+ "title": "Estimasi Kecepatan",
+ "desc": "Aktifkan estimasi kecepatan untuk objek di zona ini. Zona harus memiliki tepat 4 titik.",
+ "lineADistance": "Jarak garis A ({{unit}})",
+ "lineBDistance": "Jarak garis B ({{unit}})",
+ "lineCDistance": "Jarak garis C ({{unit}})",
+ "lineDDistance": "Jarak garis D ({{unit}})"
+ },
+ "speedThreshold": {
+ "title": "Ambang Kecepatan ({{unit}})",
+ "desc": "Menentukan kecepatan minimum agar objek dianggap berada di zona ini.",
+ "toast": {
+ "error": {
+ "pointLengthError": "Estimasi kecepatan telah dinonaktifkan untuk zona ini. Zona dengan estimasi kecepatan harus memiliki tepat 4 titik.",
+ "loiteringTimeError": "Zona dengan waktu loitering lebih besar dari 0 sebaiknya tidak digunakan dengan estimasi kecepatan."
+ }
+ }
+ },
+ "toast": {
+ "success": "Zona ({{zoneName}}) telah disimpan."
+ }
+ },
+ "motionMasks": {
+ "label": "Masker Gerakan",
+ "documentTitle": "Edit Masker Gerakan - Frigate",
+ "desc": {
+ "title": "Masker gerakan digunakan untuk mencegah jenis gerakan yang tidak diinginkan memicu deteksi. Masking yang berlebihan akan membuat objek lebih sulit dilacak.",
+ "documentation": "Dokumentasi"
+ },
+ "add": "Masker Gerakan Baru",
+ "edit": "Edit Masker Gerakan",
+ "defaultName": "Masker Gerakan {{number}}",
+ "context": {
+ "title": "Masker gerakan digunakan untuk mencegah jenis gerakan yang tidak diinginkan memicu deteksi (contoh: ranting pohon, stempel waktu kamera). Masker gerakan harus digunakan dengan sangat hemat , masking yang berlebihan akan membuat objek lebih sulit dilacak."
+ },
+ "point_other": "{{count}} titik",
+ "clickDrawPolygon": "Klik untuk menggambar poligon pada gambar.",
+ "name": {
+ "title": "Nama",
+ "description": "Nama ramah opsional untuk masker gerakan ini.",
+ "placeholder": "Masukkan nama..."
+ },
+ "polygonAreaTooLarge": {
+ "title": "Masker gerakan menutupi {{polygonArea}}% dari frame kamera. Masker gerakan besar tidak direkomendasikan.",
+ "tips": "Masker gerakan tidak mencegah objek terdeteksi. Anda sebaiknya menggunakan zona wajib sebagai gantinya."
+ },
+ "toast": {
+ "success": {
+ "title": "{{polygonName}} telah disimpan.",
+ "noName": "Masker Gerakan telah disimpan."
+ }
+ }
+ },
+ "objectMasks": {
+ "label": "Masker Objek",
+ "documentTitle": "Edit Masker Objek - Frigate",
+ "desc": {
+ "title": "Masker filter objek digunakan untuk menyaring positif palsu untuk tipe objek tertentu berdasarkan lokasi.",
+ "documentation": "Dokumentasi"
+ },
+ "add": "Tambah Masker Objek",
+ "edit": "Edit Masker Objek",
+ "context": "Masker filter objek digunakan untuk menyaring positif palsu untuk tipe objek tertentu berdasarkan lokasi.",
+ "point_other": "{{count}} titik",
+ "clickDrawPolygon": "Klik untuk menggambar poligon pada gambar.",
+ "name": {
+ "title": "Nama",
+ "description": "Nama ramah opsional untuk masker objek ini.",
+ "placeholder": "Masukkan nama..."
+ },
+ "objects": {
+ "title": "Objek",
+ "desc": "Tipe objek yang berlaku untuk masker objek ini.",
+ "allObjectTypes": "Semua tipe objek"
+ },
+ "toast": {
+ "success": {
+ "title": "{{polygonName}} telah disimpan.",
+ "noName": "Masker Objek telah disimpan."
+ }
+ }
+ },
+ "masks": {
+ "enabled": {
+ "title": "Diaktifkan",
+ "description": "Menentukan apakah masker ini diaktifkan dalam file konfigurasi. Jika dinonaktifkan, masker ini tidak dapat diaktifkan melalui MQTT. Masker yang dinonaktifkan diabaikan saat runtime."
+ }
+ }
+ },
+ "motionDetectionTuner": {
+ "title": "Penyetel Deteksi Gerakan",
+ "unsavedChanges": "Perubahan Penyetel Gerakan yang belum disimpan ({{camera}})",
+ "desc": {
+ "title": "Frigate menggunakan deteksi gerakan sebagai pemeriksaan awal untuk melihat apakah ada sesuatu yang terjadi dalam frame yang layak diperiksa dengan deteksi objek.",
+ "documentation": "Baca Panduan Penyetelan Gerakan"
+ },
+ "Threshold": {
+ "title": "Ambang",
+ "desc": "Nilai ambang menentukan seberapa besar perubahan luminansi piksel yang diperlukan agar dianggap sebagai gerakan. Default: 30 "
+ },
+ "contourArea": {
+ "title": "Area Kontur",
+ "desc": "Nilai area kontur digunakan untuk menentukan kelompok piksel yang berubah mana yang memenuhi syarat sebagai gerakan. Default: 10 "
+ },
+ "improveContrast": {
+ "title": "Tingkatkan Kontras",
+ "desc": "Tingkatkan kontras untuk adegan yang lebih gelap. Default: ON "
+ },
+ "toast": {
+ "success": "Pengaturan gerakan telah disimpan."
+ }
+ },
+ "debug": {
+ "title": "Debug",
+ "detectorDesc": "Frigate menggunakan detektor Anda ({{detectors}}) untuk mendeteksi objek di stream video kamera Anda.",
+ "desc": "Tampilan debug menunjukkan tampilan real-time objek yang dilacak dan statistiknya. Daftar objek menampilkan ringkasan objek yang terdeteksi dengan sedikit jeda waktu.",
+ "openCameraWebUI": "Buka UI Web milik {{camera}}",
+ "debugging": "Debugging",
+ "objectList": "Daftar Objek",
+ "noObjects": "Tidak ada objek",
+ "audio": {
+ "title": "Audio",
+ "noAudioDetections": "Tidak ada deteksi audio",
+ "score": "skor",
+ "currentRMS": "RMS Saat Ini",
+ "currentdbFS": "dbFS Saat Ini"
+ },
+ "boundingBoxes": {
+ "title": "Kotak pembatas",
+ "desc": "Tampilkan kotak pembatas di sekitar objek yang dilacak",
+ "colors": {
+ "label": "Warna Kotak Pembatas Objek",
+ "info": "Saat startup, warna berbeda akan ditetapkan untuk setiap label objek Garis tipis biru tua menunjukkan bahwa objek tersebut tidak terdeteksi pada titik waktu saat ini Garis tipis abu-abu menunjukkan bahwa objek tersebut terdeteksi sebagai diam Garis tebal menunjukkan bahwa objek tersebut menjadi subjek pelacakan otomatis (jika diaktifkan) "
+ }
+ },
+ "timestamp": {
+ "title": "Stempel waktu",
+ "desc": "Hamparkan stempel waktu pada gambar"
+ },
+ "zones": {
+ "title": "Zona",
+ "desc": "Tampilkan garis luar dari zona yang telah didefinisikan"
+ },
+ "mask": {
+ "title": "Masker gerakan",
+ "desc": "Tampilkan poligon masker gerakan"
+ },
+ "motion": {
+ "title": "Kotak gerakan",
+ "desc": "Tampilkan kotak di sekitar area tempat gerakan terdeteksi",
+ "tips": "Kotak Gerakan
Kotak merah akan dihamparkan pada area frame tempat gerakan saat ini terdeteksi
"
+ },
+ "regions": {
+ "title": "Wilayah",
+ "desc": "Tampilkan kotak wilayah minat yang dikirim ke detektor objek",
+ "tips": "Kotak Wilayah
Kotak hijau terang akan dihamparkan pada area minat di frame yang sedang dikirim ke detektor objek.
"
+ },
+ "paths": {
+ "title": "Jalur",
+ "desc": "Tampilkan titik-titik penting dari jalur objek yang dilacak",
+ "tips": "Jalur
Garis dan lingkaran akan menunjukkan titik-titik penting yang telah dilalui objek yang dilacak selama siklus hidupnya.
"
+ },
+ "objectShapeFilterDrawing": {
+ "title": "Gambar Filter Bentuk Objek",
+ "desc": "Gambar persegi panjang pada gambar untuk melihat detail area dan rasio",
+ "tips": "Aktifkan opsi ini untuk menggambar persegi panjang pada gambar kamera guna menampilkan area dan rasionya. Nilai-nilai ini kemudian dapat digunakan untuk menetapkan parameter filter bentuk objek dalam konfigurasi Anda.",
+ "score": "Skor",
+ "ratio": "Rasio",
+ "area": "Area"
+ }
+ },
+ "timestampPosition": {
+ "tl": "Kiri atas",
+ "tr": "Kanan atas",
+ "bl": "Kiri bawah",
+ "br": "Kanan bawah"
+ },
+ "users": {
+ "title": "Pengguna",
+ "management": {
+ "title": "Manajemen Pengguna",
+ "desc": "Kelola akun pengguna untuk instance Frigate ini."
+ },
+ "addUser": "Tambah Pengguna",
+ "updatePassword": "Atur Ulang Kata Sandi",
+ "toast": {
+ "success": {
+ "createUser": "Pengguna {{user}} berhasil dibuat",
+ "deleteUser": "Pengguna {{user}} berhasil dihapus",
+ "updatePassword": "Kata sandi berhasil diperbarui.",
+ "roleUpdated": "Peran untuk {{user}} diperbarui"
+ },
+ "error": {
+ "setPasswordFailed": "Gagal menyimpan kata sandi: {{errorMessage}}",
+ "createUserFailed": "Gagal membuat pengguna: {{errorMessage}}",
+ "deleteUserFailed": "Gagal menghapus pengguna: {{errorMessage}}",
+ "roleUpdateFailed": "Gagal memperbarui peran: {{errorMessage}}"
+ }
+ },
+ "table": {
+ "username": "Nama pengguna",
+ "actions": "Tindakan",
+ "role": "Peran",
+ "noUsers": "Tidak ada pengguna ditemukan.",
+ "changeRole": "Ubah peran pengguna",
+ "password": "Atur Ulang Kata Sandi",
+ "deleteUser": "Hapus pengguna"
+ },
+ "dialog": {
+ "form": {
+ "user": {
+ "title": "Nama pengguna",
+ "desc": "Hanya huruf, angka, titik, dan garis bawah yang diizinkan.",
+ "placeholder": "Masukkan nama pengguna"
+ },
+ "password": {
+ "title": "Kata sandi",
+ "placeholder": "Masukkan kata sandi",
+ "show": "Tampilkan kata sandi",
+ "hide": "Sembunyikan kata sandi",
+ "confirm": {
+ "title": "Konfirmasi Kata Sandi",
+ "placeholder": "Konfirmasi Kata Sandi"
+ },
+ "strength": {
+ "title": "Kekuatan kata sandi: ",
+ "weak": "Lemah",
+ "medium": "Sedang",
+ "strong": "Kuat",
+ "veryStrong": "Sangat Kuat"
+ },
+ "requirements": {
+ "title": "Persyaratan kata sandi:",
+ "length": "Setidaknya 12 karakter"
+ },
+ "match": "Kata sandi cocok",
+ "notMatch": "Kata sandi tidak cocok"
+ },
+ "newPassword": {
+ "title": "Kata Sandi Baru",
+ "placeholder": "Masukkan kata sandi baru",
+ "confirm": {
+ "placeholder": "Masukkan ulang kata sandi baru"
+ }
+ },
+ "currentPassword": {
+ "title": "Kata Sandi Saat Ini",
+ "placeholder": "Masukkan kata sandi Anda saat ini"
+ },
+ "usernameIsRequired": "Nama pengguna wajib diisi",
+ "passwordIsRequired": "Kata sandi wajib diisi"
+ },
+ "createUser": {
+ "title": "Buat Pengguna Baru",
+ "desc": "Tambahkan akun pengguna baru dan tentukan perannya untuk akses ke area UI Frigate.",
+ "usernameOnlyInclude": "Nama pengguna hanya boleh berisi huruf, angka, . atau _",
+ "confirmPassword": "Harap konfirmasi kata sandi Anda"
+ },
+ "deleteUser": {
+ "title": "Hapus Pengguna",
+ "desc": "Tindakan ini tidak dapat dibatalkan. Ini akan menghapus akun pengguna secara permanen dan menghapus semua data terkait.",
+ "warn": "Apakah Anda yakin ingin menghapus {{username}} ?"
+ },
+ "passwordSetting": {
+ "cannotBeEmpty": "Kata sandi tidak boleh kosong",
+ "doNotMatch": "Kata sandi tidak cocok",
+ "currentPasswordRequired": "Kata sandi saat ini wajib diisi",
+ "incorrectCurrentPassword": "Kata sandi saat ini salah",
+ "passwordVerificationFailed": "Gagal memverifikasi kata sandi",
+ "updatePassword": "Perbarui Kata Sandi untuk {{username}}",
+ "setPassword": "Tetapkan Kata Sandi",
+ "desc": "Buat kata sandi yang kuat untuk mengamankan akun ini.",
+ "multiDeviceWarning": "Perangkat lain tempat Anda masuk juga akan diminta login ulang dalam {{refresh_time}}.",
+ "multiDeviceAdmin": "Anda juga dapat memaksa semua pengguna untuk segera mengautentikasi ulang dengan memutar rahasia JWT Anda."
+ },
+ "changeRole": {
+ "title": "Ubah Peran Pengguna",
+ "select": "Pilih peran",
+ "desc": "Perbarui izin untuk {{username}} ",
+ "roleInfo": {
+ "intro": "Pilih peran yang sesuai untuk pengguna ini:",
+ "admin": "Admin",
+ "adminDesc": "Akses penuh ke semua fitur.",
+ "viewer": "Viewer",
+ "viewerDesc": "Terbatas hanya pada dasbor Langsung, Tinjauan, Jelajahi, dan Ekspor.",
+ "customDesc": "Peran kustom dengan akses kamera tertentu."
+ }
+ }
+ }
+ },
+ "roles": {
+ "management": {
+ "title": "Manajemen Peran Viewer",
+ "desc": "Kelola peran viewer kustom dan izin akses kameranya untuk instance Frigate ini."
+ },
+ "addRole": "Tambah Peran",
+ "table": {
+ "role": "Peran",
+ "cameras": "Kamera",
+ "actions": "Tindakan",
+ "noRoles": "Tidak ada peran kustom ditemukan.",
+ "editCameras": "Edit Kamera",
+ "deleteRole": "Hapus Peran"
+ },
+ "toast": {
+ "success": {
+ "createRole": "Peran {{role}} berhasil dibuat",
+ "updateCameras": "Kamera untuk peran {{role}} telah diperbarui",
+ "deleteRole": "Peran {{role}} berhasil dihapus",
+ "userRolesUpdated_other": "{{count}} pengguna yang ditetapkan ke peran ini telah diperbarui menjadi 'viewer', yang memiliki akses ke semua kamera."
+ },
+ "error": {
+ "createRoleFailed": "Gagal membuat peran: {{errorMessage}}",
+ "updateCamerasFailed": "Gagal memperbarui kamera: {{errorMessage}}",
+ "deleteRoleFailed": "Gagal menghapus peran: {{errorMessage}}",
+ "userUpdateFailed": "Gagal memperbarui peran pengguna: {{errorMessage}}"
+ }
+ },
+ "dialog": {
+ "createRole": {
+ "title": "Buat Peran Baru",
+ "desc": "Tambahkan peran baru dan tentukan izin akses kamera."
+ },
+ "editCameras": {
+ "title": "Edit Kamera Peran",
+ "desc": "Perbarui akses kamera untuk peran {{role}} ."
+ },
+ "deleteRole": {
+ "title": "Hapus Peran",
+ "desc": "Tindakan ini tidak dapat dibatalkan. Ini akan menghapus peran secara permanen dan menetapkan pengguna mana pun dengan peran ini ke peran 'viewer', yang akan memberikan akses viewer ke semua kamera.",
+ "warn": "Apakah Anda yakin ingin menghapus {{role}} ?",
+ "deleting": "Sedang menghapus..."
+ },
+ "form": {
+ "role": {
+ "title": "Nama Peran",
+ "placeholder": "Masukkan nama peran",
+ "desc": "Hanya huruf, angka, titik, dan garis bawah yang diizinkan.",
+ "roleIsRequired": "Nama peran wajib diisi",
+ "roleOnlyInclude": "Nama peran hanya boleh berisi huruf, angka, . atau _",
+ "roleExists": "Peran dengan nama ini sudah ada."
+ },
+ "cameras": {
+ "title": "Kamera",
+ "desc": "Pilih kamera yang dapat diakses oleh peran ini. Setidaknya satu kamera wajib dipilih.",
+ "required": "Setidaknya satu kamera harus dipilih."
+ }
+ }
+ }
+ },
+ "notification": {
+ "title": "Notifikasi",
+ "notificationSettings": {
+ "title": "Pengaturan Notifikasi",
+ "desc": "Frigate dapat secara native mengirim notifikasi push ke perangkat Anda saat berjalan di browser atau diinstal sebagai PWA."
+ },
+ "notificationUnavailable": {
+ "title": "Notifikasi Tidak Tersedia",
+ "desc": "Notifikasi push web memerlukan konteks aman (https://…). Ini adalah batasan browser. Akses Frigate secara aman untuk menggunakan notifikasi.",
+ "descPwa": "Di iOS, notifikasi push web hanya tersedia jika Frigate dipasang ke Layar Utama Anda. Buka menu Bagikan , pilih Tambahkan ke Layar Utama , lalu buka Frigate dari ikon baru tersebut untuk mendaftarkan perangkat ini agar menerima notifikasi."
+ },
+ "globalSettings": {
+ "title": "Pengaturan Global",
+ "desc": "Tangguhkan notifikasi sementara untuk kamera tertentu pada semua perangkat yang terdaftar."
+ },
+ "email": {
+ "title": "Email",
+ "placeholder": "mis. contoh@email.com",
+ "desc": "Email yang valid wajib diisi dan akan digunakan untuk memberi tahu Anda jika ada masalah dengan layanan push."
+ },
+ "cameras": {
+ "title": "Kamera",
+ "noCameras": "Tidak ada kamera tersedia",
+ "desc": "Pilih kamera mana yang akan diaktifkan notifikasinya."
+ },
+ "deviceSpecific": "Pengaturan Khusus Perangkat",
+ "registerDevice": "Daftarkan Perangkat Ini",
+ "unregisterDevice": "Batalkan Pendaftaran Perangkat Ini",
+ "sendTestNotification": "Kirim notifikasi uji",
+ "unsavedRegistrations": "Pendaftaran Notifikasi yang belum disimpan",
+ "unsavedChanges": "Perubahan Notifikasi yang belum disimpan",
+ "active": "Notifikasi Aktif",
+ "suspended": "Notifikasi ditangguhkan {{time}}",
+ "suspendTime": {
+ "suspend": "Tangguhkan",
+ "5minutes": "Tangguhkan selama 5 menit",
+ "10minutes": "Tangguhkan selama 10 menit",
+ "30minutes": "Tangguhkan selama 30 menit",
+ "1hour": "Tangguhkan selama 1 jam",
+ "12hours": "Tangguhkan selama 12 jam",
+ "24hours": "Tangguhkan selama 24 jam",
+ "untilRestart": "Tangguhkan hingga dimulai ulang"
+ },
+ "cancelSuspension": "Batalkan Penangguhan",
+ "toast": {
+ "success": {
+ "registered": "Berhasil terdaftar untuk notifikasi. Mulai ulang Frigate diperlukan sebelum notifikasi apa pun (termasuk notifikasi uji) dapat dikirim.",
+ "settingSaved": "Pengaturan notifikasi telah disimpan."
+ },
+ "error": {
+ "registerFailed": "Gagal menyimpan pendaftaran notifikasi."
+ }
+ }
+ },
+ "frigatePlus": {
+ "title": "Pengaturan Frigate+",
+ "description": "Frigate+ adalah layanan berlangganan yang menyediakan akses ke fitur dan kemampuan tambahan untuk instance Frigate Anda, termasuk kemampuan menggunakan model deteksi objek kustom yang dilatih dengan data Anda sendiri. Anda dapat mengelola pengaturan model Frigate+ Anda di sini.",
+ "cardTitles": {
+ "api": "API",
+ "currentModel": "Model Saat Ini",
+ "otherModels": "Model Lainnya",
+ "configuration": "Konfigurasi"
+ },
+ "apiKey": {
+ "title": "Kunci API Frigate+",
+ "validated": "Kunci API Frigate+ terdeteksi dan tervalidasi",
+ "notValidated": "Kunci API Frigate+ tidak terdeteksi atau tidak tervalidasi",
+ "desc": "Kunci API Frigate+ memungkinkan integrasi dengan layanan Frigate+.",
+ "plusLink": "Baca lebih lanjut tentang Frigate+"
+ },
+ "snapshotConfig": {
+ "title": "Konfigurasi Cuplikan",
+ "desc": "Mengirim ke Frigate+ mengharuskan cuplikan diaktifkan dalam konfigurasi Anda.",
+ "cleanCopyWarning": "Beberapa kamera menonaktifkan cuplikan",
+ "table": {
+ "camera": "Kamera",
+ "snapshots": "Cuplikan"
+ }
+ },
+ "modelInfo": {
+ "title": "Informasi Model",
+ "modelType": "Tipe Model",
+ "trainDate": "Tanggal Pelatihan",
+ "baseModel": "Model Dasar",
+ "plusModelType": {
+ "baseModel": "Model Dasar",
+ "userModel": "Disetel Halus"
+ },
+ "supportedDetectors": "Detektor yang Didukung",
+ "cameras": "Kamera",
+ "loading": "Memuat informasi model…",
+ "error": "Gagal memuat informasi model",
+ "noModelLoaded": "Saat ini tidak ada model Frigate+ yang dimuat.",
+ "availableModels": "Model Frigate+ yang tersedia",
+ "loadingAvailableModels": "Memuat model yang tersedia…",
+ "selectModel": "Pilih model",
+ "noModelsAvailable": "Tidak ada model tersedia",
+ "filter": {
+ "ariaLabel": "Filter model berdasarkan tipe",
+ "baseModels": "Model Dasar",
+ "fineTunedModels": "Model yang Disetel Halus"
+ },
+ "modelSelect": "Model Frigate+ Anda yang tersedia dapat dipilih di sini. Perlu dicatat bahwa hanya model yang kompatibel dengan konfigurasi detektor Anda saat ini yang dapat dipilih."
+ },
+ "changeInDetectorsAndModel": "Ubah model",
+ "unsavedChanges": "Perubahan pengaturan Frigate+ yang belum disimpan",
+ "restart_required": "Perlu mulai ulang (model Frigate+ berubah)",
+ "toast": {
+ "success": "Pengaturan Frigate+ telah disimpan. Mulai ulang Frigate untuk menerapkan perubahan.",
+ "error": "Gagal menyimpan perubahan konfigurasi: {{errorMessage}}"
+ }
+ },
+ "detectorsAndModel": {
+ "title": "Detektor dan model",
+ "description": "Konfigurasikan backend detektor yang menjalankan deteksi objek dan model yang digunakannya. Perubahan disimpan bersama agar detektor dan model tetap sinkron.",
+ "cardTitles": {
+ "detector": "Perangkat Keras Detektor",
+ "model": "Model Deteksi"
+ },
+ "tabs": {
+ "plus": "Frigate+",
+ "custom": "Model Kustom"
+ },
+ "mismatch": {
+ "warning": "Model Frigate+ saat ini \"{{model}}\" memerlukan detektor {{required}}. Pilih model yang kompatibel di bawah atau beralih ke Model Kustom sebelum menyimpan."
+ },
+ "plusModel": {
+ "requiresDetector": "Memerlukan: {{detector}}",
+ "noModelSelected": "Pilih model Frigate+"
+ },
+ "toast": {
+ "saveSuccess": "Pengaturan detektor dan model telah disimpan. Mulai ulang Frigate untuk menerapkan perubahan.",
+ "saveError": "Gagal menyimpan pengaturan detektor dan model"
+ },
+ "unsavedChanges": "Perubahan detektor dan model yang belum disimpan",
+ "restartRequired": "Perlu mulai ulang (detektor atau model berubah)"
+ },
+ "triggers": {
+ "documentTitle": "Pemicu",
+ "semanticSearch": {
+ "title": "Pencarian Semantik dinonaktifkan",
+ "desc": "Pencarian Semantik harus diaktifkan untuk menggunakan Pemicu."
+ },
+ "management": {
+ "title": "Pemicu",
+ "desc": "Kelola pemicu untuk {{camera}}. Gunakan tipe thumbnail untuk memicu pada thumbnail yang mirip dengan objek terlacak yang Anda pilih, dan tipe deskripsi untuk memicu pada deskripsi yang mirip dengan teks yang Anda tentukan."
+ },
+ "addTrigger": "Tambah Pemicu",
+ "table": {
+ "name": "Nama",
+ "type": "Tipe",
+ "content": "Konten",
+ "threshold": "Ambang",
+ "actions": "Tindakan",
+ "noTriggers": "Tidak ada pemicu yang dikonfigurasi untuk kamera ini.",
+ "edit": "Edit",
+ "deleteTrigger": "Hapus Pemicu",
+ "lastTriggered": "Terakhir dipicu"
+ },
+ "type": {
+ "thumbnail": "Thumbnail",
+ "description": "Deskripsi"
+ },
+ "actions": {
+ "notification": "Kirim Notifikasi",
+ "sub_label": "Tambahkan Sub Label",
+ "attribute": "Tambahkan Atribut"
+ },
+ "dialog": {
+ "createTrigger": {
+ "title": "Buat Pemicu",
+ "desc": "Buat pemicu untuk kamera {{camera}}"
+ },
+ "editTrigger": {
+ "title": "Edit Pemicu",
+ "desc": "Edit pengaturan pemicu pada kamera {{camera}}"
+ },
+ "deleteTrigger": {
+ "title": "Hapus Pemicu",
+ "desc": "Apakah Anda yakin ingin menghapus pemicu {{triggerName}} ? Tindakan ini tidak dapat dibatalkan."
+ },
+ "form": {
+ "name": {
+ "title": "Nama",
+ "placeholder": "Beri nama pemicu ini",
+ "description": "Masukkan nama atau deskripsi unik untuk mengidentifikasi pemicu ini",
+ "error": {
+ "minLength": "Bidang harus memiliki panjang minimal 2 karakter.",
+ "invalidCharacters": "Bidang hanya boleh berisi huruf, angka, garis bawah, dan tanda hubung.",
+ "alreadyExists": "Pemicu dengan nama ini sudah ada untuk kamera ini."
+ }
+ },
+ "enabled": {
+ "description": "Aktifkan atau nonaktifkan pemicu ini"
+ },
+ "type": {
+ "title": "Tipe",
+ "placeholder": "Pilih tipe pemicu",
+ "description": "Picu saat deskripsi objek terlacak yang serupa terdeteksi",
+ "thumbnail": "Picu saat thumbnail objek terlacak yang serupa terdeteksi"
+ },
+ "content": {
+ "title": "Konten",
+ "imagePlaceholder": "Pilih thumbnail",
+ "textPlaceholder": "Masukkan konten teks",
+ "imageDesc": "Hanya 100 thumbnail terbaru yang ditampilkan. Jika Anda tidak dapat menemukan thumbnail yang diinginkan, silakan tinjau objek sebelumnya di Jelajahi dan atur pemicu dari menu di sana.",
+ "textDesc": "Masukkan teks untuk memicu tindakan ini saat deskripsi objek terlacak yang serupa terdeteksi.",
+ "error": {
+ "required": "Konten wajib diisi."
+ }
+ },
+ "threshold": {
+ "title": "Ambang",
+ "desc": "Tetapkan ambang kemiripan untuk pemicu ini. Ambang yang lebih tinggi berarti kecocokan yang lebih dekat diperlukan agar pemicu dijalankan.",
+ "error": {
+ "min": "Ambang harus minimal 0",
+ "max": "Ambang harus maksimal 1"
+ }
+ },
+ "actions": {
+ "title": "Tindakan",
+ "desc": "Secara default, Frigate mengirim pesan MQTT untuk semua pemicu. Sub label menambahkan nama pemicu ke label objek. Atribut adalah metadata yang dapat dicari dan disimpan secara terpisah dalam metadata objek terlacak.",
+ "error": {
+ "min": "Setidaknya satu tindakan harus dipilih."
+ }
+ }
+ }
+ },
+ "wizard": {
+ "title": "Buat Pemicu",
+ "step1": {
+ "description": "Konfigurasikan pengaturan dasar untuk pemicu Anda."
+ },
+ "step2": {
+ "description": "Atur konten yang akan memicu tindakan ini."
+ },
+ "step3": {
+ "description": "Konfigurasikan ambang dan tindakan untuk pemicu ini."
+ },
+ "steps": {
+ "nameAndType": "Nama dan Tipe",
+ "configureData": "Konfigurasikan Data",
+ "thresholdAndActions": "Ambang dan Tindakan"
+ }
+ },
+ "toast": {
+ "success": {
+ "createTrigger": "Pemicu {{name}} berhasil dibuat.",
+ "updateTrigger": "Pemicu {{name}} berhasil diperbarui.",
+ "deleteTrigger": "Pemicu {{name}} berhasil dihapus."
+ },
+ "error": {
+ "createTriggerFailed": "Gagal membuat pemicu: {{errorMessage}}",
+ "updateTriggerFailed": "Gagal memperbarui pemicu: {{errorMessage}}",
+ "deleteTriggerFailed": "Gagal menghapus pemicu: {{errorMessage}}"
+ }
+ }
+ },
+ "maintenance": {
+ "title": "Pemeliharaan",
+ "sync": {
+ "title": "Sinkronisasi Media",
+ "desc": "Frigate akan secara berkala membersihkan media sesuai jadwal reguler berdasarkan konfigurasi retensi Anda. Wajar jika ada beberapa file yatim piatu saat Frigate berjalan. Gunakan fitur ini untuk menghapus file media yatim piatu dari disk yang tidak lagi direferensikan dalam basis data.",
+ "started": "Sinkronisasi media dimulai.",
+ "alreadyRunning": "Pekerjaan sinkronisasi sudah sedang berjalan",
+ "error": "Gagal memulai sinkronisasi",
+ "currentStatus": "Status",
+ "jobId": "ID Pekerjaan",
+ "startTime": "Waktu Mulai",
+ "endTime": "Waktu Selesai",
+ "statusLabel": "Status",
+ "results": "Hasil",
+ "errorLabel": "Kesalahan",
+ "mediaTypes": "Tipe Media",
+ "allMedia": "Semua Media",
+ "dryRun": "Dry Run",
+ "dryRunEnabled": "Tidak ada file yang akan dihapus",
+ "dryRunDisabled": "File akan dihapus",
+ "force": "Paksa",
+ "forceDesc": "Lewati ambang keamanan dan selesaikan sinkronisasi meskipun lebih dari 50% file akan dihapus.",
+ "verbose": "Verbose",
+ "verboseDesc": "Tulis daftar lengkap file yatim piatu ke disk untuk ditinjau.",
+ "running": "Sinkronisasi Berjalan...",
+ "start": "Mulai Sinkronisasi",
+ "inProgress": "Sinkronisasi sedang berlangsung. Halaman ini dinonaktifkan.",
+ "status": {
+ "queued": "Dalam antrean",
+ "running": "Berjalan",
+ "completed": "Selesai",
+ "failed": "Gagal",
+ "notRunning": "Tidak Berjalan"
+ },
+ "resultsFields": {
+ "filesChecked": "File Diperiksa",
+ "orphansFound": "File Yatim Piatu Ditemukan",
+ "orphansDeleted": "File Yatim Piatu Dihapus",
+ "aborted": "Dibatalkan. Penghapusan akan melebihi ambang keamanan.",
+ "error": "Kesalahan",
+ "totals": "Total"
+ },
+ "event_snapshots": "Cuplikan Objek Terlacak",
+ "event_thumbnails": "Thumbnail Objek Terlacak",
+ "review_thumbnails": "Thumbnail Tinjauan",
+ "previews": "Pratinjau",
+ "exports": "Ekspor",
+ "recordings": "Rekaman"
+ },
+ "regionGrid": {
+ "title": "Grid Wilayah",
+ "desc": "Grid wilayah adalah optimasi yang mempelajari di mana objek dengan ukuran berbeda biasanya muncul dalam bidang pandang tiap kamera. Frigate menggunakan data ini untuk menentukan ukuran wilayah deteksi secara efisien. Grid ini dibangun secara otomatis seiring waktu dari data objek terlacak.",
+ "clear": "Hapus grid wilayah",
+ "clearConfirmTitle": "Hapus Grid Wilayah",
+ "clearConfirmDesc": "Menghapus grid wilayah tidak direkomendasikan kecuali Anda baru-baru ini mengubah ukuran model detektor atau mengubah posisi fisik kamera dan mengalami masalah pelacakan objek. Grid akan dibangun ulang secara otomatis seiring waktu saat objek dilacak. Frigate perlu dimulai ulang agar perubahan diterapkan.",
+ "clearSuccess": "Grid wilayah berhasil dihapus",
+ "clearError": "Gagal menghapus grid wilayah",
+ "restartRequired": "Perlu mulai ulang agar perubahan grid wilayah diterapkan"
+ }
+ },
+ "configForm": {
+ "global": {
+ "title": "Pengaturan Global",
+ "description": "Pengaturan ini berlaku untuk semua kamera kecuali ditimpa dalam pengaturan khusus kamera."
+ },
+ "camera": {
+ "title": "Pengaturan Kamera",
+ "description": "Pengaturan ini hanya berlaku untuk kamera ini dan menimpa pengaturan global.",
+ "noCameras": "Tidak ada kamera tersedia"
+ },
+ "advancedSettingsCount": "Pengaturan Lanjutan ({{count}})",
+ "advancedCount": "Lanjutan ({{count}})",
+ "showAdvanced": "Tampilkan Pengaturan Lanjutan",
+ "tabs": {
+ "sharedDefaults": "Default Bersama",
+ "system": "Sistem",
+ "integrations": "Integrasi"
+ },
+ "additionalProperties": {
+ "keyLabel": "Kunci",
+ "valueLabel": "Nilai",
+ "keyPlaceholder": "Kunci baru",
+ "remove": "Hapus"
+ },
+ "knownPlates": {
+ "namePlaceholder": "mis., Mobil Istri",
+ "platePlaceholder": "Nomor pelat atau regex"
+ },
+ "timezone": {
+ "defaultOption": "Gunakan zona waktu browser"
+ },
+ "roleMap": {
+ "empty": "Tidak ada pemetaan peran",
+ "roleLabel": "Peran",
+ "groupsLabel": "Grup",
+ "addMapping": "Tambah pemetaan peran",
+ "remove": "Hapus"
+ },
+ "ffmpegArgs": {
+ "preset": "Preset",
+ "manual": "Argumen manual",
+ "inherit": "Warisi dari pengaturan kamera",
+ "none": "Tidak ada",
+ "useGlobalSetting": "Warisi dari pengaturan global",
+ "selectPreset": "Pilih preset",
+ "manualPlaceholder": "Masukkan argumen FFmpeg",
+ "presetLabels": {
+ "preset-rpi-64-h264": "Raspberry Pi (H.264)",
+ "preset-rpi-64-h265": "Raspberry Pi (H.265)",
+ "preset-vaapi": "VAAPI (GPU Intel/AMD)",
+ "preset-intel-qsv-h264": "Intel QuickSync (H.264)",
+ "preset-intel-qsv-h265": "Intel QuickSync (H.265)",
+ "preset-nvidia": "GPU NVIDIA",
+ "preset-jetson-h264": "NVIDIA Jetson (H.264)",
+ "preset-jetson-h265": "NVIDIA Jetson (H.265)",
+ "preset-rkmpp": "Rockchip RKMPP",
+ "preset-http-jpeg-generic": "HTTP JPEG (Generik)",
+ "preset-http-mjpeg-generic": "HTTP MJPEG (Generik)",
+ "preset-http-reolink": "HTTP - Kamera Reolink",
+ "preset-rtmp-generic": "RTMP (Generik)",
+ "preset-rtsp-generic": "RTSP (Generik)",
+ "preset-rtsp-restream": "RTSP - Restream dari go2rtc",
+ "preset-rtsp-restream-low-latency": "RTSP - Restream dari go2rtc (Latensi Rendah)",
+ "preset-rtsp-udp": "RTSP - UDP",
+ "preset-rtsp-blue-iris": "RTSP - Blue Iris",
+ "preset-record-generic": "Rekam (Generik, tanpa audio)",
+ "preset-record-generic-audio-copy": "Rekam (Generik + Salin Audio)",
+ "preset-record-generic-audio-aac": "Rekam (Generik + Audio ke AAC)",
+ "preset-record-mjpeg": "Rekam - Kamera MJPEG",
+ "preset-record-jpeg": "Rekam - Kamera JPEG",
+ "preset-record-ubiquiti": "Rekam - Kamera Ubiquiti"
+ }
+ },
+ "cameraInputs": {
+ "itemTitle": "Stream {{index}}"
+ },
+ "restartRequiredField": "Perlu mulai ulang",
+ "restartRequiredFooter": "Konfigurasi berubah - Perlu mulai ulang",
+ "sections": {
+ "detect": "Deteksi",
+ "record": "Perekaman",
+ "snapshots": "Cuplikan",
+ "motion": "Gerakan",
+ "objects": "Objek",
+ "review": "Tinjauan",
+ "audio": "Audio",
+ "notifications": "Notifikasi",
+ "live": "Tampilan Langsung",
+ "timestamp_style": "Stempel waktu",
+ "mqtt": "MQTT",
+ "database": "Basis data",
+ "telemetry": "Telemetri",
+ "auth": "Autentikasi",
+ "tls": "TLS",
+ "proxy": "Proksi",
+ "go2rtc": "go2rtc",
+ "ffmpeg": "FFmpeg",
+ "detectors": "Detektor",
+ "model": "Model",
+ "semantic_search": "Pencarian Semantik",
+ "genai": "GenAI",
+ "face_recognition": "Pengenalan Wajah",
+ "lpr": "Pengenalan Pelat Nomor",
+ "birdseye": "Birdseye",
+ "masksAndZones": "Masker / Zona"
+ },
+ "detect": {
+ "title": "Pengaturan Deteksi"
+ },
+ "detectors": {
+ "title": "Pengaturan Detektor",
+ "singleType": "Hanya satu detektor {{type}} yang diizinkan.",
+ "keyRequired": "Nama detektor wajib diisi.",
+ "keyDuplicate": "Nama detektor sudah ada.",
+ "noSchema": "Tidak ada skema detektor yang tersedia.",
+ "none": "Tidak ada instance detektor yang dikonfigurasi.",
+ "add": "Tambah detektor",
+ "addCustomKey": "Tambah kunci kustom"
+ },
+ "record": {
+ "title": "Pengaturan Perekaman"
+ },
+ "snapshots": {
+ "title": "Pengaturan Cuplikan"
+ },
+ "motion": {
+ "title": "Pengaturan Gerakan"
+ },
+ "objects": {
+ "title": "Pengaturan Objek"
+ },
+ "audioLabels": {
+ "summary": "{{count}} label audio dipilih",
+ "empty": "Tidak ada label audio tersedia"
+ },
+ "objectLabels": {
+ "summary": "{{count}} tipe objek dipilih",
+ "empty": "Tidak ada label objek tersedia"
+ },
+ "reviewLabels": {
+ "summary": "{{count}} label dipilih",
+ "empty": "Tidak ada label tersedia"
+ },
+ "filters": {
+ "objectFieldLabel": "{{field}} untuk {{label}}"
+ },
+ "zoneNames": {
+ "summary": "{{count}} dipilih",
+ "empty": "Tidak ada zona tersedia"
+ },
+ "inputRoles": {
+ "summary": "{{count}} peran dipilih",
+ "empty": "Tidak ada peran tersedia",
+ "options": {
+ "detect": "Deteksi",
+ "record": "Rekam",
+ "audio": "Audio"
+ }
+ },
+ "genaiRoles": {
+ "options": {
+ "embeddings": "Embedding",
+ "descriptions": "Deskripsi",
+ "chat": "Chat"
+ }
+ },
+ "semanticSearchModel": {
+ "placeholder": "Pilih model…",
+ "builtIn": "Model Bawaan",
+ "genaiProviders": "Penyedia GenAI"
+ },
+ "review": {
+ "title": "Pengaturan Tinjauan"
+ },
+ "audio": {
+ "title": "Pengaturan Audio"
+ },
+ "notifications": {
+ "title": "Pengaturan Notifikasi"
+ },
+ "live": {
+ "title": "Pengaturan Tampilan Langsung"
+ },
+ "timestamp_style": {
+ "title": "Pengaturan Stempel Waktu"
+ },
+ "searchPlaceholder": "Cari...",
+ "addCustomLabel": "Tambahkan label kustom...",
+ "genaiModel": {
+ "placeholder": "Pilih atau masukkan model…",
+ "search": "Cari atau masukkan model…",
+ "noModels": "Tidak ada model tersedia",
+ "available": "Model yang tersedia",
+ "useCustom": "Gunakan \"{{value}}\"",
+ "refresh": "Segarkan model",
+ "probeFailed": "Gagal memeriksa model",
+ "fetchedModels": "Berhasil mengambil daftar model"
+ },
+ "liveStreams": {
+ "streamNameLabel": "Nama stream",
+ "streamNamePlaceholder": "misalnya, Stream HD Utama",
+ "go2rtcStreamLabel": "Stream go2rtc",
+ "go2rtcStreamPlaceholder": "Pilih stream go2rtc",
+ "go2rtcStreamSearch": "Cari atau masukkan nama stream…",
+ "noGo2rtcStreams": "Tidak ada stream go2rtc yang dikonfigurasi",
+ "availableStreams": "Stream yang tersedia",
+ "useCustom": "Gunakan \"{{value}}\"",
+ "addStream": "Tambahkan stream"
+ },
+ "semanticSearchModelSize": {
+ "notApplicable": "Tidak berlaku untuk penyedia GenAI"
+ },
+ "ptzPresets": {
+ "placeholder": "Pilih atau masukkan preset...",
+ "search": "Cari atau masukkan preset...",
+ "noPresets": "Tidak ada preset yang tersedia",
+ "available": "Preset kamera",
+ "useCustom": "Gunakan \"{{value}}\""
+ },
+ "defaultRole": {
+ "admin": "Admin",
+ "viewer": "Penampil"
+ }
+ },
+ "globalConfig": {
+ "title": "Konfigurasi Global",
+ "description": "Konfigurasikan pengaturan global yang berlaku untuk semua kamera kecuali jika ditimpa.",
+ "toast": {
+ "success": "Pengaturan global berhasil disimpan",
+ "error": "Gagal menyimpan pengaturan global",
+ "validationError": "Validasi gagal"
+ }
+ },
+ "cameraConfig": {
+ "title": "Konfigurasi Kamera",
+ "description": "Konfigurasikan pengaturan untuk masing-masing kamera. Pengaturan menimpa default global.",
+ "overriddenBadge": "Ditimpa",
+ "resetToGlobal": "Atur Ulang ke Global",
+ "toast": {
+ "success": "Pengaturan kamera berhasil disimpan",
+ "error": "Gagal menyimpan pengaturan kamera"
+ }
+ },
+ "toast": {
+ "success": "Pengaturan berhasil disimpan",
+ "applied": "Pengaturan berhasil diterapkan",
+ "successRestartRequired": "Pengaturan berhasil disimpan. Mulai ulang Frigate untuk menerapkan perubahan Anda.",
+ "error": "Gagal menyimpan pengaturan",
+ "validationError": "Validasi gagal: {{message}}",
+ "resetSuccess": "Atur ulang ke default global",
+ "resetError": "Gagal mengatur ulang pengaturan",
+ "saveAllSuccess_other": "Semua {{count}} bagian berhasil disimpan.",
+ "saveAllSuccessRestartRequired_other": "Semua {{count}} bagian berhasil disimpan. Mulai ulang Frigate untuk menerapkan perubahan Anda.",
+ "saveAllPartial_other": "{{successCount}} dari {{totalCount}} bagian berhasil disimpan. {{failCount}} gagal.",
+ "saveAllFailure": "Gagal menyimpan semua bagian."
+ },
+ "profiles": {
+ "title": "Profil",
+ "activeProfile": "Profil Aktif",
+ "noActiveProfile": "Tidak ada profil aktif",
+ "active": "Aktif",
+ "activated": "Profil '{{profile}}' diaktifkan",
+ "activateFailed": "Gagal menetapkan profil",
+ "deactivated": "Profil dinonaktifkan",
+ "noProfiles": "Tidak ada profil yang didefinisikan.",
+ "noOverrides": "Tidak ada penimpaan",
+ "cameraCount_other": "{{count}} kamera",
+ "columnCamera": "Kamera",
+ "columnOverrides": "Penimpaan Profil",
+ "baseConfig": "Konfigurasi Dasar",
+ "addProfile": "Tambah Profil",
+ "newProfile": "Profil Baru",
+ "profileNamePlaceholder": "mis., Armed, Away, Night Mode",
+ "friendlyNameLabel": "Nama Profil",
+ "profileIdLabel": "ID Profil",
+ "profileIdDescription": "Pengidentifikasi internal yang digunakan dalam konfigurasi dan otomatisasi",
+ "nameInvalid": "Hanya huruf kecil, angka, dan garis bawah yang diizinkan",
+ "nameDuplicate": "Profil dengan nama ini sudah ada",
+ "error": {
+ "mustBeAtLeastTwoCharacters": "Harus minimal 2 karakter",
+ "mustNotContainPeriod": "Tidak boleh mengandung titik",
+ "alreadyExists": "Profil dengan ID ini sudah ada"
+ },
+ "renameProfile": "Ubah Nama Profil",
+ "renameSuccess": "Profil diubah namanya menjadi '{{profile}}'",
+ "deleteProfile": "Hapus Profil",
+ "deleteProfileConfirm": "Hapus profil \"{{profile}}\" dari semua kamera? Ini tidak dapat dibatalkan.",
+ "deleteSuccess": "Profil '{{profile}}' dihapus",
+ "createSuccess": "Profil '{{profile}}' dibuat",
+ "removeOverride": "Hapus Penimpaan Profil",
+ "deleteSection": "Hapus Penimpaan Bagian",
+ "deleteSectionConfirm": "Hapus penimpaan {{section}} untuk profil {{profile}} pada {{camera}}?",
+ "deleteSectionSuccess": "Penimpaan {{section}} untuk {{profile}} telah dihapus",
+ "enableSwitch": "Aktifkan Profil",
+ "enabledDescription": "Profil diaktifkan. Buat profil baru di bawah, buka bagian konfigurasi kamera untuk membuat perubahan, lalu simpan agar perubahan berlaku.",
+ "disabledDescription": "Profil memungkinkan Anda mendefinisikan kumpulan bernama dari penimpaan konfigurasi kamera (mis., armed, away, night) yang dapat diaktifkan sesuai kebutuhan."
+ },
+ "unsavedChanges": "Anda memiliki perubahan yang belum disimpan",
+ "confirmReset": "Konfirmasi Atur Ulang",
+ "resetToDefaultDescription": "Ini akan mengatur ulang semua pengaturan di bagian ini ke nilai defaultnya. Tindakan ini tidak dapat dibatalkan.",
+ "resetToGlobalDescription": "Ini akan mengatur ulang pengaturan di bagian ini ke default global. Tindakan ini tidak dapat dibatalkan.",
+ "go2rtcStreams": {
+ "title": "Stream go2rtc",
+ "description": "Kelola konfigurasi stream go2rtc untuk restreaming kamera. Setiap stream memiliki nama dan satu atau lebih URL sumber.",
+ "addStream": "Tambah stream",
+ "addStreamDesc": "Masukkan nama untuk stream baru. Nama ini akan digunakan untuk merujuk stream dalam konfigurasi kamera Anda.",
+ "addUrl": "Tambah URL",
+ "streamNumber": "Stream {{index}}",
+ "streamName": "Nama stream",
+ "streamNamePlaceholder": "mis., front_door",
+ "streamUrlPlaceholder": "mis., rtsp://user:pass@192.168.1.100/stream",
+ "deleteStream": "Hapus stream",
+ "deleteStreamConfirm": "Apakah Anda yakin ingin menghapus stream \"{{streamName}}\"? Kamera yang merujuk stream ini mungkin berhenti berfungsi.",
+ "noStreams": "Tidak ada stream go2rtc yang dikonfigurasi. Tambahkan stream untuk memulai.",
+ "validation": {
+ "nameRequired": "Nama stream wajib diisi",
+ "nameDuplicate": "Stream dengan nama ini sudah ada",
+ "nameInvalid": "Nama stream hanya boleh berisi huruf, angka, garis bawah, dan tanda hubung",
+ "urlRequired": "Setidaknya satu URL wajib diisi"
+ },
+ "renameStream": "Ubah nama stream",
+ "renameStreamDesc": "Masukkan nama baru untuk stream ini. Mengubah nama stream dapat merusak kamera atau stream lain yang merujuknya berdasarkan nama.",
+ "newStreamName": "Nama stream baru",
+ "ffmpeg": {
+ "useFfmpegModule": "Gunakan mode kompatibilitas (ffmpeg)",
+ "video": "Video",
+ "audio": "Audio",
+ "hardware": "Akselerasi perangkat keras",
+ "videoCopy": "Salin",
+ "videoH264": "Transkode ke H.264",
+ "videoH265": "Transkode ke H.265",
+ "videoExclude": "Kecualikan",
+ "audioCopy": "Salin",
+ "audioAac": "Transkode ke AAC",
+ "audioOpus": "Transkode ke Opus",
+ "audioPcmu": "Transkode ke PCM μ-law",
+ "audioPcma": "Transkode ke PCM A-law",
+ "audioPcm": "Transkode ke PCM",
+ "audioMp3": "Transkode ke MP3",
+ "audioExclude": "Kecualikan",
+ "hardwareNone": "Tanpa akselerasi perangkat keras",
+ "hardwareAuto": "Otomatis (direkomendasikan)",
+ "hardwareVaapi": "VAAPI",
+ "hardwareCuda": "CUDA",
+ "hardwareV4l2m2m": "V4L2 M2M",
+ "hardwareDxva2": "DXVA2",
+ "hardwareVideotoolbox": "VideoToolbox",
+ "addVideoCodec": "Tambah codec video",
+ "addAudioCodec": "Tambah codec audio",
+ "removeCodec": "Hapus codec"
+ },
+ "sourceNumber": "Sumber {{index}}"
+ },
+ "birdseye": {
+ "trackingMode": {
+ "objects": "Objek",
+ "motion": "Gerakan",
+ "continuous": "Kontinu"
+ },
+ "cameraOrder": {
+ "label": "Urutan kamera",
+ "description": "Seret kamera untuk mengatur urutannya dalam tata letak Birdseye.",
+ "reorderHandle": "Seret untuk mengubah urutan",
+ "saving": "Menyimpan…",
+ "saved": "Tersimpan"
+ }
+ },
+ "retainMode": {
+ "all": "Semua",
+ "motion": "Gerakan",
+ "active_objects": "Objek Aktif"
+ },
+ "previewQuality": {
+ "very_high": "Sangat Tinggi",
+ "high": "Tinggi",
+ "medium": "Sedang",
+ "low": "Rendah",
+ "very_low": "Sangat Rendah"
+ },
+ "ui": {
+ "timeFormat": {
+ "browser": "Browser",
+ "12hour": "12 jam",
+ "24hour": "24 jam"
+ },
+ "TimeOrDateStyle": {
+ "full": "Penuh",
+ "long": "Panjang",
+ "medium": "Sedang",
+ "short": "Pendek"
+ },
+ "unitSystem": {
+ "metric": "Metrik",
+ "imperial": "Imperial"
+ }
+ },
+ "review": {
+ "imageSource": {
+ "recordings": "Rekaman",
+ "previews": "Pratinjau"
+ }
+ },
+ "logger": {
+ "logLevel": {
+ "debug": "Debug",
+ "info": "Info",
+ "warning": "Peringatan",
+ "error": "Kesalahan",
+ "critical": "Kritis"
+ }
+ },
+ "onvif": {
+ "profileAuto": "Otomatis",
+ "profileLoading": "Memuat profil...",
+ "autotracking": {
+ "zooming": {
+ "disabled": "Dinonaktifkan",
+ "absolute": "Absolut",
+ "relative": "Relatif"
+ }
+ }
+ },
+ "modelSize": {
+ "small": "Kecil",
+ "large": "Besar"
}
}
diff --git a/web/public/locales/id/views/system.json b/web/public/locales/id/views/system.json
index 7cf1597d59..639d94cc21 100644
--- a/web/public/locales/id/views/system.json
+++ b/web/public/locales/id/views/system.json
@@ -1,13 +1,14 @@
{
"documentTitle": {
- "cameras": "Status kamera - Frigate",
- "storage": "Status Penyimpanan - Frigate",
- "general": "Status umum - Frigate",
+ "cameras": "Statistik Kamera - Frigate",
+ "storage": "Statistik Penyimpanan - Frigate",
+ "general": "Statistik Umum - Frigate",
"enrichments": "Statistik Enrichment - Frigate",
"logs": {
"frigate": "Log Frigate - Frigate",
"go2rtc": "Log Go2RTC - Frigate",
- "nginx": "Log NGINX - Frigate"
+ "nginx": "Log Nginx - Frigate",
+ "websocket": "Log Pesan - Frigate"
}
},
"title": "Sistem",
@@ -17,32 +18,242 @@
"label": "Unduh Log"
},
"copy": {
- "label": "Salin ke Clipboard",
- "success": "Log tersalin ke clipboard",
- "error": "Tidak dapat menyalin ke clipboard"
+ "label": "Salin ke Papan Klip",
+ "success": "Log berhasil disalin ke papan klip",
+ "error": "Tidak dapat menyalin log ke papan klip"
},
"type": {
- "label": "Tipe",
- "timestamp": "Waktu",
+ "label": "Jenis",
+ "timestamp": "Stempel waktu",
"tag": "Tag",
"message": "Pesan"
},
- "tips": "Logs sedang berjalan dari server",
+ "tips": "Log sedang dialirkan dari server",
"toast": {
"error": {
- "fetchingLogsFailed": "Error saat mengambil log: {{errorMessage}}",
- "whileStreamingLogs": "Eror saat streaming logs: {{errorMessage}}"
+ "fetchingLogsFailed": "Kesalahan saat mengambil log: {{errorMessage}}",
+ "whileStreamingLogs": "Kesalahan saat mengalirkan log: {{errorMessage}}"
+ }
+ },
+ "websocket": {
+ "label": "Pesan",
+ "pause": "Jeda",
+ "resume": "Lanjutkan",
+ "clear": "Bersihkan",
+ "filter": {
+ "all": "Semua topik",
+ "topics": "Topik",
+ "events": "Peristiwa",
+ "reviews": "Tinjauan",
+ "classification": "Klasifikasi",
+ "face_recognition": "Pengenalan Wajah",
+ "lpr": "LPR",
+ "camera_activity": "Aktivitas kamera",
+ "system": "Sistem",
+ "camera": "Kamera",
+ "all_cameras": "Semua kamera",
+ "cameras_count_one": "{{count}} Kamera",
+ "cameras_count_other": "{{count}} Kamera"
+ },
+ "empty": "Belum ada pesan yang ditangkap",
+ "count_one": "{{count}} pesan",
+ "count_other": "{{count}} pesan",
+ "expanded": {
+ "payload": "Payload"
}
}
},
"general": {
"title": "Umum",
"detector": {
- "title": "Pendeteksi",
- "inferenceSpeed": "Pendeteksi Kecepatan Inferensi",
- "temperature": "Pendeteksi Suhu",
- "cpuUsage": "Pendeteksi penggunaan CPU",
- "cpuUsageInformation": "CPU yang digunakan dalam mempersiapkan data masukan dan keluaran ke/dari model deteksi. Nilai ini tidak mengukur penggunaan inferensi, bahkan jika menggunakan GPU atau akselerator."
+ "title": "Detektor",
+ "inferenceSpeed": "Kecepatan Inferensi Detektor",
+ "temperature": "Suhu Detektor",
+ "cpuUsage": "Penggunaan CPU Detektor",
+ "cpuUsageInformation": "CPU yang digunakan untuk menyiapkan data input dan output ke/dari model deteksi. Nilai ini tidak mengukur penggunaan inferensi, meskipun menggunakan GPU atau akselerator.",
+ "memoryUsage": "Penggunaan Memori Detektor"
+ },
+ "hardwareInfo": {
+ "title": "Info Perangkat Keras",
+ "gpuUsage": "Penggunaan GPU",
+ "gpuMemory": "Memori GPU",
+ "gpuEncoder": "Encoder GPU",
+ "gpuCompute": "Komputasi / Enkode GPU",
+ "gpuDecoder": "Decoder GPU",
+ "gpuTemperature": "Suhu GPU",
+ "gpuInfo": {
+ "vainfoOutput": {
+ "title": "Output Vainfo",
+ "returnCode": "Kode Pengembalian: {{code}}",
+ "processOutput": "Output Proses:",
+ "processError": "Kesalahan Proses:"
+ },
+ "nvidiaSMIOutput": {
+ "title": "Output Nvidia SMI",
+ "name": "Nama: {{name}}",
+ "driver": "Driver: {{driver}}",
+ "cudaComputerCapability": "Kemampuan Komputasi CUDA: {{cuda_compute}}",
+ "vbios": "Info VBios: {{vbios}}"
+ },
+ "closeInfo": {
+ "label": "Tutup info GPU"
+ },
+ "copyInfo": {
+ "label": "Salin info GPU"
+ },
+ "toast": {
+ "success": "Info GPU berhasil disalin ke papan klip"
+ }
+ },
+ "npuUsage": "Penggunaan NPU",
+ "npuMemory": "Memori NPU",
+ "npuTemperature": "Suhu NPU",
+ "intelGpuWarning": {
+ "title": "Peringatan Statistik GPU Intel",
+ "message": "Statistik GPU tidak tersedia",
+ "description": "Ini adalah bug yang sudah diketahui pada alat pelaporan statistik GPU Intel (intel_gpu_top) yang dapat rusak dan berulang kali mengembalikan penggunaan GPU sebesar 0% bahkan ketika akselerasi perangkat keras dan deteksi objek berjalan dengan benar pada (i)GPU. Ini bukan bug Frigate. Anda dapat memulai ulang host untuk memperbaiki masalah ini sementara dan memastikan GPU berfungsi dengan benar. Ini tidak memengaruhi kinerja."
+ }
+ },
+ "otherProcesses": {
+ "title": "Proses Lainnya",
+ "processCpuUsage": "Penggunaan CPU Proses",
+ "processMemoryUsage": "Penggunaan Memori Proses",
+ "series": {
+ "go2rtc": "go2rtc",
+ "recording": "perekaman",
+ "review_segment": "segmen tinjauan",
+ "embeddings": "embedding",
+ "audio_detector": "detektor audio"
+ }
+ }
+ },
+ "storage": {
+ "title": "Penyimpanan",
+ "overview": "Ringkasan",
+ "recordings": {
+ "title": "Rekaman",
+ "tips": "Nilai ini menunjukkan total penyimpanan yang digunakan oleh rekaman di basis data Frigate. Frigate tidak melacak penggunaan penyimpanan untuk semua file di disk Anda.",
+ "earliestRecording": "Rekaman paling awal yang tersedia:"
+ },
+ "shm": {
+ "title": "Alokasi SHM (memori bersama)",
+ "warning": "Ukuran SHM saat ini sebesar {{total}}MB terlalu kecil. Tingkatkan menjadi setidaknya {{min_shm}}MB.",
+ "frameLifetime": {
+ "title": "Masa hidup frame",
+ "description": "Setiap kamera memiliki {{frames}} slot frame di memori bersama. Pada laju frame kamera tercepat, setiap frame tersedia selama sekitar {{lifetime}} dtk sebelum ditimpa."
+ }
+ },
+ "cameraStorage": {
+ "title": "Penyimpanan Kamera",
+ "camera": "Kamera",
+ "unusedStorageInformation": "Informasi Penyimpanan Tidak Terpakai",
+ "storageUsed": "Penyimpanan",
+ "percentageOfTotalUsed": "Persentase dari Total",
+ "bandwidth": "Bandwidth",
+ "unused": {
+ "title": "Tidak Terpakai",
+ "tips": "Nilai ini mungkin tidak secara akurat merepresentasikan ruang kosong yang tersedia untuk Frigate jika Anda memiliki file lain yang disimpan di drive selain rekaman Frigate. Frigate tidak melacak penggunaan penyimpanan di luar rekamannya."
+ }
+ }
+ },
+ "cameras": {
+ "title": "Kamera",
+ "overview": "Ringkasan",
+ "info": {
+ "aspectRatio": "rasio aspek",
+ "cameraProbeInfo": "Info Probe Kamera {{camera}}",
+ "streamDataFromFFPROBE": "Data stream diperoleh dengan ffprobe.",
+ "fetching": "Mengambil Data Kamera",
+ "stream": "Stream {{idx}}",
+ "video": "Video:",
+ "codec": "Codec:",
+ "resolution": "Resolusi:",
+ "fps": "FPS:",
+ "unknown": "Tidak diketahui",
+ "audio": "Audio:",
+ "error": "Kesalahan: {{error}}",
+ "tips": {
+ "title": "Info Probe Kamera"
+ }
+ },
+ "framesAndDetections": "Frame / Deteksi",
+ "noCameras": {
+ "title": "Tidak Ada Kamera Ditemukan"
+ },
+ "label": {
+ "camera": "kamera",
+ "detect": "deteksi",
+ "skipped": "dilewati",
+ "ffmpeg": "FFmpeg",
+ "capture": "penangkapan",
+ "overallFramesPerSecond": "jumlah frame per detik keseluruhan",
+ "overallDetectionsPerSecond": "jumlah deteksi per detik keseluruhan",
+ "overallSkippedDetectionsPerSecond": "jumlah deteksi yang dilewati per detik keseluruhan",
+ "cameraFfmpeg": "FFmpeg {{camName}}",
+ "cameraCapture": "penangkapan {{camName}}",
+ "cameraDetect": "deteksi {{camName}}",
+ "cameraGpu": "GPU {{camName}}",
+ "cameraFramesPerSecond": "frame per detik {{camName}}",
+ "cameraDetectionsPerSecond": "deteksi per detik {{camName}}",
+ "cameraSkippedDetectionsPerSecond": "deteksi yang dilewati per detik {{camName}}"
+ },
+ "connectionQuality": {
+ "title": "Kualitas Koneksi",
+ "excellent": "Sangat Baik",
+ "fair": "Cukup",
+ "poor": "Buruk",
+ "unusable": "Tidak Dapat Digunakan",
+ "fps": "FPS",
+ "expectedFps": "FPS yang Diharapkan",
+ "reconnectsLastHour": "Penyambungan ulang (1 jam terakhir)",
+ "stallsLastHour": "Macet (1 jam terakhir)"
+ },
+ "toast": {
+ "success": {
+ "copyToClipboard": "Data probe berhasil disalin ke papan klip."
+ },
+ "error": {
+ "unableToProbeCamera": "Tidak dapat memeriksa kamera: {{errorMessage}}"
+ }
+ }
+ },
+ "lastRefreshed": "Terakhir diperbarui: ",
+ "stats": {
+ "ffmpegHighCpuUsage": "{{camera}} memiliki penggunaan CPU FFmpeg yang tinggi ({{ffmpegAvg}}%)",
+ "detectHighCpuUsage": "{{camera}} memiliki penggunaan CPU deteksi yang tinggi ({{detectAvg}}%)",
+ "healthy": "Sistem sehat",
+ "reindexingEmbeddings": "Mengindeks ulang embedding ({{processed}}% selesai)",
+ "cameraIsOffline": "{{camera}} sedang offline",
+ "detectIsSlow": "{{detect}} lambat ({{speed}} md)",
+ "detectIsVerySlow": "{{detect}} sangat lambat ({{speed}} md)",
+ "shmTooLow": "Alokasi /dev/shm ({{total}} MB) harus ditingkatkan menjadi setidaknya {{min}} MB.",
+ "debugReplayActive": "Sesi pemutaran ulang debug sedang aktif"
+ },
+ "enrichments": {
+ "title": "Enrichment",
+ "infPerSecond": "Inferensi Per Detik",
+ "averageInf": "Waktu Inferensi Rata-rata",
+ "embeddings": {
+ "image_embedding": "Embedding Gambar",
+ "text_embedding": "Embedding Teks",
+ "face_recognition": "Pengenalan Wajah",
+ "plate_recognition": "Pengenalan Plat",
+ "image_embedding_speed": "Kecepatan Embedding Gambar",
+ "face_embedding_speed": "Kecepatan Embedding Wajah",
+ "face_recognition_speed": "Kecepatan Pengenalan Wajah",
+ "plate_recognition_speed": "Kecepatan Pengenalan Plat",
+ "text_embedding_speed": "Kecepatan Embedding Teks",
+ "yolov9_plate_detection_speed": "Kecepatan Deteksi Plat YOLOv9",
+ "yolov9_plate_detection": "Deteksi Plat YOLOv9",
+ "review_description": "Deskripsi Tinjauan",
+ "review_description_speed": "Kecepatan Deskripsi Tinjauan",
+ "review_description_events_per_second": "Deskripsi Tinjauan",
+ "object_description": "Deskripsi Objek",
+ "object_description_speed": "Kecepatan Deskripsi Objek",
+ "object_description_events_per_second": "Deskripsi Objek",
+ "classification": "Klasifikasi {{name}}",
+ "classification_speed": "Kecepatan Klasifikasi {{name}}",
+ "classification_events_per_second": "Peristiwa Klasifikasi {{name}} Per Detik"
}
}
}
diff --git a/web/public/locales/is/views/chat.json b/web/public/locales/is/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/is/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/is/views/motionSearch.json b/web/public/locales/is/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/is/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/is/views/replay.json b/web/public/locales/is/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/is/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/it/common.json b/web/public/locales/it/common.json
index 4067fe4fca..5394212350 100644
--- a/web/public/locales/it/common.json
+++ b/web/public/locales/it/common.json
@@ -221,7 +221,9 @@
"gl": "Galego (Galiziano)",
"id": "Bahasa Indonesia (Indonesiano)",
"ur": "اردو (Urdu)",
- "hr": "Hrvatski (Croato)"
+ "hr": "Hrvatski (Croato)",
+ "bs": "Bosanski (Bosniaco)",
+ "zhHant": "繁體中文 (Cinese Tradizionale)"
},
"darkMode": {
"label": "Modalità scura",
@@ -258,7 +260,7 @@
"label": "Documentazione di Frigate"
},
"restart": "Riavvia Frigate",
- "review": "Rivedi",
+ "review": "Revisiona",
"explore": "Esplora",
"export": "Esporta",
"uiPlayground": "Interfaccia area prove",
@@ -275,11 +277,12 @@
"classification": "Classificazione",
"chat": "Chat",
"profiles": "Profili",
- "actions": "Azioni"
+ "actions": "Azioni",
+ "features": "Caratteristiche"
},
"pagination": {
"next": {
- "title": "Successiva",
+ "title": "Successivo",
"label": "Vai alla pagina successiva"
},
"previous": {
@@ -292,8 +295,8 @@
"role": {
"title": "Ruolo",
"admin": "Amministratore",
- "viewer": "Spettatore",
- "desc": "Gli amministratori hanno accesso completo a tutte le funzionalità dell'interfaccia utente di Frigate. Gli spettatori possono visualizzare solo le telecamere, gli elementi di revisione e i filmati storici nell'interfaccia utente."
+ "viewer": "Visualizzatore",
+ "desc": "Gli amministratori hanno accesso completo a tutte le funzionalità dell'interfaccia utente di Frigate. I visualizzatori possono visualizzare solo le telecamere, gli elementi di revisione e i filmati storici nell'interfaccia utente."
},
"accessDenied": {
"desc": "Non hai i permessi per visualizzare questa pagina.",
@@ -331,5 +334,8 @@
"internalID": "L'ID interno che Frigate utilizza nella configurazione e nel database"
},
"no_items": "Nessun elemento",
- "validation_errors": "Errori di convalida"
+ "validation_errors": "Errori di convalida",
+ "credentialField": {
+ "savedPlaceholder": "Salvato — lascia vuoto per mantenere aggiornato"
+ }
}
diff --git a/web/public/locales/it/components/camera.json b/web/public/locales/it/components/camera.json
index 29ee897f36..3188fea87d 100644
--- a/web/public/locales/it/components/camera.json
+++ b/web/public/locales/it/components/camera.json
@@ -36,7 +36,7 @@
"label": "Trasmissione continua",
"desc": {
"warning": "La trasmissione continua può causare un elevato utilizzo di larghezza di banda e problemi di prestazioni. Da usare con cautela.",
- "title": "L'immagine della telecamera sarà sempre trasmessa dal vivo quando è visibile sulla schermata, anche se non viene rilevata alcuna attività."
+ "title": "L'immagine della telecamera sarà sempre trasmessa dal vivo quando è visibile sul cruscotto, anche se non viene rilevata alcuna attività."
}
},
"noStreaming": {
@@ -57,7 +57,7 @@
}
},
"audioIsUnavailable": "L'audio non è disponibile per questo flusso",
- "desc": "Modifica le opzioni di trasmissione dal vivo per la schermata di questo gruppo di telecamere. Queste impostazioni sono specifiche del dispositivo/browser. ",
+ "desc": "Modifica le opzioni di trasmissione dal vivo per il cruscotto di questo gruppo di telecamere. Queste impostazioni sono specifiche del dispositivo/browser. ",
"stream": "Flusso",
"placeholder": "Scegli un flusso"
},
@@ -68,7 +68,10 @@
"label": "Telecamere"
},
"icon": "Icona",
- "success": "Il gruppo di telecamere ({{name}}) è stato salvato."
+ "success": "Il gruppo di telecamere ({{name}}) è stato salvato.",
+ "showAll": "Mostra tutti i gruppi di telecamere",
+ "showLess": "Mostra meno",
+ "editGroups": "Modifica gruppi di telecamere"
},
"debug": {
"options": {
diff --git a/web/public/locales/it/components/dialog.json b/web/public/locales/it/components/dialog.json
index d0b09d38cd..1cf05d4137 100644
--- a/web/public/locales/it/components/dialog.json
+++ b/web/public/locales/it/components/dialog.json
@@ -64,15 +64,28 @@
"toast": {
"success": "Esportazione avviata correttamente. Visualizza il file nella pagina delle esportazioni.",
"error": {
- "failed": "Impossibile avviare l'esportazione: {{error}}",
+ "failed": "Impossibile mettere in coda l'esportazione: {{error}}",
"endTimeMustAfterStartTime": "L'ora di fine deve essere successiva all'ora di inizio",
"noVaildTimeSelected": "Nessun intervallo di tempo valido selezionato"
},
- "view": "Visualizzazione"
+ "view": "Visualizzazione",
+ "queued": "Esportazione in coda. Visualizza lo stato di avanzamento nella pagina delle esportazioni.",
+ "batchSuccess_one": "Avviata 1 esportazione. Apertura del caso in corso.",
+ "batchSuccess_many": "Avviate {{count}} esportazioni. Apertura del caso in corso.",
+ "batchSuccess_other": "Avviate {{count}} esportazioni. Apertura del caso in corso.",
+ "batchPartial": "Avviate {{successful}} esportazioni su un totale di {{total}}. Telecamere non riuscite: {{failedCameras}}",
+ "batchFailed": "Impossibile avviare {{total}} esportazioni. Telecamere non riuscite: {{failedCameras}}",
+ "batchQueuedSuccess_one": "1 esportazione in coda. Apertura del caso in corso.",
+ "batchQueuedSuccess_many": "Sono in coda {{count}} esportazioni. Apertura del caso in corso.",
+ "batchQueuedSuccess_other": "Sono in coda {{count}} esportazioni. Apertura del caso in corso.",
+ "batchQueuedPartial": "In coda {{successful}} di {{total}} esportazioni. Telecamere non riuscite: {{failedCameras}}",
+ "batchQueueFailed": "Impossibile mettere in coda {{total}} esportazioni. Telecamere non riuscite: {{failedCameras}}"
},
"fromTimeline": {
"saveExport": "Salva esportazione",
- "previewExport": "Anteprima esportazione"
+ "previewExport": "Anteprima esportazione",
+ "queueingExport": "Accodamento per l'esportazione...",
+ "useThisRange": "Utilizza questa intervallo"
},
"select": "Seleziona",
"name": {
@@ -80,7 +93,55 @@
},
"case": {
"label": "Caso",
- "placeholder": "Seleziona un caso"
+ "placeholder": "Seleziona un caso",
+ "newCaseOption": "Crea un nuovo caso",
+ "newCaseNamePlaceholder": "Nuovo nome del caso",
+ "newCaseDescriptionPlaceholder": "Descrizione del caso",
+ "nonAdminHelp": "Per queste esportazioni verrà creato un nuovo caso."
+ },
+ "queueing": "Accodamento per l'esportazione...",
+ "tabs": {
+ "export": "Telecamera singola",
+ "multiCamera": "Multicamera"
+ },
+ "multiCamera": {
+ "timeRange": "Intervallo di tempo",
+ "selectFromTimeline": "Seleziona dalla cronologia",
+ "cameraSelection": "Telecamere",
+ "cameraSelectionHelp": "Le telecamere con oggetti tracciati in questo intervallo di tempo sono preselezionate",
+ "checkingActivity": "Controllo dell'attività della telecamera...",
+ "noCameras": "Nessuna telecamera disponibile",
+ "detectionCount_one": "1 oggetto tracciato",
+ "detectionCount_many": "{{count}} oggetti tracciati",
+ "detectionCount_other": "{{count}} oggetti tracciati",
+ "nameLabel": "Nome di esportazione",
+ "namePlaceholder": "Nome base facoltativo per queste esportazioni",
+ "queueingButton": "Accodamento delle esportazioni...",
+ "exportButton_one": "Esporta 1 telecamera",
+ "exportButton_many": "Esporta {{count}} telecamere",
+ "exportButton_other": "Esporta {{count}} telecamere"
+ },
+ "multi": {
+ "title_one": "Esporta 1 revisione",
+ "title_many": "Esporta {{count}} revisioni",
+ "title_other": "Esporta {{count}} revisioni",
+ "description": "Esporta ogni revisione selezionata. Tutte le esportazioni saranno raggruppate in un unico caso.",
+ "descriptionNoCase": "Esporta ogni revisione selezionata.",
+ "caseNamePlaceholder": "Esporta revisione - {{date}}",
+ "exportButton_one": "Esporta 1 revisione",
+ "exportButton_many": "Esporta {{count}} revisioni",
+ "exportButton_other": "Esporta {{count}} revisioni",
+ "exportingButton": "Esportazione...",
+ "toast": {
+ "started_one": "Avviata 1 esportazione. Apertura del caso in corso.",
+ "started_many": "Avviate {{count}} esportazioni. Apertura del caso in corso.",
+ "started_other": "Avviate {{count}} esportazioni. Apertura del caso in corso.",
+ "startedNoCase_one": "Avviata 1 esportazione.",
+ "startedNoCase_many": "Avviate {{count}} esportazioni.",
+ "startedNoCase_other": "Avviate {{count}} esportazioni.",
+ "partial": "Avviate {{successful}} esportazioni su un totale di {{total}}. Fallite: {{failedItems}}",
+ "failed": "Impossibile avviare {{total}} esportazioni. Errori: {{failedItems}}"
+ }
}
},
"streaming": {
@@ -128,6 +189,14 @@
"success": "Il filmato associato agli elementi di recensione selezionati è stato eliminato correttamente.",
"error": "Impossibile eliminare: {{error}}"
}
+ },
+ "shareTimestamp": {
+ "label": "Condividi orario",
+ "title": "Condividi orario",
+ "description": "Condividi un URL con l'orario della posizione attuale del lettore oppure scegli un orario personalizzato. Tieni presente che questo URL non è pubblico ed è accessibile solo agli utenti che hanno accesso a Frigate e a questa telecamera.",
+ "custom": "Orario personalizzato",
+ "button": "URL dell'orario di condivisione",
+ "shareTitle": "Orario revisione Frigate: {{camera}}"
}
},
"imagePicker": {
diff --git a/web/public/locales/it/components/player.json b/web/public/locales/it/components/player.json
index 2aee1a7815..0937860483 100644
--- a/web/public/locales/it/components/player.json
+++ b/web/public/locales/it/components/player.json
@@ -4,7 +4,8 @@
"noPreviewFoundFor": "Nessuna anteprima trovata per {{cameraName}}",
"submitFrigatePlus": {
"title": "Vuoi inviare questo fotogramma a Frigate+?",
- "submit": "Invia"
+ "submit": "Invia",
+ "previewError": "Impossibile caricare l'anteprima dell'istantanea. La registrazione potrebbe non essere disponibile al momento."
},
"livePlayerRequiredIOSVersion": "Per questo tipo di trasmissione dal vivo è richiesto iOS 17.1 o versione successiva.",
"stats": {
@@ -47,5 +48,6 @@
"submitFrigatePlusFailed": "Impossibile inviare il fotogramma a Frigate+"
}
},
- "cameraDisabled": "La telecamera è disattivata"
+ "cameraDisabled": "La telecamera è disabilita",
+ "cameraOff": "La telecamera è spenta"
}
diff --git a/web/public/locales/it/config/cameras.json b/web/public/locales/it/config/cameras.json
index 491b69052e..98149073c7 100644
--- a/web/public/locales/it/config/cameras.json
+++ b/web/public/locales/it/config/cameras.json
@@ -2,30 +2,956 @@
"label": "Configurazione telecamera",
"name": {
"label": "Nome telecamera",
- "description": "Il nome della telecamera è necessario"
+ "description": "Il nome della telecamera è obbligatorio"
},
"friendly_name": {
- "description": "Nome amichevole della telecamera utilizzato nell'interfaccia utente di Frigate",
- "label": "Nome amichevole"
+ "description": "Nome descrittivo della telecamera utilizzato nell'interfaccia utente di Frigate",
+ "label": "Nome descrittivo"
},
"enabled": {
- "label": "Abilitato",
- "description": "Abilitato"
+ "label": "Abilitata",
+ "description": "Abilitata"
},
"audio": {
- "label": "Eventi audio",
+ "label": "Rilevamento audio",
"description": "Impostazioni per il rilevamento di eventi audio per questa telecamera.",
"enabled": {
"label": "Abilita il rilevamento audio",
"description": "Abilita o disabilita il rilevamento degli eventi audio per questa telecamera."
},
"min_volume": {
- "label": "Volume minimo"
+ "label": "Volume minimo",
+ "description": "È richiesta una soglia minima di volume RMS per eseguire il rilevamento audio; valori inferiori aumentano la sensibilità (ad esempio, 200 alta, 500 media, 1000 bassa)."
+ },
+ "max_not_heard": {
+ "label": "Fine pausa",
+ "description": "Numero di secondi senza il tipo di audio configurato prima che l'evento audio termini."
+ },
+ "listen": {
+ "label": "Tipi di ascolto",
+ "description": "Elenco dei tipi di eventi audio da rilevare (ad esempio: abbaio, allarme antincendio, urlo, parlato, grido)."
+ },
+ "filters": {
+ "label": "Filtri audio",
+ "description": "Impostazioni di filtro per ciascun tipo di audio, come le soglie di confidenza utilizzate per ridurre i falsi positivi.",
+ "threshold": {
+ "label": "Affidabilità audio minima",
+ "description": "Soglia minima di fiducia affinché l'evento audio venga conteggiato."
+ }
+ },
+ "enabled_in_config": {
+ "label": "Stato audio originale",
+ "description": "Indica se il rilevamento audio era originariamente abilitato nel file di configurazione statico."
+ },
+ "num_threads": {
+ "label": "Processi di rilevamento",
+ "description": "Numero di processi da utilizzare per l'elaborazione del rilevamento audio."
}
},
"ffmpeg": {
"path": {
- "label": "Percorso FFmpeg"
+ "label": "Percorso FFmpeg",
+ "description": "Percorso del file binario FFmpeg da utilizzare o alias di versione (\"5.0\" o \"7.0\")."
+ },
+ "label": "FFmpeg",
+ "hwaccel_args": {
+ "label": "Argomenti di accelerazione hardware",
+ "description": "Argomenti di accelerazione hardware per FFmpeg. Si consiglia di utilizzare preimpostazioni specifiche del provider."
+ },
+ "inputs": {
+ "hwaccel_args": {
+ "label": "Argomenti di accelerazione hardware",
+ "description": "Argomenti di accelerazione hardware per questo flusso di ingresso."
+ },
+ "global_args": {
+ "label": "Argomenti globali di FFmpeg",
+ "description": "Argomenti globali di FFmpeg per questo flusso di ingresso."
+ },
+ "input_args": {
+ "label": "Argomenti di ingresso",
+ "description": "Inserire gli argomenti specifici per questo flusso."
+ },
+ "label": "Ingressi della telecamera",
+ "description": "Elenco delle definizioni dei flussi di ingresso (percorsi e ruoli) per questa telecamera.",
+ "path": {
+ "label": "Percorso di ingresso",
+ "description": "URL o percorso del flusso di ingresso della telecamera."
+ },
+ "roles": {
+ "label": "Ruoli di ingresso",
+ "description": "Ruoli per questo flusso di ingresso."
+ }
+ },
+ "gpu": {
+ "description": "Indice GPU predefinito utilizzato per l'accelerazione hardware, se disponibile.",
+ "label": "Indice GPU"
+ },
+ "description": "Impostazioni di FFmpeg, inclusi percorso binario, argomenti, opzioni hwaccel e argomenti di output per ruolo.",
+ "global_args": {
+ "label": "Argomenti globali di FFmpeg",
+ "description": "Argomenti globali passati ai processi FFmpeg."
+ },
+ "input_args": {
+ "label": "Argomenti di ingresso",
+ "description": "Argomenti di ingresso applicati ai flussi di ingresso di FFmpeg."
+ },
+ "output_args": {
+ "label": "Argomenti di uscita",
+ "description": "Argomenti di uscita predefiniti utilizzati per i diversi ruoli di FFmpeg, come rilevamento e registrazione.",
+ "detect": {
+ "label": "Rileva gli argomenti di uscita",
+ "description": "Argomenti di uscita predefiniti per il rilevamento dei flussi di ruolo."
+ },
+ "record": {
+ "label": "Registra gli argomenti di uscita",
+ "description": "Argomenti di uscita predefiniti per i flussi del ruolo di registrazione."
+ }
+ },
+ "retry_interval": {
+ "label": "Tempo di ripetizione FFmpeg",
+ "description": "Secondi di attesa prima di tentare di riconnettere un flusso video della telecamera dopo un errore. Il valore predefinito è 10."
+ },
+ "apple_compatibility": {
+ "label": "Compatibilità Apple",
+ "description": "Attiva l'aggiunta di tag HEVC per una migliore compatibilità con i lettori Apple durante la registrazione in formato H.265."
}
+ },
+ "audio_transcription": {
+ "label": "Trascrizione audio",
+ "description": "Impostazioni per la trascrizione audio in tempo reale e del parlato utilizzata per eventi e sottotitoli in tempo reale.",
+ "enabled": {
+ "label": "Abilita la trascrizione",
+ "description": "Abilita o disabilita la trascrizione manuale degli eventi audio."
+ },
+ "enabled_in_config": {
+ "label": "Stato di trascrizione originale"
+ },
+ "live_enabled": {
+ "label": "Trascrizione dal vivo",
+ "description": "Abilita la trascrizione in diretta dell'audio non appena viene ricevuto."
+ }
+ },
+ "mqtt": {
+ "label": "MQTT",
+ "description": "Impostazioni di pubblicazione delle immagini via MQTT.",
+ "enabled": {
+ "label": "Invia immagine",
+ "description": "Abilita la pubblicazione delle istantanee dell'immagine per gli oggetti sui topic MQTT per questa telecamera."
+ },
+ "timestamp": {
+ "label": "Aggiungi timestamp",
+ "description": "Sovraimprime un timestamp sulle immagini pubblicate su MQTT."
+ },
+ "bounding_box": {
+ "label": "Aggiungi riquadro di delimitazione",
+ "description": "Disegna i riquadri di delimitazione sulle immagini pubblicate tramite MQTT."
+ },
+ "crop": {
+ "label": "Ritaglia immagine",
+ "description": "Ritaglia le immagini pubblicate su MQTT in base al riquadro di delimitazione dell'oggetto rilevato."
+ },
+ "height": {
+ "label": "Altezza immagine",
+ "description": "Altezza (in pixel) a cui ridimensionare le immagini pubblicate tramite MQTT."
+ },
+ "required_zones": {
+ "label": "Zone richieste",
+ "description": "Zone in cui un oggetto deve entrare affinché venga pubblicata un'immagine MQTT."
+ },
+ "quality": {
+ "label": "Qualità JPEG",
+ "description": "Qualità JPEG per le immagini pubblicate su MQTT (0-100)."
+ }
+ },
+ "onvif": {
+ "tls_insecure": {
+ "label": "Disabilita verifica TLS",
+ "description": "Salta la verifica TLS e disabilita l'autenticazione digest per ONVIF (non sicuro; utilizzare solo in reti sicure)."
+ },
+ "profile": {
+ "label": "Profilo ONVIF",
+ "description": "Profilo multimediale ONVIF specifico da utilizzare per il controllo PTZ, identificato tramite token o nome. Se non impostato, viene selezionato automaticamente il primo profilo con una configurazione PTZ valida."
+ },
+ "autotracking": {
+ "label": "Tracciamento automatico",
+ "enabled": {
+ "label": "Abilita il tracciamento automatico",
+ "description": "Abilita o disabilita il tracciamento automatico della telecamera PTZ per gli oggetti rilevati."
+ },
+ "calibrate_on_startup": {
+ "label": "Calibra all'avvio",
+ "description": "Misura la velocità dei motori PTZ all'avvio per migliorare la precisione del tracciamento. Frigate aggiornerà la configurazione con i valori di movement_weights dopo la calibrazione."
+ },
+ "zooming": {
+ "label": "Modalità ingrandimento",
+ "description": "Controlla il comportamento dello zoom: disabled (solo pan/tilt), absolute (più compatibile) o relative (pan/tilt/zoom simultanei)."
+ },
+ "zoom_factor": {
+ "label": "Fattore di ingrandimento",
+ "description": "Controlla il livello di zoom sugli oggetti tracciati. Valori più bassi mantengono una porzione maggiore della scena visibile; valori più alti aumentano lo zoom ma potrebbero far perdere il tracciamento. Valori compresi tra 0.1 e 0.75."
+ },
+ "track": {
+ "label": "Oggetti tracciati",
+ "description": "Elenco dei tipi di oggetto che dovrebbero attivare il tracciamento automatico."
+ },
+ "required_zones": {
+ "label": "Zone richieste",
+ "description": "Gli oggetti devono entrare in una di queste zone prima che inizi il tracciamento automatico."
+ },
+ "timeout": {
+ "label": "Scadenza di ritorno",
+ "description": "Attendi questo numero di secondi dopo aver perso il tracciamento prima di riportare la telecamera nella posizione preimpostata."
+ },
+ "movement_weights": {
+ "description": "Valori di calibrazione generati automaticamente dalla calibrazione della telecamera. Non modificare manualmente.",
+ "label": "Pesi del movimento"
+ },
+ "enabled_in_config": {
+ "label": "Stato originale del tracciamento automatico",
+ "description": "Campo interno per tracciare se il tracciamento automatico era abilitato nella configurazione."
+ },
+ "description": "Traccia automaticamente gli oggetti in movimento e li mantiene centrati nell'inquadratura utilizzando i movimenti della telecamera PTZ.",
+ "return_preset": {
+ "label": "Preset di ritorno",
+ "description": "Nome del preset ONVIF configurato nel firmware della telecamera a cui ritornare al termine del tracciamento."
+ }
+ },
+ "ignore_time_mismatch": {
+ "label": "Ignora la discrepanza oraria",
+ "description": "Ignora le differenze di sincronizzazione oraria tra la telecamera e il server Frigate per la comunicazione ONVIF."
+ },
+ "label": "ONVIF",
+ "port": {
+ "label": "Porta ONVIF",
+ "description": "Numero di porta per il servizio ONVIF."
+ },
+ "description": "Impostazioni della connessione ONVIF e del tracciamento automatico PTZ per questa telecamera.",
+ "host": {
+ "label": "Host ONVIF",
+ "description": "Host (e schema opzionale) per il servizio ONVIF di questa telecamera."
+ },
+ "user": {
+ "label": "Nome utente ONVIF",
+ "description": "Nome utente per l'autenticazione ONVIF; alcuni dispositivi richiedono un utente amministratore per l'ONVIF."
+ },
+ "password": {
+ "label": "Password ONVIF",
+ "description": "Password per l'autenticazione ONVIF."
+ }
+ },
+ "detect": {
+ "label": "Rilevamento oggetti",
+ "description": "Impostazioni per il ruolo di rilevamento/rilevamento utilizzato per eseguire il rilevamento degli oggetti e inizializzare i localizzatori.",
+ "enabled": {
+ "label": "Abilita il rilevamento degli oggetti",
+ "description": "Abilita o disabilita il rilevamento degli oggetti per questa telecamera."
+ },
+ "height": {
+ "label": "Rileva altezza",
+ "description": "Altezza (in pixel) dei fotogrammi utilizzati per il flusso di rilevamento; lascia vuoto per utilizzare la risoluzione nativa del flusso."
+ },
+ "width": {
+ "label": "Rileva larghezza",
+ "description": "Larghezza (in pixel) dei fotogrammi utilizzati per il flusso di rilevamento; lascia vuoto per utilizzare la risoluzione nativa del flusso."
+ },
+ "fps": {
+ "label": "Rileva FPS",
+ "description": "Numero di fotogrammi al secondo desiderati per eseguire il rilevamento; valori inferiori riducono l'utilizzo della CPU (il valore consigliato è 5, impostarne uno superiore - al massimo 10 - solo se si devono tracciare oggetti in movimento estremamente rapidi)."
+ },
+ "min_initialized": {
+ "label": "Frame di inizializzazione minimi",
+ "description": "Numero di rilevamenti consecutivi necessari prima di creare un oggetto tracciato. Aumenta questo valore per ridurre le inizializzazioni errate. Il valore predefinito è FPS diviso per 2."
+ },
+ "max_disappeared": {
+ "label": "Numero di fotogrammi scomparsi",
+ "description": "Numero di fotogrammi senza rilevamento prima che un oggetto tracciato venga considerato scomparso."
+ },
+ "stationary": {
+ "label": "Configurazione degli oggetti stazionari",
+ "description": "Impostazioni per rilevare e gestire gli oggetti che rimangono fermi per un certo periodo di tempo.",
+ "interval": {
+ "label": "Intervallo stazionario",
+ "description": "Con quale frequenza (in fotogrammi) eseguire un controllo di rilevamento per confermare che l'oggetto sia stazionario."
+ },
+ "threshold": {
+ "label": "Soglia stazionaria",
+ "description": "Numero di fotogrammi senza cambio di posizione necessari per contrassegnare un oggetto come stazionario."
+ },
+ "max_frames": {
+ "label": "Fotogrammi massimi",
+ "description": "Limita la durata del tracciamento degli oggetti statici prima che vengano scartati.",
+ "default": {
+ "description": "Numero massimo predefinito di fotogrammi per seguire un oggetto stazionario prima di interrompere la ripresa.",
+ "label": "Fotogrammi massimi predefiniti"
+ },
+ "objects": {
+ "label": "Fotogrammi massimi oggetto",
+ "description": "Opzioni di sovrascrittura per singolo oggetto relative al numero massimo di fotogrammi necessari per tracciare oggetti statici."
+ }
+ },
+ "classifier": {
+ "label": "Abilita il classificatore visivo",
+ "description": "Utilizza un classificatore visivo per rilevare oggetti realmente stazionari anche quando i riquadri di delimitazione sono instabili."
+ }
+ },
+ "annotation_offset": {
+ "label": "Differenza annotazione",
+ "description": "Millisecondi per spostare le annotazioni di rilevamento dello spostamento al fine di allineare meglio i riquadri di delimitazione della cronologia con le registrazioni; può essere positivo o negativo."
+ }
+ },
+ "face_recognition": {
+ "label": "Riconoscimento facciale",
+ "description": "Impostazioni per il rilevamento e il riconoscimento facciale di questa telecamera.",
+ "enabled": {
+ "label": "Abilita il riconoscimento facciale",
+ "description": "Attiva o disattiva il riconoscimento facciale."
+ },
+ "min_area": {
+ "label": "Area minima del viso",
+ "description": "Area minima (in pixel) del riquadro del volto rilevato necessaria per tentare il riconoscimento."
+ }
+ },
+ "review": {
+ "label": "Revisione",
+ "description": "Impostazioni che controllano gli avvisi, i rilevamenti e i riepiloghi di revisione GenAI utilizzati dall'interfaccia utente (UI) e dall'archiviazione per questa telecamera.",
+ "alerts": {
+ "label": "Configurazione avvisi",
+ "description": "Impostazioni relative a quali oggetti tracciati generano avvisi e alle modalità di conservazione degli avvisi stessi.",
+ "enabled": {
+ "label": "Abilita avvisi",
+ "description": "Abilita o disabilita la generazione di avvisi per questa telecamera."
+ },
+ "labels": {
+ "label": "Etichette avvisi",
+ "description": "Elenco delle etichette oggetto che si qualificano come avvisi (ad esempio: car, person)."
+ },
+ "required_zones": {
+ "label": "Zone richieste",
+ "description": "Zone in cui un oggetto deve entrare per essere considerato un avviso; lascia vuoto per consentire qualsiasi zona."
+ },
+ "enabled_in_config": {
+ "label": "Stato avvisi originale",
+ "description": "Traccia se gli avvisi erano originariamente abilitati nella configurazione statica."
+ },
+ "cutoff_time": {
+ "label": "Tempo limite avvisi",
+ "description": "Secondi da attendere dopo l'assenza di attività che genera avvisi prima di interrompere un avviso."
+ }
+ },
+ "detections": {
+ "label": "Configurazione rilevamenti",
+ "description": "Impostazioni relative a quali oggetti tracciati generano rilevamenti (non avvisi) e alle modalità di conservazione dei rilevamenti stessi.",
+ "enabled": {
+ "label": "Abilita rilevamenti",
+ "description": "Abilita o disabilita gli eventi di rilevamento per questa telecamera."
+ },
+ "labels": {
+ "label": "Etichette rilevamenti",
+ "description": "Elenco delle etichette oggetto che si qualificano come eventi di rilevamento."
+ },
+ "required_zones": {
+ "label": "Zone richieste",
+ "description": "Zone in cui un oggetto deve entrare per essere considerato un rilevamento; lascia vuoto per consentire qualsiasi zona."
+ },
+ "cutoff_time": {
+ "label": "Tempo limite rilevamenti",
+ "description": "Secondi da attendere dopo l'assenza di attività che genera rilevamenti prima di interrompere un rilevamento."
+ },
+ "enabled_in_config": {
+ "label": "Stato rilevamenti originale",
+ "description": "Traccia se i rilevamenti erano originariamente abilitati nella configurazione statica."
+ }
+ },
+ "genai": {
+ "label": "Configurazione GenAI",
+ "description": "Controlla l'uso dell'IA generativa per la produzione di descrizioni e riepiloghi degli elementi di revisione.",
+ "enabled": {
+ "label": "Abilita descrizioni GenAI",
+ "description": "Abilita o disabilita le descrizioni e i riepiloghi generati dalla GenAI per gli elementi di revisione."
+ },
+ "alerts": {
+ "label": "Abilita GenAI per avvisi",
+ "description": "Usa la GenAI per generare descrizioni per gli elementi di avviso."
+ },
+ "detections": {
+ "label": "Abilita GenAI per rilevamenti",
+ "description": "Usa la GenAI per generare descrizioni per gli elementi di rilevamento."
+ },
+ "image_source": {
+ "label": "Fonte immagini di revisione",
+ "description": "Fonte delle immagini inviate alla GenAI ('anteprime' o 'registrazioni'); 'registrazioni' utilizza fotogrammi di qualità superiore ma consuma più token."
+ },
+ "additional_concerns": {
+ "label": "Ulteriori criteri di attenzione",
+ "description": "Un elenco di note o criteri di attenzione aggiuntivi che la GenAI deve considerare quando valuta l'attività su questa telecamera."
+ },
+ "debug_save_thumbnails": {
+ "label": "Salva miniature",
+ "description": "Salva le miniature inviate al provider GenAI per il debug e la revisione."
+ },
+ "enabled_in_config": {
+ "label": "Stato GenAI originale",
+ "description": "Traccia se la revisione GenAI era originariamente abilitata nella configurazione statica."
+ },
+ "preferred_language": {
+ "label": "Lingua preferita",
+ "description": "Lingua preferita da richiedere al provider GenAI per le risposte generate."
+ },
+ "activity_context_prompt": {
+ "label": "Prompt di contesto dell'attività",
+ "description": "Prompt personalizzato che descrive cosa costituisce o meno un'attività sospetta, per fornire contesto ai riepiloghi della GenAI."
+ }
+ }
+ },
+ "profiles": {
+ "label": "Profili",
+ "description": "Profili di configurazione denominati con sovrascritture parziali che possono essere attivati in fase di esecuzione."
+ },
+ "record": {
+ "label": "Registrazione",
+ "export": {
+ "description": "Impostazioni utilizzate durante l'esportazione delle registrazioni come timelapse e accelerazione hardware.",
+ "hwaccel_args": {
+ "description": "Argomenti di accelerazione hardware da utilizzare per le operazioni di esportazione/transcodifica.",
+ "label": "Argomenti hwaccel esportazione"
+ },
+ "label": "Configurazione esportazione",
+ "max_concurrent": {
+ "label": "Esportazioni simultanee massime",
+ "description": "Numero massimo di processi di esportazione da elaborare contemporaneamente."
+ }
+ },
+ "description": "Impostazioni di registrazione e conservazione per questa telecamera.",
+ "enabled": {
+ "label": "Abilita registrazione",
+ "description": "Attiva o disattiva la registrazione per questa telecamera."
+ },
+ "expire_interval": {
+ "label": "Intervallo di pulizia registrazioni",
+ "description": "Minuti tra i cicli di pulizia che rimuovono i segmenti di registrazione scaduti."
+ },
+ "continuous": {
+ "label": "Conservazione continua",
+ "description": "Numero di giorni per cui conservare le registrazioni, indipendentemente dagli oggetti tracciati o dal movimento. Impostare su 0 se si desidera conservare solo le registrazioni relative agli avvisi e ai rilevamenti.",
+ "days": {
+ "label": "Giorni di conservazione",
+ "description": "Numero di giorni di conservazione delle registrazioni."
+ }
+ },
+ "motion": {
+ "label": "Conservazione movimento",
+ "description": "Numero di giorni per cui conservare le registrazioni attivate dal movimento, indipendentemente dagli oggetti tracciati. Impostare su 0 se si desidera conservare solo le registrazioni relative ad allarmi e rilevamenti.",
+ "days": {
+ "label": "Giorni di conservazione",
+ "description": "Numero di giorni di conservazione delle registrazioni."
+ }
+ },
+ "detections": {
+ "label": "Conservazione rilevamento",
+ "description": "Impostazioni relative alla conservazione delle registrazioni per gli eventi di rilevamento, comprese le durate prima e dopo l'acquisizione.",
+ "pre_capture": {
+ "label": "Secondi prima della cattura",
+ "description": "Numero di secondi precedenti all'evento di rilevamento da includere nella registrazione."
+ },
+ "post_capture": {
+ "label": "Secondi dopo la cattura",
+ "description": "Numero di secondi successivi all'evento di rilevamento da includere nella registrazione."
+ },
+ "retain": {
+ "label": "Conservazione eventi",
+ "description": "Impostazioni di conservazione per le registrazioni degli eventi di rilevamento.",
+ "days": {
+ "label": "Giorni di conservazione",
+ "description": "Numero di giorni per cui conservare le registrazioni degli eventi di rilevamento."
+ },
+ "mode": {
+ "label": "Modalità di conservazione",
+ "description": "Modalità di conservazione: tutti (salva tutti i segmenti), movimento (salva i segmenti con movimento) o oggetti_attivi (salva i segmenti con oggetti attivi)."
+ }
+ }
+ },
+ "alerts": {
+ "label": "Conservazione avvisi",
+ "description": "Impostazioni di conservazione delle registrazioni per gli eventi di avviso, incluse le durate prima e dopo la cattura.",
+ "pre_capture": {
+ "label": "Secondi prima della cattura",
+ "description": "Numero di secondi precedenti all'evento di rilevamento da includere nella registrazione."
+ },
+ "post_capture": {
+ "label": "Secondi dopo la cattura",
+ "description": "Numero di secondi successivi all'evento di rilevamento da includere nella registrazione."
+ },
+ "retain": {
+ "label": "Conservazione eventi",
+ "description": "Impostazioni di conservazione per le registrazioni degli eventi di rilevamento.",
+ "days": {
+ "label": "Giorni di conservazione",
+ "description": "Numero di giorni per cui conservare le registrazioni degli eventi di rilevamento."
+ },
+ "mode": {
+ "label": "Modalità di conservazione",
+ "description": "Modalità di conservazione: tutti (salva tutti i segmenti), movimento (salva i segmenti con movimento) o oggetti_attivi (salva i segmenti con oggetti attivi)."
+ }
+ }
+ },
+ "preview": {
+ "label": "Configurazione anteprima",
+ "description": "Impostazioni che controllano la qualità delle anteprime di registrazione mostrate nell'interfaccia utente (UI).",
+ "quality": {
+ "label": "Qualità anteprima",
+ "description": "Livello di qualità dell'anteprima (very_low, low, medium, high, very_high)."
+ }
+ },
+ "enabled_in_config": {
+ "label": "Stato registrazione originale",
+ "description": "Indica se la registrazione era abilitata nella configurazione statica originale."
+ }
+ },
+ "snapshots": {
+ "label": "Istantanee",
+ "description": "Impostazioni per le istantanee generate via API degli oggetti tracciati per questa telecamera.",
+ "enabled": {
+ "label": "Abilita istantanee",
+ "description": "Abilita o disabilita il salvataggio delle istantanee per questa telecamera."
+ },
+ "timestamp": {
+ "label": "Sovrimpressione timestamp",
+ "description": "Sovraimprime un timestamp sulle istantanee provenienti dalle API."
+ },
+ "bounding_box": {
+ "label": "Sovrimpressione riquadro di delimitazione",
+ "description": "Disegna i riquadri di delimitazione per gli oggetti tracciati sulle istantanee provenienti dalle API."
+ },
+ "crop": {
+ "label": "Ritaglia istantanea",
+ "description": "Ritaglia le istantanee provenienti dalle API in base al riquadro di delimitazione dell'oggetto rilevato."
+ },
+ "required_zones": {
+ "label": "Zone richieste",
+ "description": "Zone in cui un oggetto deve entrare affinché venga salvata un'istantanea."
+ },
+ "height": {
+ "label": "Altezza istantanea",
+ "description": "Altezza (in pixel) a cui ridimensionare le istantanee provenienti dalle API; lascia vuoto per mantenere la dimensione originale."
+ },
+ "retain": {
+ "label": "Conservazione istantanee",
+ "description": "Impostazioni di conservazione per le istantanee, inclusi i giorni predefiniti e le eccezioni per singolo oggetto.",
+ "default": {
+ "label": "Conservazione predefinita",
+ "description": "Numero predefinito di giorni per cui conservare le istantanee."
+ },
+ "mode": {
+ "label": "Modalità di conservazione",
+ "description": "Modalità di conservazione: tutti (salva tutti i segmenti), movimento (salva i segmenti con movimento) o oggetti_attivi (salva i segmenti con oggetti attivi)."
+ },
+ "objects": {
+ "label": "Conservazione oggetti",
+ "description": "Eccezioni per singolo oggetto relativi ai giorni di conservazione delle istantanee."
+ }
+ },
+ "quality": {
+ "label": "Qualità istantanee",
+ "description": "Qualità di codifica per le istantanee salvate (0-100)."
+ }
+ },
+ "motion": {
+ "label": "Rilevamento movimento",
+ "contour_area": {
+ "label": "Area di contorno",
+ "description": "Area minima del contorno in pixel richiesta affinché un contorno di movimento venga conteggiato."
+ },
+ "improve_contrast": {
+ "label": "Migliora il contrasto",
+ "description": "Applica un miglioramento del contrasto ai fotogrammi prima dell'analisi del movimento per facilitare il rilevamento."
+ },
+ "description": "Impostazioni predefinite per il rilevamento del movimento di questa telecamera.",
+ "enabled": {
+ "label": "Attiva il rilevamento del movimento",
+ "description": "Attiva o disattiva il rilevamento di movimento per questa telecamera."
+ },
+ "threshold": {
+ "label": "Soglia di movimento",
+ "description": "Soglia di differenza tra i pixel utilizzata dal rilevatore di movimento; valori più alti riducono la sensibilità (intervallo 1-255)."
+ },
+ "lightning_threshold": {
+ "label": "Soglia di luminosità",
+ "description": "Soglia per rilevare e ignorare brevi picchi di luminosità (un valore più basso indica una maggiore sensibilità; i valori sono compresi tra 0.3 e 1.0). Ciò non impedisce del tutto il rilevamento del movimento, ma fa semplicemente sì che il rilevatore smetta di analizzare i fotogrammi successivi una volta superata la soglia. Durante questi eventi vengono comunque create registrazioni basate sul movimento."
+ },
+ "skip_motion_threshold": {
+ "label": "Ignora soglia di movimento",
+ "description": "Se impostato su un valore compreso tra 0.0 e 110, e se in un singolo fotogramma cambia una porzione dell'immagine superiore a tale frazione, il rilevatore non restituirà alcun riquadro di movimento e si ricalibrerà immediatamente. Ciò consente di risparmiare risorse della CPU e ridurre i falsi positivi in caso di fulmini, temporali, ecc., ma potrebbe comportare la mancata rilevazione di eventi reali, come ad esempio il tracciamento automatico di un oggetto da parte di una telecamera PTZ. Il compromesso consiste nel scegliere se sacrificare alcuni megabyte di registrazioni o rivedere un paio di brevi video. Lasciare non impostato (Nessuno) per disabilitare questa funzione."
+ },
+ "delta_alpha": {
+ "description": "Fattore di fusione alfa utilizzato nel calcolo della differenza tra fotogrammi per il calcolo del movimento.",
+ "label": "Delta alfa"
+ },
+ "frame_alpha": {
+ "description": "Valore alfa utilizzato durante la fusione dei fotogrammi per la preelaborazione del movimento.",
+ "label": "Frame alfa"
+ },
+ "frame_height": {
+ "label": "Altezza del frame",
+ "description": "Altezza in pixel alla quale ridimensionare i fotogrammi durante il calcolo del movimento."
+ },
+ "mask": {
+ "label": "Coordinate della maschera",
+ "description": "Coordinate x, y ordinate che definiscono il poligono della maschera di movimento utilizzato per includere/escludere aree."
+ },
+ "mqtt_off_delay": {
+ "label": "Ritardo di disattivazione MQTT",
+ "description": "Secondi di attesa dopo l'ultimo movimento prima di pubblicare uno stato 'off' MQTT."
+ },
+ "enabled_in_config": {
+ "description": "Indica se il rilevamento del movimento era abilitato nella configurazione statica originale.",
+ "label": "Stato movimento originale"
+ },
+ "raw_mask": {
+ "label": "Maschera grezza"
+ }
+ },
+ "objects": {
+ "label": "Oggetti",
+ "description": "Impostazioni predefinite per il tracciamento degli oggetti, tra cui le etichette da tracciare e i filtri per singolo oggetto.",
+ "track": {
+ "label": "Oggetti da tracciare",
+ "description": "Elenco delle etichette degli oggetti da tracciare per questa telecamera."
+ },
+ "filters": {
+ "label": "Filtri oggetto",
+ "description": "Filtri applicati agli oggetti rilevati per ridurre i falsi positivi (area, rapporto, livello di confidenza).",
+ "min_area": {
+ "label": "Area minima dell'oggetto",
+ "description": "Area minima del riquadro di delimitazione (in pixel o percentuale) richiesta per questo tipo di oggetto. Può essere espressa in pixel (numero intero) o in percentuale (valore decimale compreso tra 0.000001 e 0.99)."
+ },
+ "max_area": {
+ "label": "Area massima dell'oggetto",
+ "description": "Area massima del riquadro di delimitazione (in pixel o percentuale) richiesta per questo tipo di oggetto. Può essere espressa in pixel (numero intero) o in percentuale (valore decimale compreso tra 0.000001 e 0.99)."
+ },
+ "min_ratio": {
+ "label": "Rapporto di aspetto minimo",
+ "description": "Rapporto minimo tra larghezza e altezza richiesto affinché il riquadro di delimitazione sia valido."
+ },
+ "max_ratio": {
+ "label": "Rapporto di aspetto massimo",
+ "description": "Rapporto massimo tra larghezza e altezza richiesto affinché il riquadro di delimitazione sia valido."
+ },
+ "threshold": {
+ "label": "Soglia di confidenza",
+ "description": "Soglia di confidenza media di rilevamento necessaria affinché l'oggetto sia considerato un vero positivo."
+ },
+ "min_score": {
+ "label": "Confidenza minima",
+ "description": "Livello minimo di confidenza nel rilevamento in un singolo fotogramma richiesto affinché l'oggetto venga conteggiato."
+ },
+ "mask": {
+ "label": "Maschera filtro",
+ "description": "Coordinate del poligono che definiscono la posizione di applicazione di questo filtro all'interno dell'inquadratura."
+ },
+ "raw_mask": {
+ "label": "Maschera grezza"
+ }
+ },
+ "mask": {
+ "label": "Maschera oggetto",
+ "description": "Poligono di mascheramento utilizzato per impedire il rilevamento di oggetti in aree specifiche."
+ },
+ "genai": {
+ "label": "Configurazione degli oggetti GenAI",
+ "description": "Opzioni GenAI per la descrizione degli oggetti tracciati e l'invio dei fotogrammi per la generazione.",
+ "enabled": {
+ "label": "Abilita GenAI",
+ "description": "Abilita in modo predefinito la generazione di descrizioni da parte di GenAI per gli oggetti tracciati."
+ },
+ "use_snapshot": {
+ "label": "Utilizza istantanee",
+ "description": "Utilizza le istantanee degli oggetti anziché le miniature per la generazione di descrizioni con GenAI."
+ },
+ "prompt": {
+ "description": "Modello di richiesta predefinito utilizzato per la generazione di descrizioni con GenAI.",
+ "label": "Richiesta di didascalia"
+ },
+ "object_prompts": {
+ "description": "Richieste specifiche per ogni oggetto per personalizzare i risultati di GenAI in base a etichette specifiche.",
+ "label": "Richiesta di oggetto"
+ },
+ "objects": {
+ "label": "Oggetti GenAI",
+ "description": "Elenco di etichette degli oggetti da inviare a GenAI per impostazione predefinita."
+ },
+ "required_zones": {
+ "label": "Zone richieste",
+ "description": "Zone che devono essere inserite affinché gli oggetti siano idonei alla generazione della descrizione GenAI."
+ },
+ "debug_save_thumbnails": {
+ "label": "Salva miniature",
+ "description": "Salva le miniature inviate a GenAI per la correzione e la revisione."
+ },
+ "send_triggers": {
+ "label": "Attivazioni GenAI",
+ "description": "Definisce quando i fotogrammi devono essere inviati a GenAI (alla fine, dopo gli aggiornamenti, ecc.).",
+ "tracked_object_end": {
+ "label": "Invia alla fine",
+ "description": "Invia una richiesta a GenAI quando l'oggetto tracciato termina."
+ },
+ "after_significant_updates": {
+ "label": "Attivazione anticipata GenAI",
+ "description": "Invia una richiesta a GenAI dopo un numero specificato di aggiornamenti significativi per l'oggetto tracciato."
+ }
+ },
+ "enabled_in_config": {
+ "label": "Stato GenAI originale",
+ "description": "Indica se GenAI era abilitata nella configurazione statica originale."
+ }
+ },
+ "raw_mask": {
+ "label": "Maschera grezza"
+ }
+ },
+ "live": {
+ "label": "Riproduzione in diretta",
+ "description": "Impostazioni utilizzate dall'interfaccia web per controllare la selezione, la risoluzione e la qualità della trasmissione Dal vivo.",
+ "streams": {
+ "label": "Nomi dei flussi Dal vivo",
+ "description": "Mappatura dei nomi dei flussi configurati ai nomi ritrasmissioni/go2rtc utilizzati per la riproduzione dal vivo."
+ },
+ "height": {
+ "label": "Altezza dal vivo",
+ "description": "Altezza (in pixel) per visualizzare il flusso dal vivo jsmpeg nell'interfaccia utente web; deve essere <= altezza del flusso rilevato."
+ },
+ "quality": {
+ "label": "Qualità dal vivo",
+ "description": "Qualità di codifica per il flusso jsmpeg (1 massima, 31 minima)."
+ }
+ },
+ "timestamp_style": {
+ "label": "Stile orario",
+ "description": "Opzioni di stile per i timestamp applicati alle istantanee e alla vista di Debug.",
+ "position": {
+ "label": "Posizione timestamp",
+ "description": "Posizione del timestamp sull'immagine (tl/tr/bl/br)."
+ },
+ "format": {
+ "label": "Formato timestamp",
+ "description": "Stringa di formato data/ora utilizzata per i timestamp (codici di formato datetime di Python)."
+ },
+ "color": {
+ "label": "Colore timestamp",
+ "description": "Valori di colore RGB per il testo del timestamp (tutti i valori da 0 a 255).",
+ "red": {
+ "label": "Rosso",
+ "description": "Componente rosso (0-255) per il colore del timestamp."
+ },
+ "green": {
+ "label": "Verde",
+ "description": "Componente verde (0-255) per il colore del timestamp."
+ },
+ "blue": {
+ "label": "Blu",
+ "description": "Componente blu (0-255) per il colore del timestamp."
+ }
+ },
+ "thickness": {
+ "label": "Spessore timestamp",
+ "description": "Spessore della linea del testo del timestamp."
+ },
+ "effect": {
+ "label": "Effetto timestamp",
+ "description": "Effetto visivo per il testo del timestamp (none, solid, shadow)."
+ }
+ },
+ "notifications": {
+ "label": "Notifiche",
+ "enabled": {
+ "label": "Abilita le notifiche",
+ "description": "Abilita o disabilita le notifiche per questa telecamera."
+ },
+ "email": {
+ "label": "Email di notifica",
+ "description": "Indirizzo email utilizzato per le notifiche push o richiesto da alcuni fornitori di servizi di notifica."
+ },
+ "cooldown": {
+ "label": "Tempo di attesa tra notifiche",
+ "description": "Tempo di attesa (in secondi) tra le notifiche per evitare di inviare spam ai destinatari."
+ },
+ "enabled_in_config": {
+ "label": "Stato delle notifiche originali",
+ "description": "Indica se le notifiche erano abilitate nella configurazione statica originale."
+ },
+ "description": "Impostazioni per abilitare e controllare le notifiche per questa telecamera."
+ },
+ "birdseye": {
+ "label": "Birdseye",
+ "description": "Impostazioni per la vista composita Birdseye che unisce più flussi video di telecamere in un unico formato.",
+ "enabled": {
+ "label": "Abilita Birdseye",
+ "description": "Abilita o disabilita la funzione di visualizzazione Birdseye."
+ },
+ "mode": {
+ "label": "Modalità di tracciamento",
+ "description": "Modalità per includere le telecamere in Birdseye: 'oggetti', 'movimento' o 'continuo'."
+ },
+ "order": {
+ "label": "Posizione",
+ "description": "Posizione numerica che controlla l'ordine delle telecamere nella disposizione Birdseye."
+ }
+ },
+ "semantic_search": {
+ "label": "Ricerca semantica",
+ "triggers": {
+ "label": "Attivatori",
+ "friendly_name": {
+ "label": "Nome descrittivo",
+ "description": "Nome descrittivo opzionale visualizzato nell'interfaccia utente per questo attivatore."
+ },
+ "description": "Azioni e criteri di corrispondenza per gli attivatori della ricerca semantica specifici della telecamera.",
+ "enabled": {
+ "label": "Abilita questo attivatore",
+ "description": "Abilita o disabilita questo attivatore della ricerca semantica."
+ },
+ "type": {
+ "label": "Tipo di attivatore",
+ "description": "Tipo di attivatore: 'miniatura' (corrispondenza con immagine) o 'descrizione' (corrispondenza con testo)."
+ },
+ "data": {
+ "label": "Contenuto dell'attivatore",
+ "description": "Frase di testo o ID della miniatura da confrontare con gli oggetti tracciati."
+ },
+ "threshold": {
+ "label": "Soglia dell'attivatore",
+ "description": "Punteggio minimo di somiglianza (0-1) richiesto per attivare questo attivatore."
+ },
+ "actions": {
+ "label": "Azioni dell'attivatore",
+ "description": "Elenco delle azioni da eseguire quando l'attivatore trova una corrispondenza (notification, sub_label, attribute)."
+ }
+ },
+ "description": "Impostazioni per la ricerca semantica, che crea e interroga gli embedding degli oggetti per trovare elementi simili."
+ },
+ "lpr": {
+ "label": "Riconoscimento targhe",
+ "description": "Impostazioni di riconoscimento delle targhe, incluse le soglie di rilevamento, la formattazione e le targhe conosciute.",
+ "enabled": {
+ "label": "Abilita riconoscimento delle targhe (LPR)",
+ "description": "Attiva o disattiva il riconoscimento delle targhe (LPR) su questa telecamera."
+ },
+ "expire_time": {
+ "label": "Scadenza in secondi",
+ "description": "Tempo in secondi trascorso dopo il quale una targa non identificata viene rimossa dal sistema di tracciamento (solo per telecamere LPR dedicate)."
+ },
+ "min_area": {
+ "label": "Area minima della targa",
+ "description": "Area minima della targa (in pixel) richiesta per tentare il riconoscimento."
+ },
+ "enhancement": {
+ "label": "Livello di miglioramento",
+ "description": "Livello di miglioramento (0-10) da applicare alle immagini delle targhe prima dell'OCR; valori più elevati potrebbero non migliorare sempre i risultati; i livelli superiori a 5 potrebbero funzionare solo con immagini di targhe scattate di notte e devono essere utilizzati con cautela."
+ }
+ },
+ "ui": {
+ "description": "Visualizza l'ordine e la visibilità di questa telecamera nell'interfaccia utente. L'ordine influisce sul cruscotto predefinito. Per un controllo più granulare, utilizza i gruppi di telecamere.",
+ "order": {
+ "description": "L'ordine numerico viene utilizzato per ordinare le telecamere nell'interfaccia utente (cruscotto ed elenchi predefiniti); i numeri più grandi compaiono successivamente.",
+ "label": "Ordine interfaccia utente"
+ },
+ "dashboard": {
+ "label": "Mostra nell'interfaccia utente",
+ "description": "Abilita o disabilita la visualizzazione di questa telecamera in ogni punto dell'interfaccia utente di Frigate. Disabilitando questa opzione, sarà necessario modificare manualmente la configurazione per visualizzare nuovamente la telecamera nell'interfaccia utente."
+ },
+ "label": "Interfaccia utente telecamera",
+ "review": {
+ "label": "Mostra in revisione",
+ "description": "Abilita o disabilita la visibilità di questa telecamera nella sezione di revisione (la pagina di revisione e il relativo filtro telecamera, la revisione del movimento e la vista della cronologia)."
+ }
+ },
+ "zones": {
+ "enabled": {
+ "label": "Abilitata",
+ "description": "Abilita o disabilita questa zona. Le zone disabilitate vengono ignorate durante l'esecuzione."
+ },
+ "label": "Zone",
+ "friendly_name": {
+ "label": "Nome zona",
+ "description": "Un nome intuitivo per la zona, visualizzato nell'interfaccia utente di Frigate. Se non specificato, verrà utilizzata una versione formattata del nome della zona."
+ },
+ "filters": {
+ "raw_mask": {
+ "label": "Maschera grezza"
+ },
+ "label": "Filtri Zona",
+ "description": "Filtri da applicare agli oggetti all'interno di questa zona. Utilizzati per ridurre i falsi positivi o limitare quali oggetti sono considerati presenti nella zona.",
+ "min_area": {
+ "label": "Area minima oggetto",
+ "description": "Area minima del riquadro di delimitazione (in pixel o percentuale) richiesta per questo tipo di oggetto. Può essere in pixel (int) o in percentuale (float compreso tra 0.000001 e 0.99)."
+ },
+ "max_area": {
+ "label": "Area massima oggetto",
+ "description": "Area massima del riquadro di delimitazione (in pixel o percentuale) richiesta per questo tipo di oggetto. Può essere espressa in pixel (numero intero) o in percentuale (valore decimale compreso tra 0.000001 e 0.99)."
+ },
+ "min_ratio": {
+ "label": "Rapporto di aspetto minimo",
+ "description": "Rapporto minimo tra larghezza e altezza richiesto affinché il riquadro di delimitazione sia valido."
+ },
+ "max_ratio": {
+ "label": "Rapporto di aspetto massimo",
+ "description": "Rapporto massimo tra larghezza e altezza richiesto affinché il riquadro di delimitazione sia valido."
+ },
+ "threshold": {
+ "label": "Soglia di confidenza",
+ "description": "Soglia di confidenza media di rilevamento necessaria affinché l'oggetto sia considerato un vero positivo."
+ },
+ "min_score": {
+ "label": "Confidenza minima",
+ "description": "Livello minimo di confidenza nel rilevamento in un singolo fotogramma richiesto affinché l'oggetto venga conteggiato."
+ },
+ "mask": {
+ "label": "Maschera filtro",
+ "description": "Coordinate del poligono che definiscono la posizione di applicazione di questo filtro all'interno dell'inquadratura."
+ }
+ },
+ "description": "Le zone consentono di definire un'area specifica dell'inquadratura per determinare se un oggetto si trova o meno all'interno di una particolare area.",
+ "enabled_in_config": {
+ "label": "Mantieni traccia dello stato originale della zona."
+ },
+ "coordinates": {
+ "label": "Coordinate",
+ "description": "Coordinate del poligono che definiscono l'area della zona. Può essere una stringa separata da virgole o un elenco di stringhe di coordinate. Le coordinate devono essere relative (0-1) o assolute (legacy)."
+ },
+ "distances": {
+ "label": "Distanze reali",
+ "description": "Distanze reali opzionali per ciascun lato del quadrilatero della zona, utilizzate per i calcoli di velocità o distanza. Deve avere esattamente 4 valori se impostato."
+ },
+ "inertia": {
+ "label": "Fotogrammi di inerzia",
+ "description": "Numero di fotogrammi consecutivi in cui un oggetto deve essere rilevato nella zona prima di essere considerato presente. Aiuta a filtrare i rilevamenti transitori."
+ },
+ "loitering_time": {
+ "label": "Secondi di stazionamento",
+ "description": "Numero di secondi per cui un oggetto deve rimanere nella zona per essere considerato in stazionamento nella stessa. Imposta a 0 per disabilitare il rilevamento dello stazionamento."
+ },
+ "speed_threshold": {
+ "label": "Velocità minima",
+ "description": "Velocità minima (in unità reali se le distanze sono impostate) richiesta affinché un oggetto sia considerato presente nella zona. Utilizzata per gli attivatori di zona basati sulla velocità."
+ },
+ "objects": {
+ "label": "Oggetti attivatori",
+ "description": "Elenco dei tipi di oggetto (dalla labelmap) che possono attivare questa zona. Può essere una stringa o un elenco di stringhe. Se vuoto, vengono considerati tutti gli oggetti."
+ }
+ },
+ "type": {
+ "description": "Tipo di telecamera",
+ "label": "Tipo di telecamera"
+ },
+ "best_image_timeout": {
+ "label": "Timeout immagine migliore",
+ "description": "Quanto tempo attendere per l'immagine con il punteggio di accuratezza più alto."
+ },
+ "webui_url": {
+ "label": "URL telecamera",
+ "description": "URL per visitare direttamente la telecamera dalla pagina di sistema"
+ },
+ "enabled_in_config": {
+ "label": "Stato telecamera originale",
+ "description": "Mantiene traccia dello stato originale della telecamera."
}
}
diff --git a/web/public/locales/it/config/global.json b/web/public/locales/it/config/global.json
index dbd4f3ec65..b8c557ce3c 100644
--- a/web/public/locales/it/config/global.json
+++ b/web/public/locales/it/config/global.json
@@ -12,24 +12,50 @@
"description": "Versione numerica o stringa della configurazione attiva per facilitare il rilevamento di migrazioni o modifiche di formato."
},
"audio": {
- "label": "Eventi audio",
+ "label": "Rilevamento audio",
"enabled": {
"label": "Abilita il rilevamento audio"
},
"min_volume": {
- "label": "Volume minimo"
+ "label": "Volume minimo",
+ "description": "È richiesta una soglia minima di volume RMS per eseguire il rilevamento audio; valori inferiori aumentano la sensibilità (ad esempio, 200 alta, 500 media, 1000 bassa)."
+ },
+ "max_not_heard": {
+ "label": "Fine pausa",
+ "description": "Numero di secondi senza il tipo di audio configurato prima che l'evento audio termini."
+ },
+ "listen": {
+ "label": "Tipi di ascolto",
+ "description": "Elenco dei tipi di eventi audio da rilevare (ad esempio: abbaio, allarme antincendio, urlo, parlato, grido)."
+ },
+ "filters": {
+ "label": "Filtri audio",
+ "description": "Impostazioni di filtro per ciascun tipo di audio, come le soglie di confidenza utilizzate per ridurre i falsi positivi.",
+ "threshold": {
+ "label": "Affidabilità audio minima",
+ "description": "Soglia minima di fiducia affinché l'evento audio venga conteggiato."
+ }
+ },
+ "enabled_in_config": {
+ "label": "Stato audio originale",
+ "description": "Indica se il rilevamento audio era originariamente abilitato nel file di configurazione statico."
+ },
+ "num_threads": {
+ "label": "Processi di rilevamento",
+ "description": "Numero di processi da utilizzare per l'elaborazione del rilevamento audio."
}
},
"logger": {
"description": "Consente di controllare il livello di dettaglio predefinito dei registri e le opzioni di sovrascrittura per ciascun componente.",
"default": {
- "label": "Livello di registrazione",
+ "label": "Livello del registro",
"description": "Livello di dettaglio predefinito del registro globale (debug, info, warning, error)."
},
"logs": {
"label": "Livello di registro per processo",
"description": "Opzioni di sovrsacrittura del livello di registro per ciascun componente, per aumentare o diminuire il livello di dettaglio dei singoli moduli."
- }
+ },
+ "label": "Registro"
},
"auth": {
"label": "Autenticazione",
@@ -41,11 +67,1187 @@
"reset_admin_password": {
"label": "Reimposta la password di amministratore",
"description": "Se la condizione è vera, reimposta la password dell'utente amministratore all'avvio e stampa la nuova password nei registri."
+ },
+ "cookie_name": {
+ "label": "Nome del cookie JWT",
+ "description": "Nome del cookie utilizzato per memorizzare il token JWT per l'autenticazione nativa."
+ },
+ "cookie_secure": {
+ "label": "Attributo dei cookie sicuri",
+ "description": "Imposta l'attributo 'sicuro' sul cookie di autenticazione; deve essere impostato su 'vero' quando si utilizza TLS."
+ },
+ "session_length": {
+ "label": "Durata della sessione",
+ "description": "Durata della sessione in secondi per le sessioni basate su JWT."
+ },
+ "refresh_time": {
+ "label": "Finestra di aggiornamento della sessione",
+ "description": "Quando una sessione sta per scadere entro questo numero di secondi, aggiornala per ripristinarne la durata completa."
+ },
+ "trusted_proxies": {
+ "label": "Proxy affidabili",
+ "description": "Elenco degli indirizzi IP proxy attendibili utilizzati per determinare l'indirizzo IP del client ai fini della limitazione della velocità."
+ },
+ "roles": {
+ "label": "Mappatura dei ruoli",
+ "description": "Associa i ruoli agli elenchi delle telecamere. Un elenco vuoto garantisce l'accesso a tutte le telecamere per quel ruolo."
+ },
+ "failed_login_rate_limit": {
+ "label": "Limiti di accesso non riusciti",
+ "description": "Regole di limitazione della frequenza per i tentativi di accesso non riusciti al fine di ridurre gli attacchi di forza bruta."
+ },
+ "hash_iterations": {
+ "description": "Numero di iterazioni PBKDF2-SHA256 da utilizzare per criptare le password utente.",
+ "label": "Iterazioni di crittografia"
+ },
+ "admin_first_time_login": {
+ "label": "Flag primo accesso amministratore",
+ "description": "Quando impostato su true, l'interfaccia utente può mostrare un link di aiuto nella pagina di login per informare gli utenti su come accedere dopo un ripristino della password di amministratore. "
}
},
"ffmpeg": {
"path": {
- "label": "Percorso FFmpeg"
+ "label": "Percorso FFmpeg",
+ "description": "Percorso del file binario FFmpeg da utilizzare o alias di versione (\"5.0\" o \"7.0\")."
+ },
+ "label": "FFmpeg",
+ "hwaccel_args": {
+ "label": "Argomenti di accelerazione hardware",
+ "description": "Argomenti di accelerazione hardware per FFmpeg. Si consiglia di utilizzare preimpostazioni specifiche del provider."
+ },
+ "inputs": {
+ "hwaccel_args": {
+ "label": "Argomenti di accelerazione hardware",
+ "description": "Argomenti di accelerazione hardware per questo flusso di ingresso."
+ },
+ "global_args": {
+ "label": "Argomenti globali di FFmpeg",
+ "description": "Argomenti globali di FFmpeg per questo flusso di ingresso."
+ },
+ "input_args": {
+ "label": "Argomenti di ingresso",
+ "description": "Inserire gli argomenti specifici per questo flusso."
+ },
+ "label": "Ingressi della telecamera",
+ "description": "Elenco delle definizioni dei flussi di ingresso (percorsi e ruoli) per questa telecamera.",
+ "path": {
+ "label": "Percorso di ingresso",
+ "description": "URL o percorso del flusso di ingresso della telecamera."
+ },
+ "roles": {
+ "label": "Ruoli di ingresso",
+ "description": "Ruoli per questo flusso di ingresso."
+ }
+ },
+ "gpu": {
+ "description": "Indice GPU predefinito utilizzato per l'accelerazione hardware, se disponibile.",
+ "label": "Indice GPU"
+ },
+ "description": "Impostazioni di FFmpeg, inclusi percorso binario, argomenti, opzioni hwaccel e argomenti di output per ruolo.",
+ "global_args": {
+ "label": "Argomenti globali di FFmpeg",
+ "description": "Argomenti globali passati ai processi FFmpeg."
+ },
+ "input_args": {
+ "label": "Argomenti di ingresso",
+ "description": "Argomenti di ingresso applicati ai flussi di ingresso di FFmpeg."
+ },
+ "output_args": {
+ "label": "Argomenti di uscita",
+ "description": "Argomenti di uscita predefiniti utilizzati per i diversi ruoli di FFmpeg, come rilevamento e registrazione.",
+ "detect": {
+ "label": "Rileva gli argomenti di uscita",
+ "description": "Argomenti di uscita predefiniti per il rilevamento dei flussi di ruolo."
+ },
+ "record": {
+ "label": "Registra gli argomenti di uscita",
+ "description": "Argomenti di uscita predefiniti per i flussi del ruolo di registrazione."
+ }
+ },
+ "retry_interval": {
+ "label": "Tempo di ripetizione FFmpeg",
+ "description": "Secondi di attesa prima di tentare di riconnettere un flusso video della telecamera dopo un errore. Il valore predefinito è 10."
+ },
+ "apple_compatibility": {
+ "label": "Compatibilità Apple",
+ "description": "Attiva l'aggiunta di tag HEVC per una migliore compatibilità con i lettori Apple durante la registrazione in formato H.265."
}
+ },
+ "detectors": {
+ "cpu": {
+ "num_threads": {
+ "label": "Numero di processi di rilevamento",
+ "description": "Il numero di processi utilizzati per l'inferenza basata sulla CPU."
+ },
+ "description": "Rilevatore CPU TFLite che esegue modelli TensorFlow Lite sulla CPU di sistema senza accelerazione hardware. Sconsigliato.",
+ "label": "CPU"
+ },
+ "label": "Dispositivo di rilevamento",
+ "hailo8l": {
+ "description": "Rilevatore Hailo-8/Hailo-8L che utilizza modelli HEF e l'SDK HailoRT per l'inferenza sul dispositivo Hailo.",
+ "label": "Hailo-8/Hailo-8L",
+ "device": {
+ "label": "Tipo di dispositivo",
+ "description": "Il dispositivo da utilizzare per l'inferenza Hailo (ad es. 'PCIe', 'M.2')."
+ }
+ },
+ "openvino": {
+ "description": "Rilevatore OpenVINO per CPU AMD e Intel, GPU Intel e dispositivo Intel VPU."
+ },
+ "rknn": {
+ "description": "Rilevatore RKNN per NPU Rockchip; esegue modelli RKNN compilati su dispositivo Rockchip."
+ },
+ "synaptics": {
+ "description": "Rilevatore NPU Synaptics per modelli in formato .synap utilizzando l'SDK Synap su dispositivo Synaptics."
+ },
+ "type": {
+ "label": "Tipo"
+ },
+ "description": "Configurazione per i rilevatori di oggetti (backend CPU, GPU, ONNX) e impostazioni del modello specifiche per il rilevatore.",
+ "model": {
+ "label": "Configurazione del modello specifica del rilevatore",
+ "description": "Opzioni di configurazione del modello specifiche del rilevatore (percorso, dimensioni di input, ecc.).",
+ "path": {
+ "label": "Percorso del modello di rilevamento oggetti personalizzato",
+ "description": "Percorso di un file del modello di rilevamento personalizzato (o plus:// per i modelli Frigate+)."
+ },
+ "labelmap_path": {
+ "label": "Mappa delle etichette (label map) per il rilevatore di oggetti personalizzato",
+ "description": "Percorso di un file labelmap che mappa le classi numeriche in etichette testuali per il rilevatore."
+ },
+ "width": {
+ "label": "Larghezza di input del modello di rilevamento oggetti",
+ "description": "Larghezza del tensore di input del modello in pixel."
+ },
+ "height": {
+ "label": "Altezza di input del modello di rilevamento oggetti",
+ "description": "Altezza del tensore di input del modello in pixel."
+ },
+ "labelmap": {
+ "label": "Personalizzazione labelmap",
+ "description": "Sostituzioni o voci di rimappatura da unire alla labelmap standard."
+ },
+ "attributes_map": {
+ "label": "Mappa delle etichette oggetto con le relative etichette di attributo",
+ "description": "Mappatura dalle etichette oggetto alle etichette di attributo utilizzata per allegare metadati (ad esempio 'car' -> ['license_plate'])."
+ },
+ "input_tensor": {
+ "label": "Forma del tensore di input del modello",
+ "description": "Formato del tensore richiesto dal modello: 'nhwc' o 'nchw'."
+ },
+ "input_pixel_format": {
+ "label": "Formato colore dei pixel di input del modello",
+ "description": "Spazio colore dei pixel richiesto dal modello: 'rgb', 'bgr' o 'yuv'."
+ },
+ "input_dtype": {
+ "label": "Tipo di dati di input del modello (Dtype)",
+ "description": "Tipo di dati del tensore di input del modello (ad esempio 'float32')."
+ },
+ "model_type": {
+ "label": "Tipo di modello di rilevamento oggetti",
+ "description": "Tipo di architettura del modello del rilevatore (ssd, yolox, yolonas) utilizzato da alcuni rilevatori per l'ottimizzazione."
+ }
+ },
+ "model_path": {
+ "label": "Percorso del modello specifico del rilevatore",
+ "description": "Percorso del file binario del modello del rilevatore, se richiesto dal rilevatore scelto."
+ },
+ "axengine": {
+ "label": "NPU AXEngine",
+ "description": "Rilevatore NPU AXERA AX650N/AX8850N che esegue file .axmodel compilati tramite il runtime AXEngine."
+ },
+ "deepstack": {
+ "label": "DeepStack",
+ "description": "Rilevatore DeepStack/CodeProject.AI che invia immagini a una API HTTP DeepStack remota per l'inferenza. Non consigliato.",
+ "api_url": {
+ "label": "URL API DeepStack",
+ "description": "L'URL delle API DeepStack."
+ },
+ "api_timeout": {
+ "label": "Timeout API DeepStack (in secondi)",
+ "description": "Tempo massimo consentito per una richiesta alle API DeepStack."
+ },
+ "api_key": {
+ "label": "Chiave API DeepStack (se richiesta)",
+ "description": "Chiave API opzionale per i servizi DeepStack autenticati."
+ }
+ },
+ "degirum": {
+ "label": "DeGirum",
+ "description": "Rilevatore DeGirum per l'esecuzione di modelli tramite il cloud DeGirum o servizi di inferenza locali.",
+ "zoo": {
+ "label": "Model Zoo",
+ "description": "Percorso o URL del model zoo di DeGirum."
+ },
+ "token": {
+ "label": "Token Cloud DeGirum",
+ "description": "Token per l'accesso al Cloud DeGirum."
+ }
+ },
+ "edgetpu": {
+ "label": "EdgeTPU",
+ "description": "Rilevatore EdgeTPU che esegue modelli TensorFlow Lite compilati per Coral EdgeTPU utilizzando il delegate EdgeTPU.",
+ "device": {
+ "label": "Tipo di dispositivo",
+ "description": "Il dispositivo da utilizzare per l'inferenza EdgeTPU (ad es. 'usb', 'pci')."
+ }
+ },
+ "memryx": {
+ "label": "MemryX",
+ "description": "Rilevatore MemryX MX3 che esegue modelli DFP compilati su acceleratori MemryX.",
+ "device": {
+ "label": "Percorso del dispositivo",
+ "description": "Il dispositivo da utilizzare per l'inferenza MemryX (ad es. 'PCIe')."
+ }
+ },
+ "onnx": {
+ "label": "ONNX",
+ "description": "Rilevatore ONNX per l'esecuzione di modelli ONNX; utilizzerà i backend di accelerazione disponibili (CUDA/ROCm/OpenVINO) quando presenti.",
+ "device": {
+ "label": "Tipo di dispositivo",
+ "description": "Il dispositivo da utilizzare per l'inferenza ONNX (ad es. 'AUTO', 'CPU', 'GPU')."
+ }
+ }
+ },
+ "audio_transcription": {
+ "label": "Trascrizione audio",
+ "description": "Impostazioni per la trascrizione audio in tempo reale e del parlato utilizzata per eventi e sottotitoli in tempo reale.",
+ "enabled": {
+ "label": "Abilita la trascrizione audio"
+ },
+ "live_enabled": {
+ "label": "Trascrizione dal vivo",
+ "description": "Abilita la trascrizione in diretta dell'audio non appena viene ricevuto."
+ },
+ "model_size": {
+ "label": "Dimensioni del modello"
+ }
+ },
+ "mqtt": {
+ "label": "MQTT",
+ "enabled": {
+ "label": "Abilita MQTT",
+ "description": "Abilita o disabilita l'integrazione MQTT per stato, eventi e istantanee."
+ },
+ "host": {
+ "label": "Sistema MQTT",
+ "description": "Nome sistema o indirizzo IP del broker MQTT."
+ },
+ "port": {
+ "label": "Porta MQTT",
+ "description": "Porta del broker MQTT (solitamente 1883 per MQTT standard)."
+ },
+ "topic_prefix": {
+ "label": "Prefisso argomento",
+ "description": "Prefisso dell'argomento MQTT per tutti gli argomenti Frigate; deve essere univoco se si eseguono più istanze."
+ },
+ "client_id": {
+ "label": "ID client",
+ "description": "Identificativo del client utilizzato per la connessione al broker MQTT; deve essere univoco per ogni istanza."
+ },
+ "stats_interval": {
+ "label": "Intervallo statistiche",
+ "description": "Intervallo in secondi per la pubblicazione delle statistiche di sistema e della telecamera su MQTT."
+ },
+ "user": {
+ "label": "Nome utente MQTT",
+ "description": "Nome utente MQTT facoltativo; può essere fornito tramite variabili d'ambiente o segreti."
+ },
+ "password": {
+ "label": "Password MQTT",
+ "description": "Password MQTT facoltativa; può essere fornita tramite variabili d'ambiente o segreti."
+ },
+ "tls_ca_certs": {
+ "label": "Certificati CA TLS",
+ "description": "Percorso al certificato CA per le connessioni TLS al broker (per certificati autofirmati)."
+ },
+ "tls_client_cert": {
+ "label": "Certificato client",
+ "description": "Percorso del certificato client per l'autenticazione reciproca TLS; non impostare nome utente/password quando si utilizzano certificati client."
+ },
+ "description": "Impostazioni per la connessione e la pubblicazione di dati di telemetria, istantanee e dettagli degli eventi a un broker MQTT.",
+ "tls_client_key": {
+ "label": "Chiave client",
+ "description": "Percorso della chiave privata per il certificato client."
+ },
+ "tls_insecure": {
+ "label": "TLS non sicuro",
+ "description": "Consenti connessioni TLS non sicure saltando la verifica del nome sistema (sconsigliato)."
+ },
+ "qos": {
+ "label": "QoS MQTT",
+ "description": "Livello di qualità del servizio per le pubblicazioni/sottoscrizioni MQTT (0, 1 o 2)."
+ }
+ },
+ "onvif": {
+ "tls_insecure": {
+ "label": "Disabilita verifica TLS",
+ "description": "Salta la verifica TLS e disabilita l'autenticazione digest per ONVIF (non sicuro; utilizzare solo in reti sicure)."
+ },
+ "profile": {
+ "label": "Profilo ONVIF",
+ "description": "Profilo multimediale ONVIF specifico da utilizzare per il controllo PTZ, identificato tramite token o nome. Se non impostato, viene selezionato automaticamente il primo profilo con una configurazione PTZ valida."
+ },
+ "autotracking": {
+ "label": "Tracciamento automatico",
+ "enabled": {
+ "label": "Abilita il tracciamento automatico",
+ "description": "Abilita o disabilita il tracciamento automatico della telecamera PTZ per gli oggetti rilevati."
+ },
+ "calibrate_on_startup": {
+ "label": "Calibra all'avvio",
+ "description": "Misura la velocità dei motori PTZ all'avvio per migliorare la precisione del tracciamento. Frigate aggiornerà la configurazione con i valori di movement_weights dopo la calibrazione."
+ },
+ "zooming": {
+ "label": "Modalità ingrandimento",
+ "description": "Controlla il comportamento dello zoom: disabled (solo pan/tilt), absolute (più compatibile) o relative (pan/tilt/zoom simultanei)."
+ },
+ "zoom_factor": {
+ "label": "Fattore di ingrandimento",
+ "description": "Controlla il livello di zoom sugli oggetti tracciati. Valori più bassi mantengono una porzione maggiore della scena visibile; valori più alti aumentano lo zoom ma potrebbero far perdere il tracciamento. Valori compresi tra 0.1 e 0.75."
+ },
+ "track": {
+ "label": "Oggetti tracciati",
+ "description": "Elenco dei tipi di oggetto che dovrebbero attivare il tracciamento automatico."
+ },
+ "required_zones": {
+ "label": "Zone richieste",
+ "description": "Gli oggetti devono entrare in una di queste zone prima che inizi il tracciamento automatico."
+ },
+ "timeout": {
+ "label": "Scadenza di ritorno",
+ "description": "Attendi questo numero di secondi dopo aver perso il tracciamento prima di riportare la telecamera nella posizione preimpostata."
+ },
+ "movement_weights": {
+ "description": "Valori di calibrazione generati automaticamente dalla calibrazione della telecamera. Non modificare manualmente.",
+ "label": "Pesi del movimento"
+ },
+ "enabled_in_config": {
+ "label": "Stato originale del tracciamento automatico",
+ "description": "Campo interno per tracciare se il tracciamento automatico era abilitato nella configurazione."
+ },
+ "description": "Traccia automaticamente gli oggetti in movimento e li mantiene centrati nell'inquadratura utilizzando i movimenti della telecamera PTZ.",
+ "return_preset": {
+ "label": "Preset di ritorno",
+ "description": "Nome del preset ONVIF configurato nel firmware della telecamera a cui ritornare al termine del tracciamento."
+ }
+ },
+ "ignore_time_mismatch": {
+ "label": "Ignora la discrepanza oraria",
+ "description": "Ignora le differenze di sincronizzazione oraria tra la telecamera e il server Frigate per la comunicazione ONVIF."
+ },
+ "label": "ONVIF",
+ "port": {
+ "label": "Porta ONVIF",
+ "description": "Numero di porta per il servizio ONVIF."
+ },
+ "description": "Impostazioni della connessione ONVIF e del tracciamento automatico PTZ per questa telecamera.",
+ "host": {
+ "label": "Host ONVIF",
+ "description": "Host (e schema opzionale) per il servizio ONVIF di questa telecamera."
+ },
+ "user": {
+ "label": "Nome utente ONVIF",
+ "description": "Nome utente per l'autenticazione ONVIF; alcuni dispositivi richiedono un utente amministratore per l'ONVIF."
+ },
+ "password": {
+ "label": "Password ONVIF",
+ "description": "Password per l'autenticazione ONVIF."
+ }
+ },
+ "detect": {
+ "label": "Rilevamento oggetti",
+ "description": "Impostazioni per il ruolo di rilevamento/rilevamento utilizzato per eseguire il rilevamento degli oggetti e inizializzare i localizzatori.",
+ "enabled": {
+ "label": "Abilita il rilevamento degli oggetti",
+ "description": "Abilita o disabilita il rilevamento degli oggetti per tutte le telecamere; l'impostazione può essere modificata per ogni singola telecamera."
+ },
+ "height": {
+ "label": "Rileva altezza",
+ "description": "Altezza (in pixel) dei fotogrammi utilizzati per il flusso di rilevamento; lascia vuoto per utilizzare la risoluzione nativa del flusso."
+ },
+ "width": {
+ "label": "Rileva larghezza",
+ "description": "Larghezza (in pixel) dei fotogrammi utilizzati per il flusso di rilevamento; lascia vuoto per utilizzare la risoluzione nativa del flusso."
+ },
+ "fps": {
+ "label": "Rileva FPS",
+ "description": "Numero di fotogrammi al secondo desiderati per eseguire il rilevamento; valori inferiori riducono l'utilizzo della CPU (il valore consigliato è 5, impostarne uno superiore - al massimo 10 - solo se si devono tracciare oggetti in movimento estremamente rapidi)."
+ },
+ "min_initialized": {
+ "label": "Frame di inizializzazione minimi",
+ "description": "Numero di rilevamenti consecutivi necessari prima di creare un oggetto tracciato. Aumenta questo valore per ridurre le inizializzazioni errate. Il valore predefinito è FPS diviso per 2."
+ },
+ "max_disappeared": {
+ "label": "Numero di fotogrammi scomparsi",
+ "description": "Numero di fotogrammi senza rilevamento prima che un oggetto tracciato venga considerato scomparso."
+ },
+ "stationary": {
+ "label": "Configurazione degli oggetti stazionari",
+ "description": "Impostazioni per rilevare e gestire gli oggetti che rimangono fermi per un certo periodo di tempo.",
+ "interval": {
+ "label": "Intervallo stazionario",
+ "description": "Con quale frequenza (in fotogrammi) eseguire un controllo di rilevamento per confermare che l'oggetto sia stazionario."
+ },
+ "threshold": {
+ "label": "Soglia stazionaria",
+ "description": "Numero di fotogrammi senza cambio di posizione necessari per contrassegnare un oggetto come stazionario."
+ },
+ "max_frames": {
+ "label": "Fotogrammi massimi",
+ "description": "Limita la durata del tracciamento degli oggetti statici prima che vengano scartati.",
+ "default": {
+ "description": "Numero massimo predefinito di fotogrammi per seguire un oggetto stazionario prima di interrompere la ripresa.",
+ "label": "Fotogrammi massimi predefiniti"
+ },
+ "objects": {
+ "label": "Fotogrammi massimi oggetto",
+ "description": "Opzioni di sovrascrittura per singolo oggetto relative al numero massimo di fotogrammi necessari per tracciare oggetti statici."
+ }
+ },
+ "classifier": {
+ "label": "Abilita il classificatore visivo",
+ "description": "Utilizza un classificatore visivo per rilevare oggetti realmente stazionari anche quando i riquadri di delimitazione sono instabili."
+ }
+ },
+ "annotation_offset": {
+ "label": "Differenza annotazione",
+ "description": "Millisecondi per spostare le annotazioni di rilevamento dello spostamento al fine di allineare meglio i riquadri di delimitazione della cronologia con le registrazioni; può essere positivo o negativo."
+ }
+ },
+ "face_recognition": {
+ "label": "Riconoscimento facciale",
+ "model_size": {
+ "label": "Dimensioni del modello"
+ },
+ "enabled": {
+ "label": "Abilita il riconoscimento facciale"
+ },
+ "min_area": {
+ "label": "Area minima del viso",
+ "description": "Area minima (in pixel) del riquadro del volto rilevato necessaria per tentare il riconoscimento."
+ }
+ },
+ "proxy": {
+ "logout_url": {
+ "description": "URL per reindirizzare gli utenti al momento della registrazione tramite il proxy.",
+ "label": "URL di disconnessione"
+ },
+ "label": "Proxy",
+ "description": "Impostazioni per l'integrazione di Frigate dietro un proxy inverso che trasmette le intestazioni utente autenticate.",
+ "header_map": {
+ "label": "Mappatura dell'intestazione",
+ "description": "Mappa le intestazioni proxy in entrata ai campi utente e ruolo di Frigate per l'autenticazione basata su proxy.",
+ "user": {
+ "label": "Intestazione utente",
+ "description": "Intestazione contenente il nome utente autenticato fornito dal proxy a monte."
+ },
+ "role": {
+ "label": "Intestazione ruolo",
+ "description": "Intestazione contenente il ruolo o i gruppi dell'utente autenticato, provenienti dal proxy a monte."
+ },
+ "role_map": {
+ "label": "Mappatura dei ruoli",
+ "description": "Mappa i valori dei gruppi a monte dei ruoli di Frigate (ad esempio, mappa i gruppi di amministrazione al ruolo di amministratore)."
+ }
+ },
+ "auth_secret": {
+ "label": "Segreto di proxy",
+ "description": "Segreto opzionale verificato rispetto all'intestazione X-Proxy-Secret per convalidare i proxy attendibili."
+ },
+ "default_role": {
+ "label": "Ruolo predefinito",
+ "description": "Ruolo predefinito assegnato agli utenti autenticati tramite proxy quando non si applica nessuna mappatura dei ruoli."
+ },
+ "separator": {
+ "label": "Carattere separatore",
+ "description": "Carattere utilizzato per separare valori multipli forniti negli header del proxy."
+ }
+ },
+ "review": {
+ "label": "Revisione",
+ "alerts": {
+ "label": "Configurazione avvisi",
+ "description": "Impostazioni relative a quali oggetti tracciati generano avvisi e alle modalità di conservazione degli avvisi stessi.",
+ "enabled": {
+ "label": "Abilita avvisi"
+ },
+ "labels": {
+ "label": "Etichette avvisi",
+ "description": "Elenco delle etichette oggetto che si qualificano come avvisi (ad esempio: car, person)."
+ },
+ "required_zones": {
+ "label": "Zone richieste",
+ "description": "Zone in cui un oggetto deve entrare per essere considerato un avviso; lascia vuoto per consentire qualsiasi zona."
+ },
+ "enabled_in_config": {
+ "label": "Stato avvisi originale",
+ "description": "Traccia se gli avvisi erano originariamente abilitati nella configurazione statica."
+ },
+ "cutoff_time": {
+ "label": "Tempo limite avvisi",
+ "description": "Secondi da attendere dopo l'assenza di attività che genera avvisi prima di interrompere un avviso."
+ }
+ },
+ "detections": {
+ "label": "Configurazione rilevamenti",
+ "description": "Impostazioni relative a quali oggetti tracciati generano rilevamenti (non avvisi) e alle modalità di conservazione dei rilevamenti stessi.",
+ "enabled": {
+ "label": "Abilita rilevamenti"
+ },
+ "labels": {
+ "label": "Etichette rilevamenti",
+ "description": "Elenco delle etichette oggetto che si qualificano come eventi di rilevamento."
+ },
+ "required_zones": {
+ "label": "Zone richieste",
+ "description": "Zone in cui un oggetto deve entrare per essere considerato un rilevamento; lascia vuoto per consentire qualsiasi zona."
+ },
+ "cutoff_time": {
+ "label": "Tempo limite rilevamenti",
+ "description": "Secondi da attendere dopo l'assenza di attività che genera rilevamenti prima di interrompere un rilevamento."
+ },
+ "enabled_in_config": {
+ "label": "Stato rilevamenti originale",
+ "description": "Traccia se i rilevamenti erano originariamente abilitati nella configurazione statica."
+ }
+ },
+ "genai": {
+ "label": "Configurazione GenAI",
+ "description": "Controlla l'uso dell'IA generativa per la produzione di descrizioni e riepiloghi degli elementi di revisione.",
+ "enabled": {
+ "label": "Abilita descrizioni GenAI",
+ "description": "Abilita o disabilita le descrizioni e i riepiloghi generati dalla GenAI per gli elementi di revisione."
+ },
+ "alerts": {
+ "label": "Abilita GenAI per avvisi",
+ "description": "Usa la GenAI per generare descrizioni per gli elementi di avviso."
+ },
+ "detections": {
+ "label": "Abilita GenAI per rilevamenti",
+ "description": "Usa la GenAI per generare descrizioni per gli elementi di rilevamento."
+ },
+ "image_source": {
+ "label": "Fonte immagini di revisione",
+ "description": "Fonte delle immagini inviate alla GenAI ('anteprime' o 'registrazioni'); 'registrazioni' utilizza fotogrammi di qualità superiore ma consuma più token."
+ },
+ "additional_concerns": {
+ "label": "Ulteriori criteri di attenzione",
+ "description": "Un elenco di note o criteri di attenzione aggiuntivi che la GenAI deve considerare quando valuta l'attività su questa telecamera."
+ },
+ "debug_save_thumbnails": {
+ "label": "Salva miniature",
+ "description": "Salva le miniature inviate al provider GenAI per il debug e la revisione."
+ },
+ "enabled_in_config": {
+ "label": "Stato GenAI originale",
+ "description": "Traccia se la revisione GenAI era originariamente abilitata nella configurazione statica."
+ },
+ "preferred_language": {
+ "label": "Lingua preferita",
+ "description": "Lingua preferita da richiedere al provider GenAI per le risposte generate."
+ },
+ "activity_context_prompt": {
+ "label": "Prompt di contesto dell'attività",
+ "description": "Prompt personalizzato che descrive cosa costituisce o meno un'attività sospetta, per fornire contesto ai riepiloghi della GenAI."
+ }
+ }
+ },
+ "ui": {
+ "label": "Interfaccia utente",
+ "description": "Preferenze dell'interfaccia utente come fuso orario, formato di data/ora e unità di misura.",
+ "timezone": {
+ "label": "Fuso orario",
+ "description": "Fuso orario opzionale da visualizzare nell'interfaccia utente (se non impostato, viene usata l'ora locale del browser)."
+ },
+ "time_format": {
+ "label": "Formato Ora",
+ "description": "Formato dell'ora da utilizzare nell'interfaccia utente (browser, 12 ore o 24 ore)."
+ },
+ "unit_system": {
+ "label": "Sistema di unità di misura",
+ "description": "Sistema di unità di misura per la visualizzazione (metrico o imperiale) utilizzato nell'interfaccia utente e nei messaggi MQTT."
+ }
+ },
+ "profiles": {
+ "label": "Profili",
+ "friendly_name": {
+ "label": "Nome descrittivo",
+ "description": "Nome visualizzato per questo profilo nell'interfaccia utente."
+ },
+ "description": "Definizioni di profili denominati con nomi descrittivi. I profili delle telecamere devono fare riferimento ai nomi definiti qui."
+ },
+ "record": {
+ "label": "Registrazione",
+ "export": {
+ "description": "Impostazioni utilizzate durante l'esportazione delle registrazioni come timelapse e accelerazione hardware.",
+ "hwaccel_args": {
+ "description": "Argomenti di accelerazione hardware da utilizzare per le operazioni di esportazione/transcodifica.",
+ "label": "Argomenti hwaccel esportazione"
+ },
+ "label": "Configurazione esportazione",
+ "max_concurrent": {
+ "label": "Esportazioni simultanee massime",
+ "description": "Numero massimo di processi di esportazione da elaborare contemporaneamente."
+ }
+ },
+ "enabled": {
+ "label": "Abilita registrazione",
+ "description": "Attiva o disattiva la registrazione per tutte le telecamere; l'impostazione può essere modificata per ogni singola telecamera."
+ },
+ "expire_interval": {
+ "label": "Intervallo di pulizia registrazioni",
+ "description": "Minuti tra i cicli di pulizia che rimuovono i segmenti di registrazione scaduti."
+ },
+ "continuous": {
+ "label": "Conservazione continua",
+ "description": "Numero di giorni per cui conservare le registrazioni, indipendentemente dagli oggetti tracciati o dal movimento. Impostare su 0 se si desidera conservare solo le registrazioni relative agli avvisi e ai rilevamenti.",
+ "days": {
+ "label": "Giorni di conservazione",
+ "description": "Numero di giorni di conservazione delle registrazioni."
+ }
+ },
+ "motion": {
+ "label": "Conservazione movimento",
+ "description": "Numero di giorni per cui conservare le registrazioni attivate dal movimento, indipendentemente dagli oggetti tracciati. Impostare su 0 se si desidera conservare solo le registrazioni relative ad allarmi e rilevamenti.",
+ "days": {
+ "label": "Giorni di conservazione",
+ "description": "Numero di giorni di conservazione delle registrazioni."
+ }
+ },
+ "detections": {
+ "label": "Conservazione rilevamento",
+ "description": "Impostazioni relative alla conservazione delle registrazioni per gli eventi di rilevamento, comprese le durate prima e dopo l'acquisizione.",
+ "pre_capture": {
+ "label": "Secondi prima della cattura",
+ "description": "Numero di secondi precedenti all'evento di rilevamento da includere nella registrazione."
+ },
+ "post_capture": {
+ "label": "Secondi dopo la cattura",
+ "description": "Numero di secondi successivi all'evento di rilevamento da includere nella registrazione."
+ },
+ "retain": {
+ "label": "Conservazione eventi",
+ "description": "Impostazioni di conservazione per le registrazioni degli eventi di rilevamento.",
+ "days": {
+ "label": "Giorni di conservazione",
+ "description": "Numero di giorni per cui conservare le registrazioni degli eventi di rilevamento."
+ },
+ "mode": {
+ "label": "Modalità di conservazione",
+ "description": "Modalità di conservazione: tutti (salva tutti i segmenti), movimento (salva i segmenti con movimento) o oggetti_attivi (salva i segmenti con oggetti attivi)."
+ }
+ }
+ },
+ "alerts": {
+ "label": "Conservazione avvisi",
+ "description": "Impostazioni di conservazione delle registrazioni per gli eventi di avviso, incluse le durate prima e dopo la cattura.",
+ "pre_capture": {
+ "label": "Secondi prima della cattura",
+ "description": "Numero di secondi precedenti all'evento di rilevamento da includere nella registrazione."
+ },
+ "post_capture": {
+ "label": "Secondi dopo la cattura",
+ "description": "Numero di secondi successivi all'evento di rilevamento da includere nella registrazione."
+ },
+ "retain": {
+ "label": "Conservazione eventi",
+ "description": "Impostazioni di conservazione per le registrazioni degli eventi di rilevamento.",
+ "days": {
+ "label": "Giorni di conservazione",
+ "description": "Numero di giorni per cui conservare le registrazioni degli eventi di rilevamento."
+ },
+ "mode": {
+ "label": "Modalità di conservazione",
+ "description": "Modalità di conservazione: tutti (salva tutti i segmenti), movimento (salva i segmenti con movimento) o oggetti_attivi (salva i segmenti con oggetti attivi)."
+ }
+ }
+ },
+ "preview": {
+ "label": "Configurazione anteprima",
+ "description": "Impostazioni che controllano la qualità delle anteprime di registrazione mostrate nell'interfaccia utente (UI).",
+ "quality": {
+ "label": "Qualità anteprima",
+ "description": "Livello di qualità dell'anteprima (very_low, low, medium, high, very_high)."
+ }
+ },
+ "enabled_in_config": {
+ "label": "Stato registrazione originale",
+ "description": "Indica se la registrazione era abilitata nella configurazione statica originale."
+ },
+ "description": "Impostazioni di registrazione e conservazione vengono applicate alle telecamere a meno che non vengano modificate singolarmente per ciascuna telecamera."
+ },
+ "snapshots": {
+ "label": "Istantanee",
+ "enabled": {
+ "label": "Abilita istantanee"
+ },
+ "timestamp": {
+ "label": "Sovrimpressione timestamp",
+ "description": "Sovraimprime un timestamp sulle istantanee provenienti dalle API."
+ },
+ "bounding_box": {
+ "label": "Sovrimpressione riquadro di delimitazione",
+ "description": "Disegna i riquadri di delimitazione per gli oggetti tracciati sulle istantanee provenienti dalle API."
+ },
+ "crop": {
+ "label": "Ritaglia istantanea",
+ "description": "Ritaglia le istantanee provenienti dalle API in base al riquadro di delimitazione dell'oggetto rilevato."
+ },
+ "required_zones": {
+ "label": "Zone richieste",
+ "description": "Zone in cui un oggetto deve entrare affinché venga salvata un'istantanea."
+ },
+ "height": {
+ "label": "Altezza istantanea",
+ "description": "Altezza (in pixel) a cui ridimensionare le istantanee provenienti dalle API; lascia vuoto per mantenere la dimensione originale."
+ },
+ "retain": {
+ "label": "Conservazione istantanee",
+ "description": "Impostazioni di conservazione per le istantanee, inclusi i giorni predefiniti e le eccezioni per singolo oggetto.",
+ "default": {
+ "label": "Conservazione predefinita",
+ "description": "Numero predefinito di giorni per cui conservare le istantanee."
+ },
+ "mode": {
+ "label": "Modalità di conservazione",
+ "description": "Modalità di conservazione: tutti (salva tutti i segmenti), movimento (salva i segmenti con movimento) o oggetti_attivi (salva i segmenti con oggetti attivi)."
+ },
+ "objects": {
+ "label": "Conservazione oggetti",
+ "description": "Eccezioni per singolo oggetto relativi ai giorni di conservazione delle istantanee."
+ }
+ },
+ "quality": {
+ "label": "Qualità istantanee",
+ "description": "Qualità di codifica per le istantanee salvate (0-100)."
+ }
+ },
+ "motion": {
+ "label": "Rilevamento movimento",
+ "contour_area": {
+ "label": "Area di contorno",
+ "description": "Area minima del contorno in pixel richiesta affinché un contorno di movimento venga conteggiato."
+ },
+ "improve_contrast": {
+ "label": "Migliora il contrasto",
+ "description": "Applica un miglioramento del contrasto ai fotogrammi prima dell'analisi del movimento per facilitare il rilevamento."
+ },
+ "enabled": {
+ "label": "Attiva il rilevamento del movimento"
+ },
+ "threshold": {
+ "label": "Soglia di movimento",
+ "description": "Soglia di differenza tra i pixel utilizzata dal rilevatore di movimento; valori più alti riducono la sensibilità (intervallo 1-255)."
+ },
+ "lightning_threshold": {
+ "label": "Soglia di luminosità",
+ "description": "Soglia per rilevare e ignorare brevi picchi di luminosità (un valore più basso indica una maggiore sensibilità; i valori sono compresi tra 0.3 e 1.0). Ciò non impedisce del tutto il rilevamento del movimento, ma fa semplicemente sì che il rilevatore smetta di analizzare i fotogrammi successivi una volta superata la soglia. Durante questi eventi vengono comunque create registrazioni basate sul movimento."
+ },
+ "skip_motion_threshold": {
+ "label": "Ignora soglia di movimento",
+ "description": "Se impostato su un valore compreso tra 0.0 e 110, e se in un singolo fotogramma cambia una porzione dell'immagine superiore a tale frazione, il rilevatore non restituirà alcun riquadro di movimento e si ricalibrerà immediatamente. Ciò consente di risparmiare risorse della CPU e ridurre i falsi positivi in caso di fulmini, temporali, ecc., ma potrebbe comportare la mancata rilevazione di eventi reali, come ad esempio il tracciamento automatico di un oggetto da parte di una telecamera PTZ. Il compromesso consiste nel scegliere se sacrificare alcuni megabyte di registrazioni o rivedere un paio di brevi video. Lasciare non impostato (Nessuno) per disabilitare questa funzione."
+ },
+ "delta_alpha": {
+ "description": "Fattore di fusione alfa utilizzato nel calcolo della differenza tra fotogrammi per il calcolo del movimento.",
+ "label": "Delta alfa"
+ },
+ "frame_alpha": {
+ "description": "Valore alfa utilizzato durante la fusione dei fotogrammi per la preelaborazione del movimento.",
+ "label": "Frame alfa"
+ },
+ "frame_height": {
+ "label": "Altezza del frame",
+ "description": "Altezza in pixel alla quale ridimensionare i fotogrammi durante il calcolo del movimento."
+ },
+ "mask": {
+ "label": "Coordinate della maschera",
+ "description": "Coordinate x, y ordinate che definiscono il poligono della maschera di movimento utilizzato per includere/escludere aree."
+ },
+ "mqtt_off_delay": {
+ "label": "Ritardo di disattivazione MQTT",
+ "description": "Secondi di attesa dopo l'ultimo movimento prima di pubblicare uno stato 'off' MQTT."
+ },
+ "enabled_in_config": {
+ "description": "Indica se il rilevamento del movimento era abilitato nella configurazione statica originale.",
+ "label": "Stato movimento originale"
+ },
+ "raw_mask": {
+ "label": "Maschera grezza"
+ }
+ },
+ "objects": {
+ "label": "Oggetti",
+ "description": "Impostazioni predefinite per il tracciamento degli oggetti, tra cui le etichette da tracciare e i filtri per singolo oggetto.",
+ "track": {
+ "label": "Oggetti da tracciare",
+ "description": "Elenco delle etichette degli oggetti da tracciare per tutte le telecamere; può essere sovrascritto per ogni singola telecamera."
+ },
+ "filters": {
+ "label": "Filtri oggetto",
+ "description": "Filtri applicati agli oggetti rilevati per ridurre i falsi positivi (area, rapporto, livello di confidenza).",
+ "min_area": {
+ "label": "Area minima dell'oggetto",
+ "description": "Area minima del riquadro di delimitazione (in pixel o percentuale) richiesta per questo tipo di oggetto. Può essere espressa in pixel (numero intero) o in percentuale (valore decimale compreso tra 0.000001 e 0.99)."
+ },
+ "max_area": {
+ "label": "Area massima dell'oggetto",
+ "description": "Area massima del riquadro di delimitazione (in pixel o percentuale) richiesta per questo tipo di oggetto. Può essere espressa in pixel (numero intero) o in percentuale (valore decimale compreso tra 0.000001 e 0.99)."
+ },
+ "min_ratio": {
+ "label": "Rapporto di aspetto minimo",
+ "description": "Rapporto minimo tra larghezza e altezza richiesto affinché il riquadro di delimitazione sia valido."
+ },
+ "max_ratio": {
+ "label": "Rapporto di aspetto massimo",
+ "description": "Rapporto massimo tra larghezza e altezza richiesto affinché il riquadro di delimitazione sia valido."
+ },
+ "threshold": {
+ "label": "Soglia di confidenza",
+ "description": "Soglia di confidenza media di rilevamento necessaria affinché l'oggetto sia considerato un vero positivo."
+ },
+ "min_score": {
+ "label": "Confidenza minima",
+ "description": "Livello minimo di confidenza nel rilevamento in un singolo fotogramma richiesto affinché l'oggetto venga conteggiato."
+ },
+ "mask": {
+ "label": "Maschera filtro",
+ "description": "Coordinate del poligono che definiscono la posizione di applicazione di questo filtro all'interno dell'inquadratura."
+ },
+ "raw_mask": {
+ "label": "Maschera grezza"
+ }
+ },
+ "mask": {
+ "label": "Maschera oggetto",
+ "description": "Poligono di mascheramento utilizzato per impedire il rilevamento di oggetti in aree specifiche."
+ },
+ "genai": {
+ "label": "Configurazione degli oggetti GenAI",
+ "description": "Opzioni GenAI per la descrizione degli oggetti tracciati e l'invio dei fotogrammi per la generazione.",
+ "enabled": {
+ "label": "Abilita GenAI",
+ "description": "Abilita in modo predefinito la generazione di descrizioni da parte di GenAI per gli oggetti tracciati."
+ },
+ "use_snapshot": {
+ "label": "Utilizza istantanee",
+ "description": "Utilizza le istantanee degli oggetti anziché le miniature per la generazione di descrizioni con GenAI."
+ },
+ "prompt": {
+ "description": "Modello di richiesta predefinito utilizzato per la generazione di descrizioni con GenAI.",
+ "label": "Richiesta di didascalia"
+ },
+ "object_prompts": {
+ "description": "Richieste specifiche per ogni oggetto per personalizzare i risultati di GenAI in base a etichette specifiche.",
+ "label": "Richiesta di oggetto"
+ },
+ "objects": {
+ "label": "Oggetti GenAI",
+ "description": "Elenco di etichette degli oggetti da inviare a GenAI per impostazione predefinita."
+ },
+ "required_zones": {
+ "label": "Zone richieste",
+ "description": "Zone che devono essere inserite affinché gli oggetti siano idonei alla generazione della descrizione GenAI."
+ },
+ "debug_save_thumbnails": {
+ "label": "Salva miniature",
+ "description": "Salva le miniature inviate a GenAI per la correzione e la revisione."
+ },
+ "send_triggers": {
+ "label": "Attivazioni GenAI",
+ "description": "Definisce quando i fotogrammi devono essere inviati a GenAI (alla fine, dopo gli aggiornamenti, ecc.).",
+ "tracked_object_end": {
+ "label": "Invia alla fine",
+ "description": "Invia una richiesta a GenAI quando l'oggetto tracciato termina."
+ },
+ "after_significant_updates": {
+ "label": "Attivazione anticipata GenAI",
+ "description": "Invia una richiesta a GenAI dopo un numero specificato di aggiornamenti significativi per l'oggetto tracciato."
+ }
+ },
+ "enabled_in_config": {
+ "label": "Stato GenAI originale",
+ "description": "Indica se GenAI era abilitata nella configurazione statica originale."
+ }
+ },
+ "raw_mask": {
+ "label": "Maschera grezza"
+ },
+ "filters_attribute": {
+ "raw_mask": {
+ "label": "Maschera grezza"
+ },
+ "min_ratio": {
+ "label": "Rapporto di aspetto minimo",
+ "description": "Rapporto minimo tra larghezza e altezza richiesto affinché il riquadro di delimitazione sia valido."
+ },
+ "max_ratio": {
+ "label": "Rapporto di aspetto massimo",
+ "description": "Rapporto massimo tra larghezza e altezza richiesto affinché il riquadro di delimitazione sia valido."
+ },
+ "threshold": {
+ "label": "Soglia di confidenza"
+ },
+ "min_score": {
+ "label": "Confidenza minima"
+ },
+ "mask": {
+ "label": "Maschera filtro",
+ "description": "Coordinate del poligono che definiscono la posizione di applicazione di questo filtro all'interno dell'inquadratura."
+ }
+ }
+ },
+ "live": {
+ "label": "Riproduzione in diretta",
+ "streams": {
+ "label": "Nomi dei flussi Dal vivo",
+ "description": "Mappatura dei nomi dei flussi configurati ai nomi ritrasmissioni/go2rtc utilizzati per la riproduzione dal vivo."
+ },
+ "height": {
+ "label": "Altezza dal vivo",
+ "description": "Altezza (in pixel) per visualizzare il flusso dal vivo jsmpeg nell'interfaccia utente web; deve essere <= altezza del flusso rilevato."
+ },
+ "quality": {
+ "label": "Qualità dal vivo",
+ "description": "Qualità di codifica per il flusso jsmpeg (1 massima, 31 minima)."
+ }
+ },
+ "timestamp_style": {
+ "label": "Stile orario",
+ "position": {
+ "label": "Posizione timestamp",
+ "description": "Posizione del timestamp sull'immagine (tl/tr/bl/br)."
+ },
+ "format": {
+ "label": "Formato timestamp",
+ "description": "Stringa di formato data/ora utilizzata per i timestamp (codici di formato datetime di Python)."
+ },
+ "color": {
+ "label": "Colore timestamp",
+ "description": "Valori di colore RGB per il testo del timestamp (tutti i valori da 0 a 255).",
+ "red": {
+ "label": "Rosso",
+ "description": "Componente rosso (0-255) per il colore del timestamp."
+ },
+ "green": {
+ "label": "Verde",
+ "description": "Componente verde (0-255) per il colore del timestamp."
+ },
+ "blue": {
+ "label": "Blu",
+ "description": "Componente blu (0-255) per il colore del timestamp."
+ }
+ },
+ "thickness": {
+ "label": "Spessore timestamp",
+ "description": "Spessore della linea del testo del timestamp."
+ },
+ "effect": {
+ "label": "Effetto timestamp",
+ "description": "Effetto visivo per il testo del timestamp (none, solid, shadow)."
+ }
+ },
+ "database": {
+ "label": "Database",
+ "description": "Impostazioni per il database SQLite utilizzato da Frigate per memorizzare i metadati relativi agli oggetti tracciati e alle registrazioni.",
+ "path": {
+ "label": "Percorso del database",
+ "description": "Percorso del filesystem in cui verrà memorizzato il file del database Frigate SQLite."
+ }
+ },
+ "go2rtc": {
+ "label": "go2rtc",
+ "description": "Impostazioni per il servizio di ritrasmissione integrato go2rtc utilizzato per la ritrasmissione e la traduzione di flussi dal vivo."
+ },
+ "camera_mqtt": {
+ "label": "MQTT"
+ },
+ "notifications": {
+ "label": "Notifiche",
+ "description": "Impostazioni per abilitare e controllare le notifiche per tutte le telecamere; possono essere sovrascritte per ogni singola telecamera.",
+ "enabled": {
+ "label": "Abilita le notifiche",
+ "description": "Abilita o disabilita le notifiche per tutte le telecamere; l'impostazione può essere modificata per ogni singola telecamera."
+ },
+ "email": {
+ "label": "Email di notifica",
+ "description": "Indirizzo email utilizzato per le notifiche push o richiesto da alcuni fornitori di servizi di notifica."
+ },
+ "cooldown": {
+ "label": "Tempo di attesa tra notifiche",
+ "description": "Tempo di attesa (in secondi) tra le notifiche per evitare di inviare spam ai destinatari."
+ },
+ "enabled_in_config": {
+ "label": "Stato delle notifiche originali",
+ "description": "Indica se le notifiche erano abilitate nella configurazione statica originale."
+ }
+ },
+ "networking": {
+ "label": "Reti",
+ "description": "Impostazioni relative alla rete, come l'abilitazione di IPv6 per i dispositivi Frigate.",
+ "ipv6": {
+ "label": "Configurazione IPv6",
+ "description": "Impostazioni specifiche IPv6 per i servizi di rete Frigate.",
+ "enabled": {
+ "label": "Abilita IPv6",
+ "description": "Abilita il supporto IPv6 per i servizi Frigate (API e interfaccia utente) ove applicabile."
+ }
+ },
+ "listen": {
+ "label": "Configurazione delle porte di ascolto",
+ "description": "Configurazione per le porte di ascolto interne ed esterne. Questa sezione è destinata agli utenti esperti. Per la maggior parte dei casi, si consiglia di modificare la sezione relativa alle porte nel file Docker Compose.",
+ "internal": {
+ "label": "Porta interna",
+ "description": "Porta di ascolto interna per Frigate (predefinita 5000)."
+ },
+ "external": {
+ "label": "Porta esterna",
+ "description": "Porta di ascolto esterna per Frigate (predefinita 8971)."
+ }
+ }
+ },
+ "tls": {
+ "label": "TLS",
+ "description": "Impostazioni TLS per gli endpoint web di Frigate (porta 8971).",
+ "enabled": {
+ "label": "Abilita TLS",
+ "description": "Abilita il protocollo TLS per l'interfaccia utente web e le API di Frigate sulla porta TLS configurata."
+ }
+ },
+ "telemetry": {
+ "label": "Telemetria",
+ "description": "Opzioni di telemetria e statistiche di sistema, incluso il monitoraggio della GPU e della larghezza di banda di rete.",
+ "network_interfaces": {
+ "label": "Interfacce di rete",
+ "description": "Elenco dei prefissi dei nomi delle interfacce di rete da monitorare per le statistiche sulla larghezza di banda."
+ },
+ "stats": {
+ "label": "Statistiche di sistema",
+ "description": "Opzioni per abilitare/disabilitare la raccolta di varie statistiche di sistema e della GPU.",
+ "amd_gpu_stats": {
+ "label": "Statistiche GPU AMD",
+ "description": "Abilita la raccolta delle statistiche della GPU AMD, se presente una GPU AMD."
+ },
+ "intel_gpu_stats": {
+ "label": "Statistiche GPU Intel",
+ "description": "Abilita la raccolta delle statistiche della GPU Intel, se presente una GPU Intel."
+ },
+ "network_bandwidth": {
+ "label": "Larghezza di banda di rete",
+ "description": "Abilita il monitoraggio della larghezza di banda di rete per singolo processo per i processi ffmpeg della telecamera e per i rilevatori (richiede capability specifiche)."
+ },
+ "intel_gpu_device": {
+ "label": "Dispositivo GPU Intel",
+ "description": "Indirizzo bus PCI o percorso del dispositivo DRM (es. /dev/dri/card1) utilizzato per associare le statistiche della GPU Intel a un dispositivo specifico quando ne sono presenti molteplici."
+ }
+ },
+ "version_check": {
+ "label": "Controllo versione",
+ "description": "Abilita un controllo in uscita per rilevare se è disponibile una versione più recente di Frigate."
+ }
+ },
+ "birdseye": {
+ "label": "Birdseye",
+ "description": "Impostazioni per la vista composita Birdseye che unisce più flussi video di telecamere in un unico formato.",
+ "enabled": {
+ "label": "Abilita Birdseye",
+ "description": "Abilita o disabilita la funzione di visualizzazione Birdseye."
+ },
+ "mode": {
+ "label": "Modalità di tracciamento",
+ "description": "Modalità per includere le telecamere in Birdseye: 'oggetti', 'movimento' o 'continuo'."
+ },
+ "order": {
+ "label": "Posizione",
+ "description": "Posizione numerica che controlla l'ordine delle telecamere nella disposizione Birdseye."
+ }
+ },
+ "model": {
+ "label": "Modello di rilevamento"
+ },
+ "semantic_search": {
+ "label": "Ricerca semantica",
+ "triggers": {
+ "label": "Attivatori",
+ "friendly_name": {
+ "label": "Nome descrittivo",
+ "description": "Nome descrittivo opzionale visualizzato nell'interfaccia utente per questo attivatore."
+ },
+ "description": "Azioni e criteri di corrispondenza per gli attivatori della ricerca semantica specifici della telecamera.",
+ "enabled": {
+ "label": "Abilita questo attivatore",
+ "description": "Abilita o disabilita questo attivatore della ricerca semantica."
+ },
+ "type": {
+ "label": "Tipo di attivatore",
+ "description": "Tipo di attivatore: 'miniatura' (corrispondenza con immagine) o 'descrizione' (corrispondenza con testo)."
+ },
+ "data": {
+ "label": "Contenuto dell'attivatore",
+ "description": "Frase di testo o ID della miniatura da confrontare con gli oggetti tracciati."
+ },
+ "threshold": {
+ "label": "Soglia dell'attivatore",
+ "description": "Punteggio minimo di somiglianza (0-1) richiesto per attivare questo attivatore."
+ },
+ "actions": {
+ "label": "Azioni dell'attivatore",
+ "description": "Elenco delle azioni da eseguire quando l'attivatore trova una corrispondenza (notification, sub_label, attribute)."
+ }
+ },
+ "model_size": {
+ "label": "Dimensioni del modello"
+ }
+ },
+ "lpr": {
+ "label": "Riconoscimento targhe",
+ "model_size": {
+ "label": "Dimensioni del modello"
+ },
+ "description": "Impostazioni di riconoscimento delle targhe, incluse le soglie di rilevamento, la formattazione e le targhe conosciute.",
+ "enabled": {
+ "label": "Abilita riconoscimento delle targhe (LPR)"
+ },
+ "expire_time": {
+ "label": "Scadenza in secondi",
+ "description": "Tempo in secondi trascorso dopo il quale una targa non identificata viene rimossa dal sistema di tracciamento (solo per telecamere LPR dedicate)."
+ },
+ "min_area": {
+ "label": "Area minima della targa",
+ "description": "Area minima della targa (in pixel) richiesta per tentare il riconoscimento."
+ },
+ "enhancement": {
+ "label": "Livello di miglioramento",
+ "description": "Livello di miglioramento (0-10) da applicare alle immagini delle targhe prima dell'OCR; valori più elevati potrebbero non migliorare sempre i risultati; i livelli superiori a 5 potrebbero funzionare solo con immagini di targhe scattate di notte e devono essere utilizzati con cautela."
+ }
+ },
+ "classification": {
+ "label": "Classificazione oggetti",
+ "bird": {
+ "enabled": {
+ "label": "Classificazione uccelli"
+ }
+ }
+ },
+ "genai": {
+ "roles": {
+ "label": "Ruoli"
+ },
+ "model": {
+ "label": "Modello"
+ }
+ },
+ "camera_ui": {
+ "description": "Visualizza l'ordine e la visibilità di questa telecamera nell'interfaccia utente. L'ordine influisce sul cruscotto predefinito. Per un controllo più granulare, utilizza i gruppi di telecamere.",
+ "order": {
+ "description": "L'ordine numerico viene utilizzato per ordinare le telecamere nell'interfaccia utente (cruscotto ed elenchi predefiniti); i numeri più grandi compaiono successivamente."
+ },
+ "dashboard": {
+ "label": "Mostra nell'interfaccia utente",
+ "description": "Abilita o disabilita la visualizzazione di questa telecamera in ogni punto dell'interfaccia utente di Frigate. Disabilitando questa opzione, sarà necessario modificare manualmente la configurazione per visualizzare nuovamente la telecamera nell'interfaccia utente."
+ }
+ },
+ "active_profile": {
+ "label": "Profilo attivo"
}
}
diff --git a/web/public/locales/it/config/validation.json b/web/public/locales/it/config/validation.json
index a37fcd3c71..c6049d5f0b 100644
--- a/web/public/locales/it/config/validation.json
+++ b/web/public/locales/it/config/validation.json
@@ -4,5 +4,32 @@
"exclusiveMinimum": "Deve essere maggiore di {{limit}}",
"exclusiveMaximum": "Deve essere minore di {{limit}}",
"minLength": "Deve essere almeno {{limit}} carattere(i)",
- "maxLength": "Deve essere al massimo {{limit}} carattere(i)"
+ "maxLength": "Deve essere al massimo {{limit}} carattere(i)",
+ "minItems": "Deve contenere almeno {{limit}} elementi",
+ "maxItems": "Deve avere al massimo {{limit}} elementi",
+ "pattern": "Formato non valido",
+ "required": "Questo campo è obbligatorio",
+ "type": "Tipo di valore non valido",
+ "enum": "Deve essere uno dei valori consentiti",
+ "const": "Il valore non corrisponde alla costante prevista",
+ "uniqueItems": "Tutti gli elementi devono essere unici",
+ "format": "Formato non valido",
+ "additionalProperties": "Proprietà sconosciuta non consentita",
+ "oneOf": "Deve corrispondere esattamente a uno degli schemi consentiti",
+ "anyOf": "Deve corrispondere ad almeno uno degli schemi consentiti",
+ "proxy": {
+ "header_map": {
+ "roleHeaderRequired": "L'intestazione del ruolo è obbligatoria quando si configurano le mappature dei ruoli."
+ }
+ },
+ "ffmpeg": {
+ "inputs": {
+ "rolesUnique": "Ciascun ruolo può essere assegnato a un solo flusso di ingresso.",
+ "detectRequired": "Ad almeno un flusso di ingresso deve essere assegnato il ruolo di 'rilevamento'.",
+ "hwaccelDetectOnly": "Solo il flusso di ingresso con il ruolo di rilevamento può definire argomenti di accelerazione hardware."
+ }
+ },
+ "detect": {
+ "dimensionMustBeEven": "Deve essere un numero pari."
+ }
}
diff --git a/web/public/locales/it/objects.json b/web/public/locales/it/objects.json
index 069acd07be..230931d635 100644
--- a/web/public/locales/it/objects.json
+++ b/web/public/locales/it/objects.json
@@ -121,5 +121,9 @@
"royal_mail": "Royal Mail",
"school_bus": "Autobus scolastico",
"skunk": "Puzzola",
- "kangaroo": "Canguro"
+ "kangaroo": "Canguro",
+ "baby": "Bambino",
+ "baby_stroller": "Passeggino per bambini",
+ "rickshaw": "Risciò",
+ "rodent": "Roditore"
}
diff --git a/web/public/locales/it/views/chat.json b/web/public/locales/it/views/chat.json
new file mode 100644
index 0000000000..dfe5166224
--- /dev/null
+++ b/web/public/locales/it/views/chat.json
@@ -0,0 +1,72 @@
+{
+ "documentTitle": "Chat - Frigate",
+ "title": "Chat Frigate",
+ "subtitle": "Il tuo assistente IA per la gestione e l'analisi delle telecamere",
+ "placeholder": "Chiedi qualsiasi cosa...",
+ "error": "Si è verificato un errore. Riprova.",
+ "processing": "Elaborazione in corso...",
+ "toolsUsed": "Utilizzato: {{tools}}",
+ "showTools": "Mostra strumenti ({{count}})",
+ "hideTools": "Nascondi strumenti",
+ "call": "Chiama",
+ "result": "Risultato",
+ "arguments": "Argomenti:",
+ "response": "Risposta:",
+ "attachment_chip_label": "{{label}} sulla {{camera}}",
+ "attachment_chip_remove": "Rimuovi allegato",
+ "open_in_explore": "Apri in Esplora",
+ "attach_event_aria": "Allega evento {{eventId}}",
+ "attachment_picker_paste_label": "Oppure incolla ID dell'evento",
+ "attachment_picker_attach": "Allega",
+ "attachment_picker_placeholder": "Allega un evento",
+ "quick_reply_find_similar": "Trova avvistamenti simili",
+ "quick_reply_tell_me_more": "Raccontami di più su questo",
+ "quick_reply_when_else": "In quali altre occasioni è stato avvistato?",
+ "quick_reply_find_similar_text": "Trova avvistamenti simili a questo.",
+ "quick_reply_tell_me_more_text": "Raccontami di più su questo.",
+ "quick_reply_when_else_text": "Quando è stato osservato in altre volte?",
+ "anchor": "Riferimento",
+ "similarity_score": "Somiglianza",
+ "no_similar_objects_found": "Nessun oggetto simile trovato.",
+ "semantic_search_required": "Per trovare oggetti simili è necessario abilitare la ricerca semantica.",
+ "send": "Invia",
+ "suggested_requests": "Prova a chiedere:",
+ "starting_requests": {
+ "show_recent_events": "Mostra gli eventi recenti",
+ "show_camera_status": "Mostra lo stato della telecamera",
+ "recap": "Cosa è successo mentre ero via?",
+ "watch_camera": "Guarda la telecamera per attività"
+ },
+ "starting_requests_prompts": {
+ "show_recent_events": "Mostrami gli eventi recenti dell'ultima ora",
+ "show_camera_status": "Qual è lo stato attuale delle mie telecamere?",
+ "recap": "Cosa è successo mentre ero via?",
+ "watch_camera": "Controlla la porta d'ingresso e fammi sapere se arriva qualcuno"
+ },
+ "new_chat": "Nuova chat",
+ "settings": {
+ "title": "Impostazioni chat",
+ "show_stats": {
+ "title": "Mostra statistiche",
+ "desc": "Mostra la frequenza di generazione e la dimensione del contesto per le risposte in chat.",
+ "while_generating": "Durante la generazione",
+ "always": "Sempre"
+ },
+ "auto_scroll": {
+ "title": "Scorrimento automatico",
+ "desc": "Segui i nuovi messaggi non appena arrivano."
+ }
+ },
+ "stats": {
+ "context": "{{tokens}} token",
+ "tokens_per_second": "{{rate}} t/s"
+ },
+ "reasoning": {
+ "active": "Ragionamento…",
+ "show": "Mostra il ragionamento",
+ "hide": "Nascondi il ragionamento"
+ },
+ "thinking": {
+ "toggle": "Alterna ragionamento"
+ }
+}
diff --git a/web/public/locales/it/views/events.json b/web/public/locales/it/views/events.json
index f1a9255f7e..45a4664066 100644
--- a/web/public/locales/it/views/events.json
+++ b/web/public/locales/it/views/events.json
@@ -2,7 +2,7 @@
"alerts": "Avvisi",
"detections": "Rilevamenti",
"motion": {
- "label": "Movimenti",
+ "label": "Movimento",
"only": "Solo movimenti"
},
"empty": {
@@ -20,9 +20,11 @@
},
"markTheseItemsAsReviewed": "Segna questi elementi come visti",
"markAsReviewed": "Segna come visto",
- "documentTitle": "Rivedi - Frigate",
- "allCameras": "Tutte le camere",
- "timeline": "Cronologia",
+ "documentTitle": "Revisiona - Frigate",
+ "allCameras": "Tutte le telecamere",
+ "timeline": {
+ "label": "Linea temporale"
+ },
"timeline.aria": "Seleziona la cronologia",
"events": {
"label": "Eventi",
@@ -30,7 +32,9 @@
"noFoundForTimePeriod": "Nessun evento trovato per questo intervallo."
},
"recordings": {
- "documentTitle": "Registrazioni - Frigate"
+ "documentTitle": "Registrazioni - Frigate",
+ "invalidSharedLink": "Impossibile aprire il collegamento alla registrazione con orario a causa di un errore di analisi.",
+ "invalidSharedCamera": "Impossibile aprire il collegamento alla registrazione con orario a causa di una telecamera sconosciuta o non autorizzata."
},
"calendarFilter": {
"last24Hours": "Ultime 24 ore"
@@ -44,7 +48,7 @@
"threateningActivity": "Attività minacciosa",
"detail": {
"noDataFound": "Nessun dato dettagliato da rivedere",
- "aria": "Attiva/disattiva la visualizzazione dettagliata",
+ "aria": "Abilita/disabilita la visualizzazione dettagliata",
"trackedObject_one": "{{count}} oggetto",
"trackedObject_other": "{{count}} oggetti",
"noObjectDetailData": "Non sono disponibili dati dettagliati sull'oggetto.",
@@ -64,5 +68,28 @@
"normalActivity": "Normale",
"needsReview": "Necessita revisione",
"securityConcern": "Rischio per la sicurezza",
- "select_all": "Tutti"
+ "select_all": "Tutti",
+ "motionSearch": {
+ "menuItem": "Ricerca movimento",
+ "openMenu": "Opzioni telecamera"
+ },
+ "motionPreviews": {
+ "title": "Anteprime di movimento: {{camera}}",
+ "mobileSettingsTitle": "Impostazioni di anteprima del movimento",
+ "mobileSettingsDesc": "Regola la velocità di riproduzione e la luminosità, poi scegli una data per rivedere i filmati che mostrano solo il movimento.",
+ "dim": "Attenua",
+ "dimAria": "Regola l'intensità della luce",
+ "dimDesc": "Aumenta l'attenuazione per migliorare la visibilità delle aree in movimento.",
+ "speed": "Velocità",
+ "speedAria": "Seleziona la velocità di riproduzione dell'anteprima",
+ "speedDesc": "Scegli la velocità di riproduzione dei video di anteprima.",
+ "back": "Indietro",
+ "empty": "Nessuna anteprima disponibile",
+ "noPreview": "Anteprima non disponibile",
+ "seekAria": "Cerca il riproduttore {{camera}} a {{time}}",
+ "filter": "Filtro",
+ "filterDesc": "Seleziona le aree per visualizzare solo i video con movimento in quelle regioni.",
+ "filterClear": "Pulisci",
+ "menuItem": "Visualizza le anteprime del movimento"
+ }
}
diff --git a/web/public/locales/it/views/explore.json b/web/public/locales/it/views/explore.json
index 7cb9b4b806..e08626bfa0 100644
--- a/web/public/locales/it/views/explore.json
+++ b/web/public/locales/it/views/explore.json
@@ -202,8 +202,8 @@
"aria": "Invia a Frigate Plus"
},
"addTrigger": {
- "label": "Aggiungi innesco",
- "aria": "Aggiungi un innesco per questo oggetto tracciato"
+ "label": "Aggiungi attivatore",
+ "aria": "Aggiungi un attivatore per questo oggetto tracciato"
},
"audioTranscription": {
"label": "Trascrivere",
@@ -224,8 +224,8 @@
"aria": "Scarica istantanea pulita"
},
"debugReplay": {
- "label": "Riproduzione di correzione",
- "aria": "Visualizza questo oggetto tracciato nella vista di riproduzione di correzione"
+ "label": "Riproduzione di correzioni",
+ "aria": "Visualizza questo oggetto tracciato nella vista di riproduzione di correzioni"
},
"more": {
"aria": "Altri"
@@ -289,7 +289,10 @@
"zones": "Zone",
"ratio": "Rapporto",
"area": "Area",
- "score": "Punteggio"
+ "score": "Punteggio",
+ "computedScore": "Punteggio calcolato",
+ "topScore": "Punteggio massimo",
+ "toggleAdvancedScores": "Attiva/disattiva i punteggi avanzati"
}
},
"annotationSettings": {
diff --git a/web/public/locales/it/views/exports.json b/web/public/locales/it/views/exports.json
index 63bebbefa7..df3ee48982 100644
--- a/web/public/locales/it/views/exports.json
+++ b/web/public/locales/it/views/exports.json
@@ -14,7 +14,9 @@
"toast": {
"error": {
"renameExportFailed": "Impossibile rinominare l'esportazione: {{errorMessage}}",
- "assignCaseFailed": "Impossibile aggiornare l'assegnazione del caso: {{errorMessage}}"
+ "assignCaseFailed": "Impossibile aggiornare l'assegnazione del caso: {{errorMessage}}",
+ "caseSaveFailed": "Impossibile salvare il caso: {{errorMessage}}",
+ "caseDeleteFailed": "Impossibile eliminare il caso: {{errorMessage}}"
}
},
"tooltip": {
@@ -22,7 +24,8 @@
"downloadVideo": "Scarica video",
"editName": "Modifica nome",
"deleteExport": "Elimina esportazione",
- "assignToCase": "Aggiungi al caso"
+ "assignToCase": "Aggiungi al caso",
+ "removeFromCase": "Rimuovi dal caso"
},
"headings": {
"cases": "Casi",
@@ -35,5 +38,91 @@
"newCaseOption": "Crea un nuovo caso",
"nameLabel": "Nome del caso",
"descriptionLabel": "Descrizione"
+ },
+ "toolbar": {
+ "newCase": "Nuovo caso",
+ "addExport": "Aggiungi esportazione",
+ "editCase": "Modifica caso",
+ "deleteCase": "Elimina il caso"
+ },
+ "deleteCase": {
+ "label": "Eliminare il caso",
+ "desc": "Sei sicuro di voler eliminare {{caseName}}?",
+ "descKeepExports": "Le esportazioni rimarranno disponibili come esportazioni non categorizzate.",
+ "descDeleteExports": "In questo caso, tutte le esportazioni verranno eliminate definitivamente.",
+ "deleteExports": "Elimina anche le esportazioni"
+ },
+ "caseCard": {
+ "emptyCase": "Ancora nessuna esportazione"
+ },
+ "jobCard": {
+ "defaultName": "{{camera}} esporta",
+ "queued": "In coda",
+ "running": "In esecuzione",
+ "preparing": "Preparazione",
+ "copying": "Copia",
+ "encoding": "Codifica",
+ "encodingRetry": "Codifica (riprova)",
+ "finalizing": "Finalizzazione"
+ },
+ "caseView": {
+ "noDescription": "Nessuna descrizione",
+ "createdAt": "Creato {{value}}",
+ "exportCount_one": "1 esportazione",
+ "exportCount_other": "{{count}} esportazioni",
+ "cameraCount_one": "1 telecamera",
+ "cameraCount_other": "{{count}} telecamere",
+ "showMore": "Mostra altro",
+ "showLess": "Mostra meno",
+ "emptyTitle": "Questo caso è vuoto",
+ "emptyDescription": "Aggiungi le esportazioni esistenti non categorizzate per mantenere il caso organizzato.",
+ "emptyDescriptionNoExports": "Al momento non sono disponibili esportazioni non categorizzate da aggiungere."
+ },
+ "caseEditor": {
+ "createTitle": "Crea un caso",
+ "editTitle": "Modifica caso",
+ "namePlaceholder": "Nome del caso",
+ "descriptionPlaceholder": "Aggiungi note o contesto per questo caso"
+ },
+ "addExportDialog": {
+ "title": "Aggiungi l'esportazione a {{caseName}}",
+ "searchPlaceholder": "Cerca esportazioni non categorizzate",
+ "empty": "Nessuna esportazione non categorizzata corrisponde a questa ricerca.",
+ "addButton_one": "Aggiungi 1 esportazione",
+ "addButton_other": "Aggiungi {{count}} esportazioni",
+ "adding": "In aggiunta..."
+ },
+ "selected_one": "{{count}} selezionati",
+ "selected_other": "{{count}} selezionati",
+ "bulkActions": {
+ "addToCase": "Aggiungi al caso",
+ "moveToCase": "Sposta al caso",
+ "removeFromCase": "Rimuovi dal caso",
+ "delete": "Elimina",
+ "deleteNow": "Elimina ora"
+ },
+ "bulkDelete": {
+ "title": "Elimina le esportazioni",
+ "desc_one": "Sei sicuro di voler eliminare l'esportazione {{count}}?",
+ "desc_other": "Sei sicuro di voler eliminare {{count}} esportazioni?"
+ },
+ "bulkRemoveFromCase": {
+ "title": "Rimuovi dal caso",
+ "desc_one": "Rimuovere l'esportazione {{count}} da questo caso?",
+ "desc_other": "Rimuovere {{count}} esportazioni da questo caso?",
+ "descKeepExports": "Le esportazioni verranno spostate nella categoria \"non classificate\".",
+ "descDeleteExports": "Le esportazioni verranno eliminate definitivamente.",
+ "deleteExports": "Elimina almeno le esportazioni"
+ },
+ "bulkToast": {
+ "success": {
+ "delete": "Esportazioni eliminate con successo",
+ "reassign": "Assegnazione del caso aggiornata con successo",
+ "remove": "Esportazioni rimosse con successo dal caso"
+ },
+ "error": {
+ "deleteFailed": "Impossibile eliminare le esportazioni: {{errorMessage}}",
+ "reassignFailed": "Impossibile aggiornare l'assegnazione del caso: {{errorMessage}}"
+ }
}
}
diff --git a/web/public/locales/it/views/faceLibrary.json b/web/public/locales/it/views/faceLibrary.json
index 12d640aa8f..842881fc69 100644
--- a/web/public/locales/it/views/faceLibrary.json
+++ b/web/public/locales/it/views/faceLibrary.json
@@ -20,7 +20,11 @@
"title": "Riconoscimenti recenti",
"aria": "Seleziona i riconoscimenti recenti",
"empty": "Non ci sono recenti tentativi di riconoscimento facciale",
- "titleShort": "Recente"
+ "titleShort": "Recente",
+ "emptyNoLibrary": {
+ "title": "Carica un volto",
+ "description": "Affinché il riconoscimento facciale funzioni è necessario aggiungere almeno un volto alla libreria."
+ }
},
"button": {
"addFace": "Aggiungi volto",
diff --git a/web/public/locales/it/views/live.json b/web/public/locales/it/views/live.json
index 7aa3302c94..ac9fc12099 100644
--- a/web/public/locales/it/views/live.json
+++ b/web/public/locales/it/views/live.json
@@ -9,7 +9,7 @@
"disable": "Disabilita audio bidirezionale"
},
"snapshots": {
- "enable": "Abilita istantanee",
+ "enable": "Abilita Istantanee",
"disable": "Disabilita istantanee"
},
"manualRecording": {
@@ -40,7 +40,8 @@
"objectDetection": "Rilevamento oggetti",
"recording": "Registrazione",
"audioDetection": "Rilevamento audio",
- "transcription": "Trascrizione audio"
+ "transcription": "Trascrizione audio",
+ "camera": "Telecamera"
},
"history": {
"label": "Mostra filmati storici"
@@ -98,7 +99,9 @@
},
"camera": {
"enable": "Abilita telecamera",
- "disable": "Disabilita telecamera"
+ "disable": "Disabilita telecamera",
+ "turnOn": "Attiva la telecamera",
+ "turnOff": "Disattiva la telecamera"
},
"muteCameras": {
"enable": "Muta tutte le telecamere",
@@ -110,7 +113,8 @@
},
"recording": {
"enable": "Abilita registrazione",
- "disable": "Disabilita registrazione"
+ "disable": "Disabilita registrazione",
+ "disabledInConfig": "Per questa telecamera è necessario prima abilitare la registrazione nelle impostazioni."
},
"audioDetect": {
"enable": "Abilita rilevamento audio",
@@ -157,7 +161,7 @@
},
"effectiveRetainMode": {
"modes": {
- "all": "Tutto",
+ "all": "Tutti",
"motion": "Movimento",
"active_objects": "Oggetti attivi"
},
diff --git a/web/public/locales/it/views/motionSearch.json b/web/public/locales/it/views/motionSearch.json
new file mode 100644
index 0000000000..4421f8bcef
--- /dev/null
+++ b/web/public/locales/it/views/motionSearch.json
@@ -0,0 +1,82 @@
+{
+ "documentTitle": "Ricerca movimenti - Frigate",
+ "title": "Ricerca movimenti",
+ "description": "Disegna un poligono per definire la regione di interesse e specifica un intervallo di tempo per la ricerca di cambiamenti di movimento all'interno di tale regione.",
+ "selectCamera": "La ricerca di movimenti è in fase di caricamento",
+ "startSearch": "Avvia ricerca",
+ "searchStarted": "Ricerca avviata",
+ "searchCancelled": "Ricerca annullata",
+ "cancelSearch": "Annulla",
+ "searching": "Ricerca in corso.",
+ "searchComplete": "Ricerca completata",
+ "noResultsYet": "Esegui una ricerca per trovare le variazioni di movimento nella regione selezionata",
+ "noChangesFound": "Nessuna modifica dei pixel rilevata nella regione selezionata",
+ "changesFound_one": "Trovato {{count}} cambiamento di movimento",
+ "changesFound_many": "Trovati {{count}} cambiamenti di movimento",
+ "changesFound_other": "Trovati {{count}} cambiamenti di movimento",
+ "framesProcessed": "{{count}} fotogrammi elaborati",
+ "jumpToTime": "Vai a questo momento",
+ "results": "Risultati",
+ "showSegmentHeatmap": "Mappa di calore",
+ "newSearch": "Nuova ricerca",
+ "clearResults": "Pulisci risultati",
+ "clearROI": "Pulisci poligono",
+ "polygonControls": {
+ "points_one": "{{count}} punto",
+ "points_many": "{{count}} punti",
+ "points_other": "{{count}} punti",
+ "undo": "Annulla ultimo punto",
+ "reset": "Reimposta poligono",
+ "drawMode": "Disegna",
+ "moveMode": "Sposta"
+ },
+ "motionHeatmapLabel": "Mappa di calore del movimento",
+ "dialog": {
+ "title": "Ricerca di movimento",
+ "cameraLabel": "Telecamera",
+ "previewAlt": "Anteprima della telecamera per {{camera}}"
+ },
+ "timeRange": {
+ "title": "Intervallo di ricerca",
+ "start": "Ora di inizio",
+ "end": "Ora di fine"
+ },
+ "settings": {
+ "title": "Impostazioni di ricerca",
+ "parallelMode": "Modalità parallela",
+ "parallelModeDesc": "Esegui la scansione simultanea di più intervalli di registrazione (più veloce; utilizza più risorse di decodifica)",
+ "threshold": "Soglia di sensibilità",
+ "thresholdDesc": "Valori più bassi indicano cambiamenti minori (1-255)",
+ "minArea": "Area di cambio minimo",
+ "minAreaDesc": "Dimensione minima di una singola regione mobile, espressa in percentuale della regione di interesse",
+ "frameSkip": "Salta fotogrammi",
+ "frameSkipDesc": "Elabora ogni N-esimo fotogramma. Imposta questo valore sulla frequenza dei fotogrammi della tua telecamera per elaborare un fotogramma al secondo (ad esempio, 5 per una telecamera a 5 FPS, 30 per una telecamera a 30 FPS). Valori più alti saranno più veloci, ma potrebbero perdere eventi di movimento brevi.",
+ "maxResults": "Risultati massimi",
+ "maxResultsDesc": "Interrompi dopo questo numero di orario corrispondenti"
+ },
+ "errors": {
+ "noCamera": "Seleziona una telecamera",
+ "noROI": "Disegna una regione di interesse",
+ "noTimeRange": "Seleziona un intervallo di tempo",
+ "invalidTimeRange": "L'ora di fine deve essere successiva all'ora di inizio",
+ "searchFailed": "Ricerca non riuscita: {{message}}",
+ "polygonTooSmall": "Il poligono deve avere almeno 3 punti",
+ "unknown": "Errore sconosciuto"
+ },
+ "changePercentage": "{{percentage}}% modificato",
+ "metrics": {
+ "title": "Metriche di ricerca",
+ "segmentsScanned": "Segmenti scansionati",
+ "segmentsProcessed": "Elaborati",
+ "segmentsSkippedInactive": "Saltati (nessuna attività)",
+ "segmentsSkippedHeatmap": "Saltati (nessuna sovrapposizione del ROI)",
+ "fallbackFullRange": "Scansione gamma completa alternativa",
+ "framesDecoded": "Fotogrammi decodificati",
+ "wallTime": "Tempo di ricerca",
+ "segmentErrors": "Errori di segmento",
+ "seconds": "{{seconds}}s",
+ "minutesSeconds": "{{minutes}}m {{seconds}}s",
+ "scanSummary": "{{segments}} segmenti · {{time}}"
+ },
+ "scanning": "Scansione {{time}}"
+}
diff --git a/web/public/locales/it/views/replay.json b/web/public/locales/it/views/replay.json
new file mode 100644
index 0000000000..fc698eb04a
--- /dev/null
+++ b/web/public/locales/it/views/replay.json
@@ -0,0 +1,59 @@
+{
+ "title": "Riproduzione correzioni",
+ "description": "Riproduci le registrazioni della telecamera per le correzioni. L'elenco degli oggetti mostra un riepilogo ritardato degli oggetti rilevati e la scheda Messaggi mostra un flusso di messaggi interni di Frigate tratti dal filmato riprodotto.",
+ "websocket_messages": "Messaggi",
+ "dialog": {
+ "title": "Avvia la riproduzione delle correzioni",
+ "description": "Crea una telecamera di riproduzione temporanea che riproduca in ciclo i filmati storici per la correzione dei problemi di rilevamento e tracciamento degli oggetti. La telecamera di riproduzione avrà la stessa configurazione di rilevamento della telecamera sorgente. Scegli un intervallo di tempo da cui iniziare.",
+ "camera": "Telecamera sorgente",
+ "timeRange": "Intervallo di tempo",
+ "preset": {
+ "1m": "Ultimo minuto",
+ "5m": "Ultimi 5 minuti",
+ "timeline": "Dalla cronologia",
+ "custom": "Personalizza"
+ },
+ "startButton": "Avvia riproduzione",
+ "selectFromTimeline": "Seleziona",
+ "starting": "Avvio riproduzione...",
+ "startLabel": "Inizio",
+ "endLabel": "Fine",
+ "toast": {
+ "error": "Impossibile avviare la riproduzione di correzioni: {{error}}",
+ "alreadyActive": "È già attiva una sessione di riproduzione",
+ "stopError": "Impossibile interrompere la riproduzione di correzioni: {{error}}",
+ "goToReplay": "Vai alla riproduzione"
+ }
+ },
+ "page": {
+ "noSession": "Nessuna sessione di riproduzione di correzioni attiva",
+ "noSessionDesc": "Avvia una riproduzione di correzioni dalla visualizzazione Cronologia facendo clic sul pulsante Azioni nella barra degli strumenti e scegliendo Riproduzione di correzioni.",
+ "goToRecordings": "Vai alla Cronologia",
+ "preparingClip": "Preparazione video…",
+ "preparingClipDesc": "Frigate sta unendo le registrazioni relative all'intervallo di tempo selezionato. Questa operazione può richiedere un minuto per intervalli più lunghi.",
+ "startingCamera": "Avvio della riproduzione di correzioni…",
+ "startError": {
+ "title": "Impossibile avviare la riproduzione di correzioni",
+ "back": "Torna alla Cronologia"
+ },
+ "sourceCamera": "Telecamera sorgente",
+ "replayCamera": "Telecamera di riproduzione",
+ "initializingReplay": "Inizializzazione riproduzione di correzioni...",
+ "stoppingReplay": "Interruzione riproduzione di correzioni...",
+ "stopReplay": "Ferma riproduzione",
+ "confirmStop": {
+ "title": "Interrompere la riproduzione di correzioni?",
+ "description": "In questo modo la sessione verrà interrotta e tutti i dati temporanei verranno eliminati. Sei sicuro?",
+ "confirm": "Ferma riproduzione",
+ "cancel": "Annulla"
+ },
+ "activity": "Attività",
+ "objects": "Elenco degli oggetti",
+ "audioDetections": "Rilevamento audio",
+ "noActivity": "Nessuna attività rilevata",
+ "activeTracking": "Tracciamento attivo",
+ "noActiveTracking": "Nessun tracciamento attivo",
+ "configuration": "Configurazione",
+ "configurationDesc": "Regola con precisione le impostazioni di rilevamento del movimento e tracciamento degli oggetti per la telecamera Riproduzione di correzioni. Nessuna modifica verrà salvata nel file di configurazione di Frigate."
+ }
+}
diff --git a/web/public/locales/it/views/settings.json b/web/public/locales/it/views/settings.json
index 38951855ed..9ef1f0f295 100644
--- a/web/public/locales/it/views/settings.json
+++ b/web/public/locales/it/views/settings.json
@@ -9,10 +9,15 @@
"object": "Correzioni - Frigate",
"general": "Impostazioni interfaccia - Frigate",
"frigatePlus": "Impostazioni Frigate+ - Frigate",
- "notifications": "Impostazioni di notifiche - Frigate",
+ "notifications": "Impostazioni di notifica - Frigate",
"enrichments": "Impostazioni di miglioramento - Frigate",
"cameraManagement": "Gestisci telecamere - Frigate",
- "cameraReview": "Impostazioni revisione telecamera - Frigate"
+ "cameraReview": "Impostazioni revisione telecamera - Frigate",
+ "globalConfig": "Configurazione globale - Frigate",
+ "cameraConfig": "Configurazione telecamera - Frigate",
+ "maintenance": "Manutenzione - Frigate",
+ "profiles": "Profili - Frigate",
+ "detectorsAndModel": "Rilevatori e modelli - Frigate"
},
"frigatePlus": {
"snapshotConfig": {
@@ -22,7 +27,7 @@
"camera": "Telecamera",
"cleanCopySnapshots": "Istantanee clean_copy"
},
- "desc": "Per inviare a Frigate+ è necessario che nella configurazione siano abilitate sia le istantanee che le istantanee clean_copy.",
+ "desc": "Per inviare i dati a Frigate+ è necessario abilitare le istantanee nella configurazione.",
"documentation": "Leggi la documentazione",
"title": "Configurazione istantanee"
},
@@ -41,14 +46,22 @@
"userModel": "Messa a punto fine",
"baseModel": "Modello base"
},
- "availableModels": "Modelli disponibili",
+ "availableModels": "Modelli Frigate+ disponibili",
"loadingAvailableModels": "Caricamento dei modelli disponibili…",
"supportedDetectors": "Rilevatori supportati",
"error": "Impossibile caricare le informazioni sul modello",
"modelType": "Tipo di modello",
"modelSelect": "Qui puoi selezionare i modelli disponibili su Frigate+. Nota: puoi selezionare solo i modelli compatibili con la configurazione attuale del tuo rilevatore.",
"title": "Informazioni sul modello",
- "loading": "Caricamento informazioni sul modello…"
+ "loading": "Caricamento informazioni sul modello…",
+ "noModelsAvailable": "Nessun modello disponibile",
+ "noModelLoaded": "Al momento non è caricato alcun modello di Frigate+.",
+ "selectModel": "Seleziona un modello",
+ "filter": {
+ "ariaLabel": "Filtra i modelli per tipo",
+ "baseModels": "Modelli base",
+ "fineTunedModels": "Modelli ottimizzati"
+ }
},
"toast": {
"error": "Impossibile salvare le modifiche alla configurazione: {{errorMessage}}",
@@ -56,7 +69,15 @@
},
"title": "Impostazioni Frigate+",
"restart_required": "Riavvio richiesto (modello Frigate+ modificato)",
- "unsavedChanges": "Modifiche alle impostazioni di Frigate+ non salvate"
+ "unsavedChanges": "Modifiche alle impostazioni di Frigate+ non salvate",
+ "description": "Frigate+ è un servizio in abbonamento che offre accesso a funzionalità e capacità aggiuntive per la tua istanza di Frigate, tra cui la possibilità di utilizzare modelli di rilevamento oggetti personalizzati addestrati sui tuoi dati. Puoi gestire le impostazioni del tuo modello Frigate+ qui.",
+ "cardTitles": {
+ "api": "API",
+ "currentModel": "Modello attuale",
+ "otherModels": "Altri modelli",
+ "configuration": "Configurazione"
+ },
+ "changeInDetectorsAndModel": "Cambia modello"
},
"debug": {
"timestamp": {
@@ -146,6 +167,12 @@
"title": "{{polygonName}} è stato salvato.",
"noName": "La maschera di movimento è stata salvata."
}
+ },
+ "defaultName": "Maschera di movimento {{number}}",
+ "name": {
+ "title": "Nome",
+ "description": "Un nome descrittivo opzionale per questa maschera di movimento.",
+ "placeholder": "Inserisci un nome..."
}
},
"form": {
@@ -185,7 +212,11 @@
"type": {
"zone": "zona",
"motion_mask": "maschera di movimento",
- "object_mask": "maschera di oggetto"
+ "object_mask": "maschera oggetto"
+ },
+ "revertOverride": {
+ "title": "Ripristina la configurazione di base",
+ "desc": "Questo rimuoverà la sovrascrittura del profilo per {{type}} {{name}} e ripristinerà la configurazione di base."
}
},
"inertia": {
@@ -202,6 +233,17 @@
"error": {
"mustBeGreaterOrEqualTo": "La soglia di velocità deve essere maggiore o uguale a 0,1."
}
+ },
+ "id": {
+ "error": {
+ "mustNotBeEmpty": "L'ID non deve essere vuoto.",
+ "alreadyExists": "Esiste già una maschera con questo ID per questa telecamera."
+ }
+ },
+ "name": {
+ "error": {
+ "mustNotBeEmpty": "Il nome non deve essere vuoto."
+ }
}
},
"filter": {
@@ -230,7 +272,7 @@
"desc": "Specifica una velocità minima affinché gli oggetti vengano presi in considerazione in questa zona.",
"toast": {
"error": {
- "pointLengthError": "La stima della velocità è stata disattivata per questa zona. Le zone con stima della velocità devono avere esattamente 4 punti.",
+ "pointLengthError": "La stima della velocità è stata disabilitata per questa zona. Le zone con stima della velocità devono avere esattamente 4 punti.",
"loiteringTimeError": "Le zone con tempi di permanenza superiori a 0 non devono essere utilizzate per la stima della velocità."
}
},
@@ -267,6 +309,10 @@
"allObjects": "Tutti gli oggetti",
"toast": {
"success": "La zona ({{zoneName}}) è stata salvata."
+ },
+ "enabled": {
+ "title": "Abilitata",
+ "description": "Indica se questa zona è attiva e abilitata nel file di configurazione. Se disabilitata, non può essere abilitata tramite MQTT. Le zone disabilitate vengono ignorate in fase di esecuzione."
}
},
"objectMasks": {
@@ -293,11 +339,26 @@
}
},
"label": "Maschere di oggetti",
- "documentTitle": "Modifica maschera oggetti - Frigate"
+ "documentTitle": "Modifica maschera oggetti - Frigate",
+ "name": {
+ "title": "Nome",
+ "placeholder": "Inserisci un nome...",
+ "description": "Un nome descrittivo facoltativo per questa maschera oggetto."
+ }
},
"restart_required": "Riavvio richiesto (maschere/zone modificate)",
"motionMaskLabel": "Maschera di movimento {{number}}",
- "objectMaskLabel": "Maschera di oggetto {{number}}"
+ "objectMaskLabel": "Maschera di oggetto {{number}}",
+ "masks": {
+ "enabled": {
+ "title": "Abilitata",
+ "description": "Indica se questa maschera è abilitata nel file di configurazione. Se disabilitata, non può essere abilitata tramite MQTT. Le maschere disabilitate vengono ignorate in fase di esecuzione."
+ }
+ },
+ "disabledInConfig": "L'elemento è disabilitato nel file di configurazione",
+ "addDisabledProfile": "Aggiungi prima alla configurazione di base, poi sovrascrivi nel profilo",
+ "profileBase": "(base)",
+ "profileOverride": "(sovrascrivi)"
},
"cameraSetting": {
"camera": "Telecamera",
@@ -380,27 +441,86 @@
"notifications": "Notifiche",
"ui": "Interfaccia utente",
"classification": "Classificazione",
- "cameras": "Impostazioni telecamera",
+ "cameras": "Configurazione telecamera",
"masksAndZones": "Maschere / Zone",
"debug": "Correzioni",
"users": "Utenti",
"frigateplus": "Frigate+",
"enrichments": "Miglioramenti",
- "triggers": "Inneschi",
+ "triggers": "Attivatori",
"roles": "Ruoli",
- "cameraManagement": "Gestione",
- "cameraReview": "Rivedi",
- "profiles": "Profili"
+ "cameraManagement": "Gestione della telecamera",
+ "cameraReview": "Revisiona",
+ "profiles": "Profili",
+ "general": "Generale",
+ "globalConfig": "Configurazione globale",
+ "system": "Sistema",
+ "integrations": "Integrazioni",
+ "uiSettings": "Impostazioni interfaccia utente",
+ "globalDetect": "Rilevamento oggetti",
+ "globalRecording": "Registrazione",
+ "globalSnapshots": "Istantanee",
+ "globalFfmpeg": "FFmpeg",
+ "globalMotion": "Rilevamento movimento",
+ "globalObjects": "Oggetti",
+ "globalReview": "Revisiona",
+ "globalAudioEvents": "Rilevamento audio",
+ "globalLivePlayback": "Riproduzione in diretta",
+ "globalTimestampStyle": "Stile orario",
+ "systemDatabase": "Database",
+ "systemTls": "TLS",
+ "systemAuthentication": "Autenticazione",
+ "systemNetworking": "Reti",
+ "systemProxy": "Proxy",
+ "systemUi": "Interfaccia utente",
+ "systemLogging": "Registro",
+ "systemEnvironmentVariables": "Variabili d'ambiente",
+ "systemTelemetry": "Telemetria",
+ "systemBirdseye": "Birdseye",
+ "systemFfmpeg": "FFmpeg",
+ "systemDetectorHardware": "Dispositivo di rilevamento",
+ "systemDetectionModel": "Modello di rilevamento",
+ "systemMqtt": "MQTT",
+ "systemGo2rtcStreams": "Flussi go2rtc",
+ "integrationSemanticSearch": "Ricerca semantica",
+ "integrationGenerativeAi": "IA Generativa",
+ "integrationFaceRecognition": "Riconoscimento facciale",
+ "integrationLpr": "Riconoscimento targhe",
+ "integrationObjectClassification": "Classificazione oggetti",
+ "integrationAudioTranscription": "Trascrizione audio",
+ "cameraDetect": "Rilevamento oggetti",
+ "cameraFfmpeg": "FFmpeg",
+ "cameraRecording": "Registrazione",
+ "cameraSnapshots": "Istantanee",
+ "cameraMotion": "Rilevamento movimento",
+ "cameraObjects": "Oggetti",
+ "cameraConfigReview": "Revisiona",
+ "cameraAudioEvents": "Rilevamento audio",
+ "cameraAudioTranscription": "Trascrizione audio",
+ "cameraNotifications": "Notifiche",
+ "cameraLivePlayback": "Riproduzione in diretta",
+ "cameraBirdseye": "Birdseye",
+ "cameraFaceRecognition": "Riconoscimento facciale",
+ "cameraLpr": "Riconoscimento targhe",
+ "cameraMqttConfig": "MQTT",
+ "cameraOnvif": "ONVIF",
+ "cameraTimestampStyle": "Stile orario",
+ "cameraUi": "Interfaccia utente telecamera",
+ "mediaSync": "Sincronizzazione multimediale",
+ "cameraMqtt": "MQTT telecamera",
+ "maintenance": "Manutenzione",
+ "regionGrid": "Griglia di regioni",
+ "systemDetectorsAndModel": "Rilevatori e modelli"
},
"users": {
"dialog": {
"changeRole": {
"roleInfo": {
- "viewerDesc": "Limitato solo alle schermate dal vivo, alle revisioni, alle esplorazioni e alle esportazioni.",
+ "viewerDesc": "Limitato solo ai cruscotti dal vivo, alle revisioni, alle esplorazioni e alle esportazioni.",
"intro": "Seleziona il ruolo appropriato per questo utente:",
"admin": "Amministratore",
"adminDesc": "Accesso completo a tutte le funzionalità.",
- "viewer": "Spettatore",
+ "viewer": "Visualizzatore",
"customDesc": "Ruolo personalizzato con accesso specifico alla telecamera."
},
"title": "Cambia ruolo utente",
@@ -424,7 +544,7 @@
"placeholder": "Conferma password"
},
"strength": {
- "title": "Forza della password: ",
+ "title": "Complessità della password: ",
"weak": "Debole",
"medium": "Media",
"strong": "Forte",
@@ -511,14 +631,14 @@
"general": {
"liveDashboard": {
"automaticLiveView": {
- "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.",
+ "desc": "Passa automaticamente alla visualizzazione dal vivo di una telecamera quando viene rilevata attività. Disabilitando questa opzione, le immagini statiche della telecamera nel cruscotto dal vivo verranno aggiornate solo una volta al minuto.",
"label": "Visualizzazione automatica dal vivo"
},
"playAlertVideos": {
"label": "Riproduci video di avvisi",
- "desc": "Per impostazione predefinita, gli avvisi recenti nella schermata dal vivo vengono riprodotti come brevi video in ciclo. Disattiva questa opzione per visualizzare solo un'immagine statica degli avvisi recenti su questo dispositivo/browser."
+ "desc": "Per impostazione predefinita, gli avvisi recenti nel cruscotto dal vivo vengono riprodotti come brevi video in ciclo. Disabilita questa opzione per visualizzare solo un'immagine statica degli avvisi recenti su questo dispositivo/browser."
},
- "title": "Schermata dal vivo",
+ "title": "Cruscotto dal vivo",
"displayCameraNames": {
"label": "Mostra sempre i nomi delle telecamere",
"desc": "Mostra sempre i nomi delle telecamere in una scheda nel cruscotto della visualizzazione dal vivo multi telecamera."
@@ -528,7 +648,7 @@
"desc": "Quando la trasmissione dal vivo ad alta qualità di una telecamera non è disponibile, dopo questo numero di secondi torna alla modalità a bassa larghezza di banda. Valore predefinito: 3."
}
},
- "title": "Impostazioni interfaccia",
+ "title": "Impostazioni interfaccia utente",
"storedLayouts": {
"title": "Formati memorizzati",
"desc": "La disposizione delle telecamere in un gruppo può essere trascinata/ridimensionata. Le posizioni vengono salvate nella memoria locale del browser.",
@@ -552,7 +672,7 @@
"label": "Primo giorno della settimana",
"desc": "Giorno in cui iniziano le settimane del calendario di revisione.",
"sunday": "Domenica",
- "monday": "Lunedi"
+ "monday": "Lunedì"
}
},
"toast": {
@@ -630,7 +750,7 @@
},
"dialog": {
"unsavedChanges": {
- "title": "Ci sono modifiche non salvate.",
+ "title": "Sono presenti modifiche non salvate.",
"desc": "Vuoi salvare le modifiche prima di continuare?"
}
},
@@ -661,7 +781,7 @@
"email": {
"placeholder": "es. esempio@email.com",
"desc": "È richiesto un indirizzo email valido che verrà utilizzato per avvisarti in caso di problemi con il servizio push.",
- "title": "E-mail"
+ "title": "Email"
},
"cameras": {
"title": "Telecamere",
@@ -690,7 +810,8 @@
"notificationUnavailable": {
"desc": "Le notifiche push web richiedono un contesto sicuro (https://...). Questa è una limitazione del browser. Accedi a Frigate in modo sicuro per utilizzare le notifiche.",
"documentation": "Leggi la documentazione",
- "title": "Notifiche non disponibili"
+ "title": "Notifiche non disponibili",
+ "descPwa": "Su iOS, le notifiche push web sono disponibili solo se Frigate è installato sulla schermata Home. Apri il menu Condividi , scegli Aggiungi alla schermata Home , quindi apri Frigate dalla nuova icona per registrare questo dispositivo per le notifiche."
},
"deviceSpecific": "Impostazioni specifiche del dispositivo",
"toast": {
@@ -704,7 +825,7 @@
},
"title": "Notifiche",
"notificationSettings": {
- "title": "Impostazioni notifiche",
+ "title": "Impostazioni di notifica",
"desc": "Frigate può inviare notifiche push in modo nativo al tuo dispositivo quando è in esecuzione nel browser o installato come PWA.",
"documentation": "Leggi la documentazione"
},
@@ -764,7 +885,7 @@
},
"birdClassification": {
"desc": "La classificazione degli uccelli identifica gli uccelli noti utilizzando un modello Tensorflow quantizzato. Quando un uccello noto viene riconosciuto, il suo nome comune viene aggiunto come sub_label. Queste informazioni sono incluse nell'interfaccia utente, nei filtri e nelle notifiche.",
- "title": "Classificazione degli uccelli"
+ "title": "Classificazione uccelli"
},
"licensePlateRecognition": {
"desc": "Frigate può riconoscere le targhe dei veicoli e aggiungere automaticamente i caratteri rilevati al campo recognized_license_plate o un nome noto come sub_label agli oggetti di tipo automobile (car). Un caso d'uso comune potrebbe essere la lettura delle targhe delle auto che entrano in un vialetto o che transitano lungo una strada.",
@@ -775,22 +896,22 @@
"restart_required": "Riavvio richiesto (impostazioni di miglioramento modificate)"
},
"triggers": {
- "documentTitle": "Inneschi",
+ "documentTitle": "Attivatori",
"management": {
- "title": "Inneschi",
- "desc": "Gestisci gli inneschi per {{camera}}. Utilizza il tipo miniatura per attivare miniature simili all'oggetto tracciato selezionato e il tipo descrizione per attivare descrizioni simili al testo specificato."
+ "title": "Attivatori",
+ "desc": "Gestisci gli attivatori per {{camera}}. Utilizza il tipo miniatura per attivare miniature simili all'oggetto tracciato selezionato e il tipo descrizione per attivare descrizioni simili al testo specificato."
},
- "addTrigger": "Aggiungi innesco",
+ "addTrigger": "Aggiungi attivatore",
"table": {
"name": "Nome",
"type": "Tipo",
"content": "Contenuto",
"threshold": "Soglia",
"actions": "Azioni",
- "noTriggers": "Nessun innesco configurato per questa telecamera.",
+ "noTriggers": "Nessun attivatore configurato per questa telecamera.",
"edit": "Modifica",
- "deleteTrigger": "Elimina innesco",
- "lastTriggered": "Ultimo innesco"
+ "deleteTrigger": "Elimina attivatore",
+ "lastTriggered": "Ultimo attivatore"
},
"type": {
"thumbnail": "Miniatura",
@@ -804,34 +925,34 @@
},
"dialog": {
"createTrigger": {
- "title": "Crea innesco",
- "desc": "Crea un innesco per la telecamera {{camera}}"
+ "title": "Crea attivatore",
+ "desc": "Crea un attivatore per la telecamera {{camera}}"
},
"editTrigger": {
- "title": "Modifica innesco",
- "desc": "Modifica le impostazioni per l'innesco della telecamera {{camera}}"
+ "title": "Modifica attivatore",
+ "desc": "Modifica le impostazioni per l'attivatore della telecamera {{camera}}"
},
"deleteTrigger": {
- "title": "Elimina innesco",
- "desc": "Vuoi davvero eliminare l'innesco {{triggerName}} ? Questa azione non può essere annullata."
+ "title": "Elimina attivatore",
+ "desc": "Vuoi davvero eliminare l'attivatore {{triggerName}} ? Questa azione non può essere annullata."
},
"form": {
"name": {
"title": "Nome",
- "placeholder": "Assegna un nome a questo innesco",
+ "placeholder": "Assegna un nome a questo attivatore",
"error": {
"minLength": "Il campo deve contenere almeno 2 caratteri.",
"invalidCharacters": "Il campo può contenere solo lettere, numeri, caratteri di sottolineatura e trattini.",
- "alreadyExists": "Per questa telecamera esiste già un innesco con questo nome."
+ "alreadyExists": "Per questa telecamera esiste già un attivatore con questo nome."
},
- "description": "Inserisci un nome o una descrizione univoca per identificare questo innesco"
+ "description": "Inserisci un nome o una descrizione univoca per identificare questo attivatore"
},
"enabled": {
- "description": "Abilita o disabilita questo innesco"
+ "description": "Abilita o disabilita questo attivatore"
},
"type": {
"title": "Tipo",
- "placeholder": "Seleziona il tipo di innesco",
+ "placeholder": "Seleziona il tipo di attivatore",
"description": "Si attiva quando viene rilevata una descrizione di un oggetto simile tracciato",
"thumbnail": "Attiva quando viene rilevata una miniatura di un oggetto simile tracciato"
},
@@ -839,7 +960,7 @@
"title": "Contenuto",
"imagePlaceholder": "Seleziona una miniatura",
"textPlaceholder": "Inserisci il contenuto del testo",
- "imageDesc": "Vengono visualizzate solo le 100 miniature più recenti. Se non riesci a trovare la miniatura desiderata, controlla gli oggetti precedenti in Esplora e imposta un innesco dal menu.",
+ "imageDesc": "Vengono visualizzate solo le 100 miniature più recenti. Se non riesci a trovare la miniatura desiderata, controlla gli oggetti precedenti in Esplora e imposta un attivatore dal menu.",
"textDesc": "Inserisci il testo per attivare questa azione quando viene rilevata una descrizione simile dell'oggetto tracciato.",
"error": {
"required": "Il contenuto è obbligatorio."
@@ -851,11 +972,11 @@
"min": "La soglia deve essere almeno 0",
"max": "La soglia deve essere al massimo 1"
},
- "desc": "Imposta la soglia di similarità per questo innesco. Una soglia più alta indica che è necessaria una corrispondenza più vicina per attivare l'innesco."
+ "desc": "Imposta la soglia di similarità per questo attivatore. Una soglia più alta indica che è necessaria una corrispondenza più vicina per attivare l'attivatore."
},
"actions": {
"title": "Azioni",
- "desc": "Per impostazione predefinita, Frigate invia un messaggio MQTT per tutti gli inneschi. Le sottoetichette aggiungono il nome dell'innesco all'etichetta dell'oggetto. Gli attributi sono metadati ricercabili, memorizzati separatamente nei metadati dell'oggetto tracciato.",
+ "desc": "Per impostazione predefinita, Frigate invia un messaggio MQTT per tutti gli attivatori. Le sottoetichette aggiungono il nome dell'attivatore all'etichetta dell'oggetto. Gli attributi sono metadati ricercabili, memorizzati separatamente nei metadati dell'oggetto tracciato.",
"error": {
"min": "È necessario selezionare almeno un'azione."
}
@@ -869,30 +990,30 @@
},
"toast": {
"success": {
- "createTrigger": "L'innesco {{name}} è stato creato correttamente.",
- "updateTrigger": "L'innesco {{name}} è stato aggiornato correttamente.",
- "deleteTrigger": "L'innesco {{name}} è stato eliminato correttamente."
+ "createTrigger": "L'attivatore {{name}} è stato creato correttamente.",
+ "updateTrigger": "L'attivatore {{name}} è stato aggiornato correttamente.",
+ "deleteTrigger": "L'attivatore {{name}} è stato eliminato correttamente."
},
"error": {
- "createTriggerFailed": "Impossibile creare l'innesco: {{errorMessage}}",
- "updateTriggerFailed": "Impossibile aggiornare l'innesco: {{errorMessage}}",
- "deleteTriggerFailed": "Impossibile eliminare l'innesco: {{errorMessage}}"
+ "createTriggerFailed": "Impossibile creare l'attivatore: {{errorMessage}}",
+ "updateTriggerFailed": "Impossibile aggiornare l'attivatore: {{errorMessage}}",
+ "deleteTriggerFailed": "Impossibile eliminare l'attivatore: {{errorMessage}}"
}
},
"semanticSearch": {
"title": "La ricerca semantica è disabilitata",
- "desc": "Per utilizzare gli inneschi, è necessario abilitare la ricerca semantica."
+ "desc": "Per utilizzare gli attivatori, è necessario abilitare la ricerca semantica."
},
"wizard": {
- "title": "Crea innesco",
+ "title": "Crea attivatore",
"step1": {
- "description": "Configura le impostazioni di base per il tuo innesco."
+ "description": "Configura le impostazioni di base per il tuo attivatore."
},
"step2": {
"description": "Imposta il contenuto che attiverà questa azione."
},
"step3": {
- "description": "Configura la soglia e le azioni per questo innesco."
+ "description": "Configura la soglia e le azioni per questo attivatore."
},
"steps": {
"nameAndType": "Nome e tipo",
@@ -903,8 +1024,8 @@
},
"roles": {
"management": {
- "title": "Gestione del ruolo di spettatore",
- "desc": "Gestisci i ruoli di spettatori personalizzati e le relative autorizzazioni di accesso alla telecamera per questa istanza Frigate."
+ "title": "Gestione del ruolo visualizzatore",
+ "desc": "Gestisci i ruoli di visualizzatori personalizzati e le relative autorizzazioni di accesso alla telecamera per questa istanza Frigate."
},
"addRole": "Aggiungi ruolo",
"table": {
@@ -920,9 +1041,9 @@
"createRole": "Ruolo {{role}} creato con successo",
"updateCameras": "Telecamere aggiornate per il ruolo {{role}}",
"deleteRole": "Ruolo {{role}} eliminato con successo",
- "userRolesUpdated_one": "{{count}} utente assegnato a questo ruolo è stato aggiornato a \"spettatore\", che ha accesso a tutte le telecamere.",
- "userRolesUpdated_many": "{{count}} utenti assegnati a questo ruolo sono stati aggiornati a \"spettatore\", che ha accesso a tutte le telecamere.",
- "userRolesUpdated_other": "{{count}} utenti assegnati a questo ruolo sono stati aggiornati a \"spettatore\", che ha accesso a tutte le telecamere."
+ "userRolesUpdated_one": "{{count}} utente assegnato a questo ruolo è stato aggiornato a \"visualizzatore\", che ha accesso a tutte le telecamere.",
+ "userRolesUpdated_many": "{{count}} utenti assegnati a questo ruolo sono stati aggiornati a \"visualizzatore\", che ha accesso a tutte le telecamere.",
+ "userRolesUpdated_other": "{{count}} utenti assegnati a questo ruolo sono stati aggiornati a \"visualizzatore\", che ha accesso a tutte le telecamere."
},
"error": {
"createRoleFailed": "Impossibile creare il ruolo: {{errorMessage}}",
@@ -942,7 +1063,7 @@
},
"deleteRole": {
"title": "Elimina ruolo",
- "desc": "Questa azione non può essere annullata. Ciò eliminerà definitivamente il ruolo e assegnerà a tutti gli utenti il ruolo di 'spettatore', che darà loro accesso a tutte le telecamere.",
+ "desc": "Questa azione non può essere annullata. Ciò eliminerà definitivamente il ruolo e assegnerà a tutti gli utenti il ruolo di 'visualizzatore', che darà loro accesso a tutte le telecamere.",
"warn": "Sei sicuro di voler eliminare {{role}} ?",
"deleting": "Eliminazione in corso..."
},
@@ -966,15 +1087,15 @@
"cameraReview": {
"title": "Impostazioni revisione telecamera",
"object_descriptions": {
- "title": "Descrizioni oggetti IA generativa",
+ "title": "Descrizioni oggetti IA Generativa",
"desc": "Abilita/disabilita temporaneamente le descrizioni degli oggetti generate dall'IA per questa telecamera fino al riavvio di Frigate. Se disabilitate, le descrizioni generate dall'IA non verranno richieste per gli oggetti tracciati su questa telecamera."
},
"review_descriptions": {
- "title": "Descrizioni revisioni IA generativa",
+ "title": "Descrizioni revisioni IA Generativa",
"desc": "Abilita/disabilita temporaneamente le descrizioni di revisione generate dall'IA per questa telecamera fino al riavvio di Frigate. Se disabilitate, le descrizioni generate dall'IA non saranno richieste per gli elementi di revisione su questa telecamera."
},
"review": {
- "title": "Rivedi",
+ "title": "Revisiona",
"desc": "Abilita/disabilita temporaneamente avvisi e rilevamenti per questa telecamera fino al riavvio di Frigate. Se disabilitato, non verranno generati nuovi elementi di revisione. ",
"alerts": "Avvisi ",
"detections": "Rilevamenti "
@@ -1058,7 +1179,7 @@
"quality": "Qualità",
"selectQuality": "Seleziona la qualità",
"roleLabels": {
- "detect": "Rilevamento di oggetti",
+ "detect": "Rilevamento oggetti",
"record": "Registrazione",
"audio": "Audio"
},
@@ -1066,8 +1187,8 @@
"testSuccess": "Prova del flusso riuscita!",
"testFailed": "Prova del flusso fallita",
"testFailedTitle": "Prova fallita",
- "connected": "Connesso",
- "notConnected": "Non connesso",
+ "connected": "Connessa",
+ "notConnected": "Non connessa",
"featuresTitle": "Caratteristiche",
"go2rtc": "Riduci le connessioni alla telecamera",
"detectRoleWarning": "Per procedere, almeno un flusso deve avere il ruolo \"rilevamento\".",
@@ -1137,7 +1258,7 @@
"brands": {
"reolink-rtsp": "Reolink RTSP non è consigliato. Abilita HTTP nelle impostazioni del firmware della telecamera e riavvia la procedura guidata."
},
- "customUrlRtspRequired": "Gli URL personalizzati devono iniziare con \"rtsp://\". Per i flussi di telecamere non RTSP è richiesta la configurazione manuale."
+ "customUrlRtspRequired": "Gli URL personalizzati devono iniziare con \"rtsp://\" o \"rtsps://\". La configurazione manuale è necessaria per i flussi video non RTSP."
},
"docs": {
"reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras"
@@ -1247,7 +1368,7 @@
"roles": "Ruoli",
"ffmpegModule": "Utilizza la modalità di compatibilità del flusso",
"ffmpegModuleDescription": "Se il flusso non si carica dopo diversi tentativi, prova ad abilitare questa opzione. Se abilitata, Frigate utilizzerà il modulo ffmpeg con go2rtc. Questo potrebbe garantire una migliore compatibilità con alcuni flussi di telecamere.",
- "none": "Nessuno",
+ "none": "Nessuna",
"error": "Errore",
"streamValidated": "Flusso {{number}} convalidato con successo",
"streamValidationFailed": "Convalida del flusso {{number}} non riuscita",
@@ -1272,7 +1393,8 @@
},
"hikvision": {
"substreamWarning": "Il sottoflusso 1 è bloccato a bassa risoluzione. Molte telecamere Hikvision supportano sottoflussi aggiuntivi che devono essere abilitati nelle impostazioni della telecamera. Si consiglia di controllare e utilizzare tali flussi, se disponibili."
- }
+ },
+ "resolutionUnknown": "Non è stato possibile rilevare la risoluzione di questo flusso. È necessario impostare manualmente la risoluzione di rilevamento nelle Impostazioni o nella configurazione."
}
}
},
@@ -1283,8 +1405,45 @@
"selectCamera": "Seleziona una telecamera",
"backToSettings": "Torna alle impostazioni della telecamera",
"streams": {
- "title": "Abilita/Disabilita telecamere",
- "desc": "Disattiva temporaneamente una telecamera fino al riavvio di Frigate. La disattivazione completa di una telecamera interrompe l'elaborazione dei flussi di questa telecamera da parte di Frigate. Rilevamento, registrazione e correzioni non saranno disponibili. Nota: questa operazione non disattiva le ritrasmissioni di go2rtc. "
+ "title": "Stato e dettagli della telecamera",
+ "desc": "Disattiva temporaneamente una telecamera fino al riavvio di Frigate. La disattivazione completa di una telecamera interrompe l'elaborazione dei flussi di questa telecamera da parte di Frigate. Rilevamento, registrazione e correzioni non saranno disponibili. Nota: questa operazione non disattiva le ritrasmissioni di go2rtc. ",
+ "enableLabel": "Telecamere abilitate",
+ "enableDesc": "Disabilita temporaneamente una telecamera abilitata fino al riavvio di Frigate. La disabilitazione completa di una telecamera interrompe l'elaborazione dei flussi video di tale telecamera da parte di Frigate. Le funzioni di rilevamento, registrazione e correzioni non saranno disponibili. Nota: questa operazione non disabilita le ritrasmissioni go2rtc. Trascina le schede per riordinare le telecamere nell'interfaccia utente. L'ordine delle telecamere abilitate verrà visualizzato in tutta l'interfaccia utente inclusa la schermata Dal vivo e i menu a tendina di selezione delle telecamere.",
+ "disableLabel": "Telecamere disabilitate",
+ "disableDesc": "Abilita una telecamera attualmente non visibile nell'interfaccia utente e disabilitata nella configurazione. Dopo l'abilitazione è necessario riavviare Frigate.",
+ "enableSuccess": "{{cameraName}} abilitata. Riavvia Frigate per applicare le modifiche.",
+ "friendlyName": {
+ "edit": "Modifica il nome visualizzato della telecamera",
+ "title": "Modifica il nome visualizzato",
+ "description": "Imposta il nome amichevole visualizzato per questa telecamera nell'interfaccia utente di Frigate. Lascia vuoto per utilizzare l'ID della telecamera.",
+ "rename": "Rinomina"
+ },
+ "reorderHandle": "Trascina per riordinare",
+ "saving": "Salvataggio…",
+ "saved": "Salvato",
+ "details": {
+ "edit": "Modifica i dettagli della telecamera",
+ "title": "Modifica i dettagli della telecamera",
+ "description": "Aggiorna il nome visualizzato, l'URL esterno e la visibilità utilizzati per questa telecamera nell'interfaccia utente di Frigate.",
+ "friendlyNameLabel": "Nome da visualizzare",
+ "friendlyNameHelp": "Nome descrittivo visualizzato per questa telecamera nell'interfaccia utente di Frigate. Lasciare vuoto per utilizzare l'ID della telecamera.",
+ "webuiUrlLabel": "URL dell'interfaccia web della telecamera",
+ "webuiUrlHelp": "URL per accedere direttamente all'interfaccia web della telecamera dalla vista Correzioni. Lasciare vuoto per disabilitare il collegamento.",
+ "webuiUrlInvalid": "Deve essere un URL valido (ad esempio, https://esempio.com).",
+ "dashboardLabel": "Mostra nella schermata Dal vivo",
+ "dashboardHelp": "Mostra questa telecamera nella schermata Dal vivo.",
+ "reviewLabel": "Mostra in Revisiona",
+ "reviewHelp": "Mostra questa telecamera in Revisiona, incluso il filtro della telecamera, la revisione del movimento e la visualizzazione della cronologia."
+ },
+ "label": "Stato della telecamera",
+ "description": "Imposta lo stato operativo per ciascuna telecamera.Accesa : i flussi vengono elaborati normalmente.Spenta : mette temporaneamente in pausa l'elaborazione. Non viene mantenuta dopo il riavvio di Frigate.Disabilitata : interrompe l'elaborazione e salva la modifica nella configurazione. È necessario riavviare Frigate per riattivare una telecamera disabilitata.Nota: la disabilitazione non influisce sulle ritrasmissioni go2rtc. Trascina la maniglia per riordinare le telecamere attive nell'interfaccia utente, inclusi il pannello di controllo Dal vivo e i menu a tendina di selezione della telecamera.",
+ "disabledSubheading": "Disabilitata nella configurazione",
+ "status": {
+ "on": "Accesa",
+ "off": "Spenta",
+ "disabled": "Disabilitata"
+ },
+ "disableSuccess": "{{cameraName}} disabilitata e salvata nella configurazione."
},
"cameraConfig": {
"add": "Aggiungi telecamera",
@@ -1314,6 +1473,769 @@
"streamUrls": "URL dei flussi",
"addUrl": "Aggiungi URL",
"addGo2rtcStream": "Aggiungi flusso go2rtc"
+ },
+ "profiles": {
+ "enabled": "Abilitato",
+ "title": "Sovrascritture della telecamera del profilo",
+ "selectLabel": "Seleziona il profilo",
+ "description": "Configura quali telecamere vengono accese o spente all'attivazione di un profilo. Le telecamere impostate su \"Eredita\" mantengono il loro stato predefinito.",
+ "inherit": "Eredita",
+ "disabled": "Disabilitato",
+ "on": "Attivato",
+ "off": "Disattivato"
+ },
+ "description": "Aggiungi, modifica ed elimina le telecamere, controlla lo stato di ciascuna telecamera e configura le impostazioni personalizzate per profilo e tipo di telecamera. Per configurare flussi video, rilevamento, movimento e altre impostazioni specifiche per ciascuna telecamera, seleziona la sezione corrispondente in Configurazione telecamera.",
+ "deleteCamera": "Elimina telecamera",
+ "deleteCameraDialog": {
+ "title": "Elimina telecamera",
+ "description": "L'eliminazione di una telecamera rimuoverà in modo permanente tutte le registrazioni, gli oggetti tracciati e la configurazione relativi a tale telecamera. Potrebbe essere comunque necessario rimuovere manualmente gli eventuali flussi go2rtc associati a questa telecamera.",
+ "selectPlaceholder": "Scegli telecamera...",
+ "confirmTitle": "Sei sicuro?",
+ "confirmWarning": "L'eliminazione di {{cameraName}} è irreversibile.",
+ "deleteExports": "Elimina anche le esportazioni per questa telecamera",
+ "confirmButton": "Elimina definitivamente",
+ "success": "Telecamera {{cameraName}} eliminata con successo",
+ "error": "Impossibile eliminare la telecamera {{cameraName}}"
+ },
+ "cameraType": {
+ "title": "Tipo di telecamera",
+ "label": "Tipo di telecamera",
+ "description": "Imposta il tipo per ogni telecamera. Le telecamere LPR dedicate sono telecamere monouso con un potente zoom ottico per acquisire le targhe dei veicoli distanti. La maggior parte delle telecamere dovrebbe utilizzare il tipo di telecamera normale, a meno che non siano specificamente progettate per il riconoscimento delle targhe e abbiano una visuale molto ravvicinata sulle targhe.",
+ "normal": "Normale",
+ "dedicatedLpr": "LPR dedicata",
+ "saveSuccess": "Tipo di telecamera aggiornato per {{cameraName}}. Riavviare Frigate per applicare le modifiche."
+ },
+ "clone": {
+ "sectionTitle": "Clona impostazioni",
+ "button": "Clona impostazioni",
+ "sectionDescription": "Copia la configurazione da una telecamera ad un'altra telecamera o a una nuova.",
+ "title": "Clona impostazioni della telecamera",
+ "description": "Copia la configurazione di una telecamera su una o altre telecamere o su una nuova telecamera. L'identità (nome, nome descrittivo, URL dell'interfaccia web, ordine di visualizzazione) non viene mai copiata.",
+ "source": {
+ "label": "Telecamera sorgente",
+ "placeholder": "Seleziona una telecamera sorgente",
+ "required": "Seleziona una telecamera sorgente"
+ },
+ "target": {
+ "legend": "Destinazione",
+ "newRadio": "Nuova telecamera",
+ "newNameLabel": "Nome telecamera",
+ "newNamePlaceholder": "p.es., porta_posteriore o Porta Posteriore",
+ "newNameRequired": "Il nome della telecamera è obbligatorio",
+ "newNameInvalid": "Nome della telecamera non valido",
+ "newNameCollision": "Esiste già una telecamera con questo nome",
+ "newStreamsForced": "I flussi video vengono sempre copiati per ogni nuova telecamera.",
+ "existingCamerasRadio": "Telecamere esistenti",
+ "allCameras": "Tutte le telecamere",
+ "existingPlaceholder": "Seleziona almeno una telecamera",
+ "existingDisabled": "Nessun'altra telecamera da copiare"
+ },
+ "categories": {
+ "legend": "Impostazioni da clonare",
+ "description": "Scegli quali impostazioni copiare dalla telecamera sorgente.",
+ "selectAll": "Seleziona tutto",
+ "selectNone": "Nessuna selezione",
+ "resetDefaults": "Ripristina le impostazioni predefinite",
+ "general": "Generale",
+ "spatial": "Impostazioni spaziali",
+ "streams": "Flussi",
+ "spatialWarningTitle": "Risoluzione non coincidente",
+ "spatialWarning": "La risoluzione di rilevamento della telecamera sorgente {{srcCamera}} ({{srcWidth}}×{{srcHeight}}) è diversa da quella di {{cameras}}. I poligoni potrebbero non allinearsi su queste telecamere. Queste impostazioni predefinite sono disattivate; abilitale per copiare così come sono.",
+ "restartHint": "Riavvio richiesto",
+ "items": {
+ "record": "Registrazione",
+ "snapshots": "Istantanee",
+ "review": "Revisiona",
+ "motion": "Rilevamento movimento",
+ "objects": "Oggetti",
+ "audio": "Rilevamento audio",
+ "audio_transcription": "Trascrizione audio",
+ "notifications": "Notifiche",
+ "birdseye": "Birdseye",
+ "mqtt": "MQTT",
+ "timestamp_style": "Stile orario",
+ "onvif": "ONVIF",
+ "lpr": "Riconoscimento targhe",
+ "face_recognition": "Riconoscimento facciale",
+ "semantic_search": "Ricerca semantica",
+ "genai": "IA Generativa",
+ "type": "Tipo di telecamera (normale / dedicata al riconoscimento targhe)",
+ "profiles": "Profili",
+ "detect": "Rileva le dimensioni",
+ "zones": "Zone",
+ "motion_mask": "Maschere di movimento",
+ "object_masks": "Maschere di oggetti",
+ "ffmpeg_live": "URL e ruoli del flusso"
+ }
+ },
+ "footer": {
+ "changeCount_one": "{{count}} modifica verrà applicata",
+ "changeCount_many": "{{count}} modifiche verranno applicate",
+ "changeCount_other": "{{count}} modifiche verranno applicate",
+ "restartNeeded": "Per alcune modifiche sarà necessario riavviare il sistema.",
+ "liveOnly": "Tutte le modifiche verranno applicate immediatamente, senza bisogno di riavviare il sistema.",
+ "submit": "Clona",
+ "submitting": "Clonazione…"
+ },
+ "toast": {
+ "success": "Impostazioni copiate in {{cameraName}}",
+ "successWithRestart": "Impostazioni copiate in {{cameraName}}. Riavvia Frigate per applicare tutte le modifiche.",
+ "successMulti_one": "Impostazioni copiate su {{count}} telecamera",
+ "successMulti_many": "Impostazioni copiate su {{count}} telecamere",
+ "successMulti_other": "Impostazioni copiate su {{count}} telecamere",
+ "successMultiWithRestart_one": "Impostazioni copiate su {{count}} telecamera. Riavvia Frigate per applicare tutte le modifiche.",
+ "successMultiWithRestart_many": "Impostazioni copiate su {{count}} telecamere. Riavvia Frigate per applicare tutte le modifiche.",
+ "successMultiWithRestart_other": "Impostazioni copiate su {{count}} telecamere. Riavvia Frigate per applicare tutte le modifiche.",
+ "partialFailure": "{{successCount}} sezioni applicate; '{{failedSection}}' non riuscita: {{errorMessage}}",
+ "partialFailureMulti": "Copiato su {{successCount}} telecamera(e); errore per {{failed}}: {{errorMessage}}",
+ "newCameraPartialFailure": "La telecamera {{cameraName}} è stata creata, ma alcune impostazioni non sono state copiate: {{errorMessage}}",
+ "sourceMissing": "La telecamera sorgente non esiste più",
+ "submitError": "Impossibile clonare la telecamera: {{errorMessage}}"
+ }
}
+ },
+ "button": {
+ "overriddenGlobal": "Sovrascritto (Globale)",
+ "overriddenGlobalTooltip": "Questa telecamera sovrascrive le impostazioni di configurazione globali in questa sezione",
+ "overriddenBaseConfig": "Sovrascritto (Configurazione di base)",
+ "overriddenBaseConfigTooltip": "Il profilo {{profile}} sovrascrive le impostazioni di configurazione in questa sezione",
+ "overriddenInCameras": {
+ "label_one": "Sovrascritto in {{count}} telecamera",
+ "label_many": "Sovrascritto in {{count}} telecamere",
+ "label_other": "Sovrascritto in {{count}} telecamere",
+ "tooltip_one": "{{count}} telecamera sovrascrive i valori in questa sezione. Fai clic per visualizzare i dettagli.",
+ "tooltip_many": "{{count}} telecamere sovrascrivono i valori in questa sezione. Fai clic per visualizzare i dettagli.",
+ "tooltip_other": "{{count}} telecamere sovrascrivono i valori in questa sezione. Fai clic per visualizzare i dettagli.",
+ "heading_one": "Questa sezione globale contiene campi che vengono sovrascritti in {{count}} telecamera.",
+ "heading_many": "Questa sezione globale contiene campi che vengono sovrascritti in {{count}} telecamere.",
+ "heading_other": "Questa sezione globale contiene campi che vengono sovrascritti in {{count}} telecamere.",
+ "othersField_one": "{{count}} altro",
+ "othersField_many": "{{count}} altri",
+ "othersField_other": "{{count}} altri",
+ "profilePrefix": "Profilo {{profile}}: {{fields}}"
+ },
+ "overriddenGlobalHeading_one": "Questa telecamera sovrascrive il campo {{count}} dalla configurazione globale:",
+ "overriddenGlobalHeading_many": "Questa telecamera sovrascrive i campi {{count}} della configurazione globale:",
+ "overriddenGlobalHeading_other": "Questa telecamera sovrascrive i campi {{count}} della configurazione globale:",
+ "overriddenGlobalNoDeltas": "Questa telecamera sovrascrive la configurazione globale, ma nessun valore dei campi risulta diverso.",
+ "overriddenBaseConfigHeading_one": "Il profilo {{profile}} sovrascrive il campo {{count}} della configurazione di base:",
+ "overriddenBaseConfigHeading_many": "Il profilo {{profile}} sovrascrive i campi {{count}} della configurazione di base:",
+ "overriddenBaseConfigHeading_other": "Il profilo {{profile}} sovrascrive i campi {{count}} della configurazione di base:",
+ "overriddenBaseConfigNoDeltas": "Il profilo {{profile}} sovrascrive questa sezione, ma nessun valore di campo differisce dalla configurazione di base."
+ },
+ "go2rtcStreams": {
+ "title": "Flussi go2rtc",
+ "ffmpeg": {
+ "video": "Video",
+ "audio": "Audio",
+ "audioCopy": "Copia",
+ "videoCopy": "Copia",
+ "hardware": "Accelerazione hardware",
+ "hardwareNone": "Nessuna accelerazione hardware",
+ "hardwareAuto": "Automatico (consigliato)",
+ "useFfmpegModule": "Utilizza la modalità di compatibilità (ffmpeg)",
+ "videoH264": "Transcodifica in H.264",
+ "videoH265": "Transcodifica in H.265",
+ "videoExclude": "Escludi",
+ "audioAac": "Transcodifica in AAC",
+ "audioOpus": "Transcodifica in Opus",
+ "audioPcmu": "Transcodifica in PCM μ-law",
+ "audioPcma": "Transcodifica in PCM A-law",
+ "audioPcm": "Transcodifica in PCM",
+ "audioMp3": "Transcodifica in MP3",
+ "audioExclude": "Escludi",
+ "hardwareVaapi": "VAAPI",
+ "hardwareCuda": "CUDA",
+ "hardwareV4l2m2m": "V4L2 M2M",
+ "hardwareDxva2": "DXVA2",
+ "hardwareVideotoolbox": "VideoToolbox",
+ "addVideoCodec": "Aggiungi codec video",
+ "addAudioCodec": "Aggiungi codec audio",
+ "removeCodec": "Rimuovi codec"
+ },
+ "description": "Gestisci le configurazioni del flusso go2rtc per la ritrasmissione delle immagini della telecamera. Ogni flusso ha un nome e uno o più URL sorgente.",
+ "addStream": "Aggiungi flusso",
+ "addStreamDesc": "Inserisci un nome per il nuovo flusso. Questo nome verrà utilizzato per identificare il flusso nella configurazione della telecamera.",
+ "addUrl": "Aggiungi URL",
+ "streamName": "Nome flusso",
+ "streamNamePlaceholder": "p. es., porta_ingresso",
+ "streamUrlPlaceholder": "p. es., rtsp://utente:password@192.168.1.100/flusso",
+ "deleteStream": "Elimina flusso",
+ "deleteStreamConfirm": "Sei sicuro di voler eliminare il flusso \"{{streamName}}\"? Le telecamere che fanno riferimento a questo flusso potrebbero smettere di funzionare.",
+ "noStreams": "Nessun flusso go2rtc configurato. Aggiungi un flusso per iniziare.",
+ "validation": {
+ "nameRequired": "Il nome del flusso è obbligatorio",
+ "nameDuplicate": "Esiste già un flusso con questo nome",
+ "nameInvalid": "Il nome del flusso può contenere solo lettere, numeri, trattini bassi e trattini",
+ "urlRequired": "È richiesto almeno un URL"
+ },
+ "renameStream": "Rinomina flusso",
+ "renameStreamDesc": "Inserisci un nuovo nome per questo flusso. Rinominare un flusso potrebbe causare problemi alle telecamere o ad altri flussi che lo referenziano tramite il suo nome.",
+ "newStreamName": "Nuovo nome del flusso",
+ "streamNumber": "Flusso {{index}}",
+ "sourceNumber": "Sorgente {{index}}"
+ },
+ "configForm": {
+ "sections": {
+ "review": "Revisiona",
+ "record": "Registrazione",
+ "snapshots": "Istantanee",
+ "ffmpeg": "FFmpeg",
+ "objects": "Oggetti",
+ "database": "Database",
+ "mqtt": "MQTT",
+ "notifications": "Notifiche",
+ "tls": "TLS",
+ "auth": "Autenticazione",
+ "proxy": "Proxy",
+ "telemetry": "Telemetria",
+ "birdseye": "Birdseye",
+ "semantic_search": "Ricerca semantica",
+ "lpr": "Riconoscimento targhe",
+ "face_recognition": "Riconoscimento facciale",
+ "masksAndZones": "Maschere / Zone",
+ "audio": "Audio",
+ "model": "Modello",
+ "detect": "Rilevamento",
+ "motion": "Movimento",
+ "live": "Vista dal vivo",
+ "timestamp_style": "Orari",
+ "go2rtc": "go2rtc",
+ "detectors": "Rivelatori",
+ "genai": "GenAI"
+ },
+ "tabs": {
+ "system": "Sistema",
+ "integrations": "Integrazioni",
+ "sharedDefaults": "Impostazioni predefinite condivise"
+ },
+ "inputRoles": {
+ "options": {
+ "audio": "Audio",
+ "detect": "Rileva",
+ "record": "Registra"
+ },
+ "summary": "{{count}} ruoli selezionati",
+ "empty": "Nessun ruolo disponibile"
+ },
+ "roleMap": {
+ "roleLabel": "Ruolo",
+ "remove": "Rimuovi",
+ "empty": "Nessuna mappatura dei ruoli",
+ "groupsLabel": "Gruppi",
+ "addMapping": "Aggiungi la mappatura dei ruoli"
+ },
+ "notifications": {
+ "title": "Impostazioni di notifica"
+ },
+ "global": {
+ "title": "Impostazioni globali",
+ "description": "Queste impostazioni si applicano a tutte le telecamere a meno che non vengano modificate nelle impostazioni specifiche della singola telecamera."
+ },
+ "camera": {
+ "noCameras": "Nessuna telecamera disponibile",
+ "title": "Impostazioni telecamera",
+ "description": "Queste impostazioni si applicano solo a questa telecamera e sovrascrivono le impostazioni globali."
+ },
+ "advancedSettingsCount": "Impostazioni avanzate ({{count}})",
+ "advancedCount": "Avanzato ({{count}})",
+ "showAdvanced": "Mostra impostazioni avanzate",
+ "additionalProperties": {
+ "keyLabel": "Chiave",
+ "valueLabel": "Valore",
+ "keyPlaceholder": "Nuova chiave",
+ "remove": "Rimuovi"
+ },
+ "ffmpegArgs": {
+ "preset": "Preimpostazione",
+ "manual": "Argomenti manuali",
+ "inherit": "Eredita dalle impostazioni della telecamera",
+ "none": "Nessuna",
+ "useGlobalSetting": "Eredita dalle impostazioni globali",
+ "selectPreset": "Seleziona preimpostazione",
+ "manualPlaceholder": "Inserisci argomenti FFmpeg",
+ "presetLabels": {
+ "preset-rpi-64-h264": "Raspberry Pi (H.264)",
+ "preset-rpi-64-h265": "Raspberry Pi (H.265)",
+ "preset-vaapi": "VAAPI (GPU Intel/AMD)",
+ "preset-intel-qsv-h264": "Intel QuickSync (H.264)",
+ "preset-intel-qsv-h265": "Intel QuickSync (H.265)",
+ "preset-nvidia": "GPU NVIDIA",
+ "preset-jetson-h264": "NVIDIA Jetson (H.264)",
+ "preset-jetson-h265": "NVIDIA Jetson (H.265)",
+ "preset-rkmpp": "Rockchip RKMPP",
+ "preset-http-jpeg-generic": "HTTP JPEG (Generico)",
+ "preset-http-mjpeg-generic": "HTTP JPEG (Generico)",
+ "preset-http-reolink": "HTTP - Telecamere Reolink",
+ "preset-rtmp-generic": "RTMP (Generico)",
+ "preset-rtsp-generic": "RTSP (Generico)",
+ "preset-rtsp-restream": "RTSP - Ritrasmissione da go2rtc",
+ "preset-rtsp-restream-low-latency": "RTSP - Ritrasmissione da go2rtc (bassa latenza)",
+ "preset-rtsp-udp": "RTSP - UDP",
+ "preset-rtsp-blue-iris": "RTSP - Blue Iris",
+ "preset-record-generic": "Registrazione (Generica, senza audio)",
+ "preset-record-generic-audio-copy": "Registrazione (generica + copia audio)",
+ "preset-record-generic-audio-aac": "Registrazione (generica + audio in AAC)",
+ "preset-record-mjpeg": "Registrazione - Telecamere MJPEG",
+ "preset-record-jpeg": "Registrazione - Telecamere JPEG",
+ "preset-record-ubiquiti": "Registrazione - Telecamere Ubiquiti"
+ }
+ },
+ "cameraInputs": {
+ "itemTitle": "Flusso {{index}}"
+ },
+ "restartRequiredField": "Riavvio richiesto",
+ "restartRequiredFooter": "Configurazione modificata - Riavvio necessario",
+ "detect": {
+ "title": "Impostazioni di rilevamento"
+ },
+ "detectors": {
+ "title": "Impostazioni del rilevatore",
+ "singleType": "È consentito un solo rilevatore di tipo {{type}}.",
+ "keyRequired": "Il nome del rilevatore è obbligatorio.",
+ "keyDuplicate": "Il nome del rilevatore esiste già.",
+ "noSchema": "Non sono disponibili schemi di rilevamento.",
+ "none": "Nessuna istanza del rilevatore configurata.",
+ "add": "Aggiungi rilevatore",
+ "addCustomKey": "Aggiungi chiave personalizzata"
+ },
+ "record": {
+ "title": "Impostazioni di registrazione"
+ },
+ "snapshots": {
+ "title": "Impostazioni istantanea"
+ },
+ "motion": {
+ "title": "Impostazioni di movimento"
+ },
+ "objects": {
+ "title": "Impostazioni oggetto"
+ },
+ "audioLabels": {
+ "summary": "{{count}} etichette audio selezionate",
+ "empty": "Nessuna etichetta audio disponibile"
+ },
+ "objectLabels": {
+ "summary": "{{count}} tipi di oggetto selezionati",
+ "empty": "Non sono disponibili etichette per gli oggetti"
+ },
+ "reviewLabels": {
+ "summary": "{{count}} etichette selezionate",
+ "empty": "Nessuna etichetta disponibile"
+ },
+ "filters": {
+ "objectFieldLabel": "{{field}} per {{label}}"
+ },
+ "zoneNames": {
+ "summary": "{{count}} selezionati",
+ "empty": "Nessuna zona disponibile"
+ },
+ "genaiRoles": {
+ "options": {
+ "embeddings": "Incorporamento",
+ "descriptions": "Descrizioni",
+ "chat": "Chat"
+ }
+ },
+ "semanticSearchModel": {
+ "placeholder": "Seleziona il modello…",
+ "builtIn": "Modelli integrati",
+ "genaiProviders": "Fornitori di GenAI"
+ },
+ "genaiModel": {
+ "placeholder": "Seleziona o inserisci un modello…",
+ "search": "Cerca o inserisci un modello…",
+ "noModels": "Nessun modello disponibile",
+ "available": "Modelli disponibili",
+ "useCustom": "Utilizza \"{{value}}\"",
+ "refresh": "Aggiorna modelli",
+ "probeFailed": "Impossibile rilevare i modelli",
+ "fetchedModels": "Elenco dei modelli recuperato con successo"
+ },
+ "review": {
+ "title": "Impostazioni di revisione"
+ },
+ "audio": {
+ "title": "Impostazioni audio"
+ },
+ "live": {
+ "title": "Impostazioni della visualizzazione dal vivo"
+ },
+ "timestamp_style": {
+ "title": "Impostazioni orario"
+ },
+ "searchPlaceholder": "Ricerca...",
+ "addCustomLabel": "Aggiungi etichetta personalizzata...",
+ "knownPlates": {
+ "namePlaceholder": "p. es., auto della moglie",
+ "platePlaceholder": "Numero di targa o espressione regolare"
+ },
+ "timezone": {
+ "defaultOption": "Utilizza il fuso orario del browser"
+ },
+ "semanticSearchModelSize": {
+ "notApplicable": "Non applicabile ai fornitori GenAI"
+ },
+ "liveStreams": {
+ "streamNameLabel": "Nome flusso",
+ "streamNamePlaceholder": "p.es., flusso HD principale",
+ "go2rtcStreamLabel": "flusso go2rtc",
+ "go2rtcStreamPlaceholder": "Seleziona un flusso go2rtc",
+ "go2rtcStreamSearch": "Cerca o inserisci il nome di un flusso…",
+ "noGo2rtcStreams": "Nessun flusso go2rtc configurato",
+ "availableStreams": "Flussi disponibili",
+ "useCustom": "Utilizza \"{{value}}\"",
+ "addStream": "Aggiungi flusso"
+ },
+ "ptzPresets": {
+ "placeholder": "Seleziona o inserisci un valore preimpostato...",
+ "search": "Cerca o inserisci un valore preimpostato...",
+ "noPresets": "Nessun valore preimpostato disponibile",
+ "available": "Preimpostazioni della telecamera",
+ "useCustom": "Utilizza \"{{value}}\""
+ },
+ "defaultRole": {
+ "admin": "Amministratore",
+ "viewer": "Visualizzatore"
+ }
+ },
+ "globalConfig": {
+ "title": "Configurazione globale",
+ "description": "Configura le impostazioni globali che si applicano a tutte le telecamere, a meno che non vengano sovrascritte.",
+ "toast": {
+ "success": "Impostazioni globali salvate correttamente",
+ "error": "Impossibile salvare le impostazioni globali",
+ "validationError": "Validazione fallita"
+ }
+ },
+ "cameraConfig": {
+ "title": "Configurazione telecamera",
+ "description": "Configura le impostazioni per le singole telecamere. Le impostazioni personalizzate sovrascrivono le impostazioni predefinite globali.",
+ "overriddenBadge": "Sovrascritto",
+ "resetToGlobal": "Ripristina impostazioni globali",
+ "toast": {
+ "success": "Impostazioni della telecamera salvate correttamente",
+ "error": "Impossibile salvare le impostazioni della telecamera"
+ }
+ },
+ "profiles": {
+ "title": "Profili",
+ "columnCamera": "Telecamera",
+ "activeProfile": "Profilo attivo",
+ "noActiveProfile": "Nessun profilo attivo",
+ "active": "Attivo",
+ "activated": "Profilo '{{profile}}' attivato",
+ "activateFailed": "Impossibile impostare il profilo",
+ "deactivated": "Profilo disattivato",
+ "noProfiles": "Nessun profilo definito.",
+ "noOverrides": "Nessuna sovrascrittura",
+ "cameraCount_one": "{{count}} telecamera",
+ "cameraCount_many": "{{count}} telecamere",
+ "cameraCount_other": "{{count}} telecamere",
+ "columnOverrides": "Sovrascritture del profilo",
+ "baseConfig": "Configurazione di base",
+ "addProfile": "Aggiungi profilo",
+ "newProfile": "Nuovo profilo",
+ "profileNamePlaceholder": "p. es., Inserita, Assente, Modalità notturna",
+ "friendlyNameLabel": "Nome profilo",
+ "profileIdLabel": "ID profilo",
+ "profileIdDescription": "Identificativo interno utilizzato nella configurazione e nelle automazioni",
+ "nameInvalid": "Sono consentite solo lettere minuscole, numeri e trattini bassi",
+ "nameDuplicate": "Esiste già un profilo con questo nome",
+ "error": {
+ "mustBeAtLeastTwoCharacters": "Deve contenere almeno 2 caratteri",
+ "mustNotContainPeriod": "Non deve contenere punti",
+ "alreadyExists": "Esiste già un profilo con questo ID"
+ },
+ "renameProfile": "Rinomina profilo",
+ "renameSuccess": "Profilo rinominato in '{{profile}}'",
+ "deleteProfile": "Elimina profilo",
+ "deleteProfileConfirm": "Eliminare il profilo \"{{profile}}\" da tutte le telecamere? Questa operazione non può essere annullata.",
+ "deleteSuccess": "Profilo '{{profile}}' eliminato",
+ "createSuccess": "Profilo '{{profile}}' creato",
+ "removeOverride": "Rimuovi la sovrascrittura del profilo",
+ "deleteSection": "Elimina le sostituzioni della sezione",
+ "deleteSectionConfirm": "Rimuovere le sovrascritture {{section}} per il profilo {{profile}} su {{camera}}?",
+ "deleteSectionSuccess": "Rimosse le sovrascritture di {{section}} per {{profile}}",
+ "enableSwitch": "Abilita profili",
+ "enabledDescription": "I profili sono abilitati. Crea un nuovo profilo qui sotto, vai alla sezione di configurazione della telecamera per apportare le modifiche e salva affinché le modifiche abbiano effetto.",
+ "disabledDescription": "I profili consentono di definire insiemi denominati di impostazioni di configurazione della telecamera (p.es., inserita, assente, notturna) che possono essere attivate su richiesta."
+ },
+ "timestampPosition": {
+ "tl": "In alto a sinistra",
+ "tr": "In alto a destra",
+ "bl": "In basso a sinistra",
+ "br": "In basso a destra"
+ },
+ "detectionModel": {
+ "plusActive": {
+ "title": "Gestione del modello Frigate+",
+ "label": "Fonte del modello attuale",
+ "description": "Questa istanza utilizza un modello Frigate+. Seleziona o modifica il tuo modello nelle impostazioni di Frigate+.",
+ "goToFrigatePlus": "Vai alle impostazioni di Frigate+",
+ "showModelForm": "Configura manualmente un modello"
+ }
+ },
+ "maintenance": {
+ "title": "Manutenzione",
+ "sync": {
+ "title": "Sincronizzazione multimediale",
+ "desc": "Frigate pulirà periodicamente i supporti di memorizzazione secondo una pianificazione regolare, in base alla configurazione di conservazione. È normale che Frigate visualizzi alcuni file orfani durante il suo funzionamento. Utilizza questa funzione per rimuovere dal disco i file multimediali orfani che non sono più referenziati nel database.",
+ "started": "Sincronizzazione multimediale avviata.",
+ "alreadyRunning": "È già in corso un'operazione di sincronizzazione",
+ "error": "Impossibile avviare la sincronizzazione",
+ "currentStatus": "Stato",
+ "statusLabel": "Stato",
+ "jobId": "ID lavoro",
+ "startTime": "Ora di inizio",
+ "endTime": "Ora di fine",
+ "results": "Risultati",
+ "errorLabel": "Errore",
+ "resultsFields": {
+ "error": "Errore",
+ "totals": "Totali",
+ "filesChecked": "File controllati",
+ "orphansFound": "Orfani trovati",
+ "orphansDeleted": "Orfani eliminati",
+ "aborted": "Interrotto. La cancellazione supererebbe la soglia di sicurezza."
+ },
+ "event_snapshots": "Istantanee degli oggetti tracciati",
+ "event_thumbnails": "Miniature degli oggetti tracciati",
+ "review_thumbnails": "Anteprima delle miniature",
+ "previews": "Anteprime",
+ "exports": "Esportazioni",
+ "recordings": "Registrazioni",
+ "mediaTypes": "Tipi di supporto",
+ "allMedia": "Tutti i supporti",
+ "dryRun": "Prova a secco",
+ "dryRunEnabled": "Nessun file verrà eliminato",
+ "dryRunDisabled": "I file verranno eliminati",
+ "force": "Forza",
+ "forceDesc": "Ignora la soglia di sicurezza e completa la sincronizzazione anche se più del 50% dei file verrebbe eliminato.",
+ "verbose": "Dettagliato",
+ "verboseDesc": "Scrivi un elenco completo dei file orfani su disco per la revisione.",
+ "running": "Sincronizzazione in corso...",
+ "start": "Avvia sincronizzazione",
+ "inProgress": "Sincronizzazione in corso. Questa pagina è disabilitata.",
+ "status": {
+ "queued": "In coda",
+ "running": "In corso",
+ "completed": "Completata",
+ "failed": "Fallita",
+ "notRunning": "Non in esecuzione"
+ }
+ },
+ "regionGrid": {
+ "title": "Griglia di regioni",
+ "desc": "La griglia di regioni è un algoritmo di ottimizzazione che apprende dove gli oggetti di diverse dimensioni appaiono tipicamente nel campo visivo di ciascuna telecamera. Frigate utilizza questi dati per dimensionare in modo efficiente le regioni di rilevamento. La griglia viene creata automaticamente nel tempo a partire dai dati degli oggetti tracciati.",
+ "clear": "Pulisci griglia di regioni",
+ "clearConfirmTitle": "Pulisci griglia di regioni",
+ "clearConfirmDesc": "La pulizia della griglia delle regioni non è consigliata, a meno che non sia stata modificata di recente la dimensione del modello del rilevatore, oppure sia stata cambiata la posizione fisica della telecamera e si riscontrino problemi di tracciamento degli oggetti. La griglia verrà ricostruita automaticamente nel tempo man mano che gli oggetti vengono tracciati. È richiesto un riavvio di Frigate affinché le modifiche abbiano effetto.",
+ "clearSuccess": "Griglia delle regioni pulita con successo",
+ "clearError": "Impossibile pulire la griglia delle regioni",
+ "restartRequired": "È necessario riavviare il sistema affinché le modifiche alla griglia delle regioni abbiano effetto"
+ }
+ },
+ "retainMode": {
+ "motion": "Movimento",
+ "all": "Tutti",
+ "active_objects": "Oggetti attivi"
+ },
+ "birdseye": {
+ "trackingMode": {
+ "motion": "Movimento",
+ "objects": "Oggetti",
+ "continuous": "Continuo"
+ },
+ "cameraOrder": {
+ "reorderHandle": "Trascina per riordinare",
+ "saving": "Salvataggio…",
+ "saved": "Salvato",
+ "label": "Ordine delle telecamere",
+ "description": "Trascina le telecamere per impostarne l'ordine nella visualizzazione Birdseye."
+ }
+ },
+ "toast": {
+ "success": "Impostazioni salvate correttamente",
+ "applied": "Impostazioni applicate correttamente",
+ "successRestartRequired": "Impostazioni salvate correttamente. Riavvia Frigate per applicare le modifiche.",
+ "error": "Impossibile salvare le impostazioni",
+ "validationError": "Validazione non riuscita: {{message}}",
+ "resetSuccess": "Ripristina le impostazioni predefinite globali",
+ "resetError": "Impossibile ripristinare le impostazioni",
+ "saveAllSuccess_one": "Salvata {{count}} sezione correttamente.",
+ "saveAllSuccess_many": "Tutte le {{count}} sezioni sono state salvate correttamente.",
+ "saveAllSuccess_other": "Tutte le {{count}} sezioni sono state salvate correttamente.",
+ "saveAllPartial_one": "{{successCount}} sezione su {{totalCount}} salvata. {{failCount}} errore.",
+ "saveAllPartial_many": "{{successCount}} sezioni su {{totalCount}} salvate. {{failCount}} errori.",
+ "saveAllPartial_other": "{{successCount}} sezioni su {{totalCount}} salvate. {{failCount}} errori.",
+ "saveAllFailure": "Impossibile salvare tutte le sezioni.",
+ "saveAllSuccessRestartRequired_one": "Salvata {{count}} sezione correttamente. Riavvia Frigate per applicare le modifiche.",
+ "saveAllSuccessRestartRequired_many": "Salvate {{count}} sezioni correttamente. Riavvia Frigate per applicare le modifiche.",
+ "saveAllSuccessRestartRequired_other": "Salvate {{count}} sezioni correttamente. Riavvia Frigate per applicare le modifiche."
+ },
+ "unsavedChanges": "Hai delle modifiche non salvate",
+ "confirmReset": "Conferma il ripristino",
+ "resetToDefaultDescription": "Questa operazione ripristinerà tutte le impostazioni di questa sezione ai valori predefiniti. Tale azione è irreversibile.",
+ "resetToGlobalDescription": "Questa operazione ripristinerà le impostazioni di questa sezione ai valori predefiniti globali. Tale azione è irreversibile.",
+ "previewQuality": {
+ "very_high": "Molto alta",
+ "high": "Alta",
+ "medium": "Media",
+ "low": "Bassa",
+ "very_low": "Molto bassa"
+ },
+ "ui": {
+ "TimeOrDateStyle": {
+ "medium": "Medio",
+ "full": "Completo",
+ "long": "Lungo",
+ "short": "Corto"
+ },
+ "timeFormat": {
+ "browser": "Navigatore",
+ "12hour": "12 ore",
+ "24hour": "24 ore"
+ },
+ "unitSystem": {
+ "metric": "Metrico",
+ "imperial": "Imperiale"
+ }
+ },
+ "review": {
+ "imageSource": {
+ "recordings": "Registrazioni",
+ "previews": "Anteprime"
+ }
+ },
+ "logger": {
+ "logLevel": {
+ "debug": "Correzioni",
+ "info": "Informazioni",
+ "warning": "Avviso",
+ "error": "Errore",
+ "critical": "Critico"
+ }
+ },
+ "onvif": {
+ "profileAuto": "Automatico",
+ "profileLoading": "Caricamento profili...",
+ "autotracking": {
+ "zooming": {
+ "disabled": "Disabilitato",
+ "absolute": "Assoluto",
+ "relative": "Relativo"
+ }
+ }
+ },
+ "modelSize": {
+ "small": "Piccolo",
+ "large": "Grande"
+ },
+ "configMessages": {
+ "review": {
+ "recordDisabled": "La registrazione è disabilitata, pertanto non verranno generati elementi di revisione.",
+ "detectDisabled": "Il rilevamento degli oggetti è disabilitato. Gli elementi di revisione richiedono la presenza di oggetti rilevati per poter classificare avvisi e rilevamenti.",
+ "allNonAlertDetections": "Tutte le attività non di avviso saranno incluse tra i rilevamenti.",
+ "genaiImageSourceRecordingsRecordDisabled": "La sorgente dell'immagine è impostata su 'registrazioni', ma la registrazione è disabilitata. Frigate utilizzerà le immagini di anteprima."
+ },
+ "audio": {
+ "noAudioRole": "Nessun flusso ha il ruolo audio definito. È necessario abilitare il ruolo audio affinché il rilevamento audio funzioni."
+ },
+ "audioTranscription": {
+ "audioDetectionDisabled": "Il rilevamento audio non è abilitato per questa telecamera. La trascrizione audio richiede che il rilevamento audio sia attivo."
+ },
+ "detect": {
+ "fpsGreaterThanFive": "Impostare il valore di FPS rilevato su un valore superiore a 5 non è consigliabile. Valori più elevati potrebbero causare problemi di prestazioni e non apporteranno alcun vantaggio.",
+ "disabled": "Il rilevamento degli oggetti è disabilitato. Le istantanee, gli elementi di revisione e le funzionalità aggiuntive come il riconoscimento facciale, il riconoscimento delle targhe e l'intelligenza artificiale generativa non funzioneranno.",
+ "resolutionShouldBeMultipleOfFour": "Per ottenere risultati ottimali, la larghezza e l'altezza di rilevamento dovrebbero essere multipli di 4. Altri valori pari potrebbero produrre artefatti visivi o una leggera distorsione nel flusso di rilevamento.",
+ "aspectRatioMismatch": "La larghezza e l'altezza inserite non corrispondono al rapporto d'aspetto della risoluzione di rilevamento corrente. Ciò potrebbe produrre un'immagine allungata o distorta.",
+ "maxFramesSet": "L'impostazione del numero massimo di fotogrammi sovrascrive il comportamento predefinito e disabilita il tracciamento degli oggetti statici. Sono rare le situazioni in cui ciò è necessario, quindi si consiglia cautela nell'utilizzarlo.",
+ "squareResolution": "Una risoluzione di rilevamento quadrata è insolita. La larghezza e l'altezza di rilevamento devono corrispondere al rapporto d'aspetto della telecamera (ad esempio, 16:9), non alle dimensioni del modello di rilevamento degli oggetti. Un rapporto d'aspetto non corrispondente può distorcere l'immagine e ridurre la precisione del rilevamento.",
+ "resolutionHigh": "Questa risoluzione di rilevamento è superiore a quella consigliata e potrebbe causare un maggiore utilizzo delle risorse senza migliorare la precisione del rilevamento. Per la maggior parte delle telecamere si consiglia una risoluzione di rilevamento pari o inferiore a 1080p.",
+ "globalResolutionMultipleCameras": "Quando si configurano più telecamere, viene impostata una risoluzione di rilevamento globale. A meno che tutte le telecamere non condividano la stessa risoluzione e lo stesso rapporto d'aspetto, la larghezza e l'altezza di rilevamento devono essere definite per ciascuna telecamera in modo da corrispondere al rapporto d'aspetto nativo di ciascuna."
+ },
+ "objects": {
+ "genaiNoDescriptionsProvider": "Per generare le descrizioni è necessario configurare un provider GenAI con il ruolo 'descrizioni'."
+ },
+ "faceRecognition": {
+ "globalDisabled": "Perché le funzionalità di riconoscimento facciale funzionino correttamente su questa telecamera, è necessario abilitare l'arricchimento del riconoscimento facciale.",
+ "personNotTracked": "Il riconoscimento facciale richiede che l'oggetto 'persona' venga tracciato. Abilita 'persona' nella sezione ogggetti di questa telecamera.",
+ "modelSizeLarge": "Il modello 'grande' richiede una GPU o una NPU per prestazioni accettabili. Utilizzare il modello 'piccolo' su sistemi dotati solo di CPU."
+ },
+ "lpr": {
+ "globalDisabled": "Per il corretto funzionamento delle funzioni LPR (riconoscimento targhe) su questa telecamera, è necessario abilitare la funzione di arricchimento del riconoscimento delle targhe.",
+ "vehicleNotTracked": "Il riconoscimento della targa richiede che venga tracciato 'automobile' o 'moto'. Abilita 'automobile' o 'moto' nella sezione oggetti per questa telecamera.",
+ "modelSizeLarge": "Il modello 'grande' è ottimizzato per le targhe multilinea. Il modello 'piccolo' offre prestazioni migliori rispetto al modello 'grande' e dovrebbe essere utilizzato a meno che nella vostra regione non siano in vigore formati di targa multilinea."
+ },
+ "record": {
+ "noRecordRole": "Nessun flusso ha il ruolo di registrazione definito. La registrazione non funzionerà."
+ },
+ "birdseye": {
+ "objectsModeDetectDisabled": "Birdseye è impostato sulla modalità 'oggetti', ma il rilevamento degli oggetti è disabilitato per questa telecamera. La telecamera non verrà visualizzata in Birdseye."
+ },
+ "snapshots": {
+ "detectDisabled": "Il rilevamento degli oggetti è disabilitato. Le istantanee vengono generate dagli oggetti tracciati e non verranno create."
+ },
+ "detectors": {
+ "mixedTypes": "Tutti i rilevatori devono essere dello stesso tipo. Rimuovi i rilevatori esistenti per poter utilizzare un tipo diverso.",
+ "mixedTypesSuggestion": "Tutti i rilevatori devono essere dello stesso tipo. Rimuovi i rilevatori esistenti oppure seleziona {{type}}."
+ },
+ "semanticSearch": {
+ "jinav2SmallModelSize": "Il modello 'piccolo' Jina V2 presenta elevati consumi di RAM e di inferenza. Si consiglia il modello 'grande' con GPU dedicata."
+ },
+ "onvif": {
+ "autotrackingNoZones": "Il tracciamento automatico richiede almeno una zona. Definisci una zona per questa telecamera in Maschere/Zone, quindi impostala come zona obbligatoria qui sotto."
+ }
+ },
+ "saveAllPreview": {
+ "title": "Modifiche da salvare",
+ "triggerLabel": "Revisione delle modifiche in sospeso",
+ "empty": "Nessuna modifica in sospeso.",
+ "scope": {
+ "label": "Ambito",
+ "global": "Globale",
+ "camera": "Telecamera: {{cameraName}}"
+ },
+ "profile": {
+ "label": "Profilo"
+ },
+ "field": {
+ "label": "Campo"
+ },
+ "value": {
+ "label": "Nuovo valore",
+ "reset": "Reimposta"
+ }
+ },
+ "menuDot": {
+ "overrideGlobal": "Questa sezione sovrascrive la configurazione globale",
+ "overrideProfile": "Questa sezione viene sovrascritta dal profilo {{profile}}",
+ "unsaved": "Questa sezione contiene modifiche non salvate"
+ },
+ "detectorsAndModel": {
+ "title": "Rilevatori e modelli",
+ "description": "Configura il backend del rilevatore che esegue il rilevamento degli oggetti e il modello che utilizza. Le modifiche vengono salvate insieme in modo che il rilevatore e il modello rimangano sincronizzati.",
+ "cardTitles": {
+ "detector": "Dispositivo di rilevamento",
+ "model": "Modello di rilevamento"
+ },
+ "tabs": {
+ "plus": "Frigate+",
+ "custom": "Modello personalizzato"
+ },
+ "mismatch": {
+ "warning": "Il modello Frigate+ attuale \"{{model}}\" richiede il rilevatore {{required}}. Seleziona un modello compatibile qui sotto oppure passa a Modello personalizzato prima di salvare."
+ },
+ "plusModel": {
+ "requiresDetector": "Richiede: {{detector}}",
+ "noModelSelected": "Seleziona un modello Frigate+"
+ },
+ "toast": {
+ "saveSuccess": "Rilevatori e impostazioni del modello salvati. Riavviare Frigate per applicare le modifiche.",
+ "saveError": "Impossibile salvare le impostazioni del rilevatore e del modello"
+ },
+ "unsavedChanges": "Modifiche al rilevatore e al modello non salvate",
+ "restartRequired": "Riavvio richiesto (rilevatore o modello modificato)"
}
}
diff --git a/web/public/locales/it/views/system.json b/web/public/locales/it/views/system.json
index 6883fc3976..ed780a51e9 100644
--- a/web/public/locales/it/views/system.json
+++ b/web/public/locales/it/views/system.json
@@ -7,7 +7,8 @@
"logs": {
"frigate": "Registri Frigate - Frigate",
"go2rtc": "Registri Go2RTC - Frigate",
- "nginx": "Registri Nginx - Frigate"
+ "nginx": "Registri Nginx - Frigate",
+ "websocket": "Registri dei messaggi - Frigate"
}
},
"logs": {
@@ -31,6 +32,33 @@
"label": "Copia negli appunti",
"success": "Registri copiati negli appunti",
"error": "Impossibile copiare i registri negli appunti"
+ },
+ "websocket": {
+ "label": "Messaggi",
+ "pause": "Pausa",
+ "resume": "Riprendi",
+ "clear": "Pulisci",
+ "filter": {
+ "all": "Tutti gli argomenti",
+ "topics": "Argomenti",
+ "events": "Eventi",
+ "reviews": "Rivisti",
+ "classification": "Classificazione",
+ "face_recognition": "Riconoscimento facciale",
+ "lpr": "Risconoscimento targhe (LPR)",
+ "camera_activity": "Attività della telecamera",
+ "system": "Sistema",
+ "camera": "Telecamera",
+ "all_cameras": "Tutte le telecamere",
+ "cameras_count_one": "{{count}} telecamera",
+ "cameras_count_other": "{{count}} telecamere"
+ },
+ "empty": "Nessun messaggio ancora catturato",
+ "count_one": "{{count}} messaggio",
+ "count_other": "{{count}} messaggi",
+ "expanded": {
+ "payload": "Carico"
+ }
}
},
"general": {
@@ -72,7 +100,8 @@
"description": "Si tratta di un problema noto negli strumenti di reportistica delle statistiche GPU di Intel (intel_gpu_top), che si interrompe e restituisce ripetutamente un utilizzo della GPU pari a 0% anche nei casi in cui l'accelerazione hardware e il rilevamento degli oggetti funzionano correttamente sulla (i)GPU. Non si tratta di un problema di Frigate. È possibile riavviare il sistema per risolvere temporaneamente il problema e verificare che la GPU funzioni correttamente. Ciò non influisce sulle prestazioni."
},
"gpuTemperature": "Temperatura GPU",
- "npuTemperature": "Temperatura NPU"
+ "npuTemperature": "Temperatura NPU",
+ "gpuCompute": "Calcolo / Codifica GPU"
},
"detector": {
"inferenceSpeed": "Velocità inferenza rilevatore",
@@ -146,7 +175,7 @@
"framesAndDetections": "Fotogrammi / Rilevamenti",
"label": {
"camera": "telecamera",
- "detect": "rilevamento",
+ "detect": "rileva",
"skipped": "saltati",
"ffmpeg": "FFmpeg",
"capture": "cattura",
@@ -158,7 +187,8 @@
"cameraFramesPerSecond": "{{camName}} fotogrammi al secondo",
"cameraDetectionsPerSecond": "{{camName}} rilevamenti al secondo",
"cameraSkippedDetectionsPerSecond": "{{camName}} rilevamenti saltati al secondo",
- "cameraFfmpeg": "{{camName}} FFmpeg"
+ "cameraFfmpeg": "{{camName}} FFmpeg",
+ "cameraGpu": "GPU {{camName}}"
},
"toast": {
"success": {
@@ -178,6 +208,9 @@
"expectedFps": "FPS previsti",
"reconnectsLastHour": "Riconnessioni (ultima ora)",
"stallsLastHour": "Blocchi (ultima ora)"
+ },
+ "noCameras": {
+ "title": "Nessuna telecamera trovata"
}
},
"stats": {
@@ -188,7 +221,8 @@
"cameraIsOffline": "{{camera}} è disconnessa",
"detectIsSlow": "{{detect}} è lento ({{speed}} ms)",
"detectIsVerySlow": "{{detect}} è molto lento ({{speed}} ms)",
- "shmTooLow": "L'allocazione /dev/shm ({{total}} MB) dovrebbe essere aumentata almeno a {{min}} MB."
+ "shmTooLow": "L'allocazione /dev/shm ({{total}} MB) dovrebbe essere aumentata almeno a {{min}} MB.",
+ "debugReplayActive": "La sessione di riproduzione delle correzioni è attiva"
},
"title": "Sistema",
"metrics": "Metriche di sistema",
@@ -215,7 +249,11 @@
"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"
+ "readTheDocumentation": "Leggi la documentazione",
+ "frameLifetime": {
+ "title": "Durata del fotogramma",
+ "description": "Ogni telecamera dispone di {{frames}} posti per i fotogrammi nella memoria condivisa. Alla frequenza di fotogrammi più elevata della telecamera, ogni fotogramma è disponibile per circa {{lifetime}} secondi prima di essere sovrascritto."
+ }
}
},
"lastRefreshed": "Ultimo aggiornamento: "
diff --git a/web/public/locales/ja/audio.json b/web/public/locales/ja/audio.json
index 43811ed762..e60078e92b 100644
--- a/web/public/locales/ja/audio.json
+++ b/web/public/locales/ja/audio.json
@@ -222,7 +222,7 @@
"dubstep": "ダブステップ",
"drum_and_bass": "ドラムンベース",
"electronica": "エレクトロニカ",
- "electronic_dance_music": "EDM",
+ "electronic_dance_music": "エレクトロニック・ダンス・ミュージック",
"ambient_music": "アンビエント",
"trance_music": "トランス",
"music_of_latin_america": "ラテン音楽",
diff --git a/web/public/locales/ja/common.json b/web/public/locales/ja/common.json
index 3f04d464f2..4b538f7faa 100644
--- a/web/public/locales/ja/common.json
+++ b/web/public/locales/ja/common.json
@@ -133,14 +133,26 @@
"unsuspended": "再開",
"play": "再生",
"unselect": "選択解除",
- "export": "書き出し",
+ "export": "エクスポート",
"deleteNow": "今すぐ削除",
"next": "次へ",
- "continue": "続行"
+ "continue": "続行",
+ "add": "追加",
+ "applying": "適用中…",
+ "undo": "元に戻す",
+ "copiedToClipboard": "クリップボードにコピーしました",
+ "modified": "変更あり",
+ "overridden": "上書き済み",
+ "resetToGlobal": "グローバル設定にリセット",
+ "resetToDefault": "デフォルトにリセット",
+ "saveAll": "すべて保存",
+ "savingAll": "すべて保存中…",
+ "undoAll": "すべて元に戻す",
+ "retry": "再試行"
},
"menu": {
"system": "システム",
- "systemMetrics": "システムモニター",
+ "systemMetrics": "システムメトリクス",
"configuration": "設定",
"systemLogs": "システムログ",
"settings": "設定",
@@ -181,7 +193,7 @@
},
"review": "レビュー",
"explore": "ブラウズ",
- "export": "書き出し",
+ "export": "エクスポート",
"uiPlayground": "UI テスト環境",
"faceLibrary": "顔データベース",
"user": {
@@ -235,9 +247,15 @@
"withSystem": {
"label": "システム設定に従う"
},
- "hr": "Hrvatski (クロアチア語)"
+ "hr": "Hrvatski (クロアチア語)",
+ "bs": "Bosanski (ボスニア語)",
+ "zhHant": "繁體中文 (繁体字中国語)"
},
- "classification": "分類"
+ "classification": "分類",
+ "profiles": "プロファイル",
+ "actions": "操作",
+ "features": "機能",
+ "chat": "チャット"
},
"toast": {
"copyUrlToClipboard": "URLをクリップボードにコピーしました。",
@@ -246,7 +264,8 @@
"error": {
"title": "設定変更の保存に失敗しました: {{errorMessage}}",
"noMessage": "設定変更の保存に失敗しました"
- }
+ },
+ "success": "設定変更を保存しました。"
}
},
"role": {
@@ -289,5 +308,10 @@
"field": {
"optional": "任意",
"internalID": "Frigate が設定で使用する内部 ID です"
+ },
+ "no_items": "項目がありません",
+ "validation_errors": "入力エラー",
+ "credentialField": {
+ "savedPlaceholder": "保存済み — 変更しない場合は空欄"
}
}
diff --git a/web/public/locales/ja/components/auth.json b/web/public/locales/ja/components/auth.json
index d767e3282c..e89c35a88f 100644
--- a/web/public/locales/ja/components/auth.json
+++ b/web/public/locales/ja/components/auth.json
@@ -4,8 +4,8 @@
"password": "パスワード",
"login": "ログイン",
"errors": {
- "usernameRequired": "ユーザー名が必要です",
- "passwordRequired": "パスワードが必要です",
+ "usernameRequired": "ユーザー名は必須です",
+ "passwordRequired": "パスワードは必須です",
"rateLimit": "リクエスト制限を超えました。後でもう一度お試しください。",
"loginFailed": "ログインに失敗しました",
"unknownError": "不明なエラー。ログを確認してください。",
diff --git a/web/public/locales/ja/components/camera.json b/web/public/locales/ja/components/camera.json
index 4491d0a917..17ad52f710 100644
--- a/web/public/locales/ja/components/camera.json
+++ b/web/public/locales/ja/components/camera.json
@@ -81,6 +81,7 @@
"zones": "ゾーン",
"mask": "マスク",
"motion": "モーション",
- "regions": "領域"
+ "regions": "領域",
+ "paths": "軌跡"
}
}
diff --git a/web/public/locales/ja/components/dialog.json b/web/public/locales/ja/components/dialog.json
index c7f2b0944d..cfe49cab15 100644
--- a/web/public/locales/ja/components/dialog.json
+++ b/web/public/locales/ja/components/dialog.json
@@ -13,7 +13,7 @@
"plus": {
"submitToPlus": {
"label": "Frigate+ に送信",
- "desc": "回避したい場所でのオブジェクトは誤検出ではありません。誤検出として送信するとモデルが混乱します。"
+ "desc": "回避したい場所でのオブジェクトは誤検知ではありません。誤検知として送信するとモデルが混乱します。"
},
"review": {
"question": {
@@ -46,23 +46,72 @@
}
},
"name": {
- "placeholder": "書き出しに名前を付ける"
+ "placeholder": "エクスポートに名前を付ける"
},
"select": "選択",
- "export": "書き出し",
- "selectOrExport": "選択または書き出し",
+ "export": "エクスポート",
+ "selectOrExport": "選択またはエクスポート",
"toast": {
- "success": "書き出しを開始しました。出力ページでファイルを確認できます。",
+ "success": "エクスポートを開始しました。エクスポートページでファイルを確認できます。",
"error": {
- "failed": "書き出しの開始に失敗しました: {{error}}",
+ "failed": "エクスポートキューの開始に失敗しました: {{error}}",
"endTimeMustAfterStartTime": "終了時間は開始時間より後である必要があります",
"noVaildTimeSelected": "有効な時間範囲が選択されていません"
},
- "view": "表示"
+ "view": "表示",
+ "queued": "エクスポートがキューに追加されました。進捗状況はエクスポートページで確認できます。",
+ "batchQueuedSuccess_other": "{{count}} 件のエクスポートがキューに登録されました。現在ケースをオープンしています。",
+ "batchQueuedPartial": "{{total}} 件中 {{successful}} 件のエクスポートがキューに追加されました。失敗したカメラ: {{failedCameras}}",
+ "batchQueueFailed": "{{total}} 件のエクスポートをキューに追加できませんでした。失敗したカメラ: {{failedCameras}}",
+ "batchSuccess_other": "{{count}} 件のエクスポートを開始しました。ケースを開きます。",
+ "batchPartial": "{{total}} 件中 {{successful}} 件のエクスポートを開始しました。失敗したカメラ: {{failedCameras}}",
+ "batchFailed": "{{total}} 件のエクスポートを開始できませんでした。失敗したカメラ: {{failedCameras}}"
},
"fromTimeline": {
- "saveExport": "書き出しを保存",
- "previewExport": "書き出しをプレビュー"
+ "saveExport": "エクスポートを保存",
+ "previewExport": "エクスポートをプレビュー",
+ "queueingExport": "エクスポートをキューイングしています...",
+ "useThisRange": "この範囲を使用"
+ },
+ "queueing": "エクスポートをキューイングしています...",
+ "multiCamera": {
+ "queueingButton": "エクスポートをキューイングしています...",
+ "timeRange": "期間",
+ "selectFromTimeline": "タイムラインから選択",
+ "cameraSelection": "カメラ",
+ "cameraSelectionHelp": "この期間に追跡対象が含まれるカメラは、あらかじめ選択されています",
+ "checkingActivity": "カメラの動作を確認中...",
+ "noCameras": "利用可能なカメラがありません",
+ "detectionCount_other": "{{count}} 追跡対象",
+ "nameLabel": "エクスポート名",
+ "namePlaceholder": "これらのエクスポート用オプションのベース名",
+ "exportButton_other": "{{count}} 台のカメラをエクスポート"
+ },
+ "case": {
+ "newCaseOption": "新しいケースを作成",
+ "newCaseNamePlaceholder": "新しいケース名",
+ "newCaseDescriptionPlaceholder": "ケースの説明",
+ "label": "ケース",
+ "nonAdminHelp": "これらのエクスポートに対して新しいケースが作成されます。",
+ "placeholder": "ケースを選択"
+ },
+ "tabs": {
+ "export": "シングルカメラ",
+ "multiCamera": "マルチカメラ"
+ },
+ "multi": {
+ "title_other": "{{count}} 件のレビューをエクスポート",
+ "description": "選択した各レビューをエクスポートします。すべてのエクスポートは 1 つのケースにまとめられます。",
+ "descriptionNoCase": "選択した各レビューをエクスポートします。",
+ "caseNamePlaceholder": "レビューエクスポート - {{date}}",
+ "exportButton_other": "{{count}} 件のレビューをエクスポート",
+ "exportingButton": "エクスポート中...",
+ "toast": {
+ "started_other": "{{count}} 件のエクスポートを開始しました。ケースを開きます。",
+ "startedNoCase_other": "{{count}} 件のエクスポートを開始しました。",
+ "partial": "{{total}} 件中 {{successful}} 件のエクスポートを開始しました。失敗: {{failedItems}}",
+ "failed": "{{total}} 件のエクスポートを開始できませんでした。失敗: {{failedItems}}"
+ }
}
},
"streaming": {
@@ -105,10 +154,18 @@
}
},
"button": {
- "export": "書き出し",
+ "export": "エクスポート",
"markAsReviewed": "レビュー済みにする",
"deleteNow": "今すぐ削除",
"markAsUnreviewed": "未レビューに戻す"
+ },
+ "shareTimestamp": {
+ "label": "タイムスタンプを共有",
+ "title": "タイムスタンプを共有",
+ "description": "現在のプレーヤー位置をタイムスタンプ付き URL で共有するか、任意のタイムスタンプを指定できます。これは公開リンクではなく、Frigate とこのカメラへのアクセス権を持つユーザーのみアクセスできます。",
+ "custom": "カスタムタイムスタンプ",
+ "button": "タイムスタンプ URL を共有",
+ "shareTitle": "Frigate レビュータイムスタンプ: {{camera}}"
}
},
"imagePicker": {
diff --git a/web/public/locales/ja/components/filter.json b/web/public/locales/ja/components/filter.json
index bbcc3149d0..2bcd21bfa5 100644
--- a/web/public/locales/ja/components/filter.json
+++ b/web/public/locales/ja/components/filter.json
@@ -64,7 +64,7 @@
"cameras": {
"label": "カメラフィルター",
"all": {
- "title": "すべてのカメラ",
+ "title": "全カメラ",
"short": "カメラ"
}
},
@@ -114,7 +114,7 @@
},
"trackedObjectDelete": {
"title": "削除の確認",
- "desc": "これら {{objectLength}} 件の追跡オブジェクトを削除すると、スナップショット、保存された埋め込み、関連するオブジェクトのライフサイクル項目が削除されます。履歴ビューの録画映像は削除されません 。 続行してもよろしいですか? 今後このダイアログを表示しない場合は Shift キーを押しながら操作してください。",
+ "desc": "これら {{objectLength}} 件の追跡オブジェクトを削除すると、スナップショット、保存された埋め込み、関連するオブジェクトのライフサイクル項目が削除されます。履歴ビューの録画映像は削除されません 。 続行してもよろしいですか? 今後このダイアログを表示しない場合は Shift キーを押しながら操作してください。",
"toast": {
"success": "追跡オブジェクトを削除しました。",
"error": "追跡オブジェクトの削除に失敗しました: {{errorMessage}}"
diff --git a/web/public/locales/ja/components/player.json b/web/public/locales/ja/components/player.json
index 93befd9745..692cde4218 100644
--- a/web/public/locales/ja/components/player.json
+++ b/web/public/locales/ja/components/player.json
@@ -8,7 +8,8 @@
},
"submitFrigatePlus": {
"title": "このフレームを Frigate+ に送信しますか?",
- "submit": "送信"
+ "submit": "送信",
+ "previewError": "スナップショットのプレビューを読み込めませんでした。現在、この録画は利用できない可能性があります。"
},
"livePlayerRequiredIOSVersion": "このライブストリームタイプには iOS 17.1 以上が必要です。",
"cameraDisabled": "カメラは無効です",
@@ -19,7 +20,7 @@
},
"bandwidth": {
"title": "帯域:",
- "short": "帯域"
+ "short": "帯域幅"
},
"latency": {
"title": "遅延:",
@@ -47,5 +48,6 @@
"error": {
"submitFrigatePlusFailed": "フレームの Frigate+ への送信に失敗しました"
}
- }
+ },
+ "cameraOff": "カメラはオフです"
}
diff --git a/web/public/locales/ja/config/cameras.json b/web/public/locales/ja/config/cameras.json
index 8c5cb3254f..aa6ca74e74 100644
--- a/web/public/locales/ja/config/cameras.json
+++ b/web/public/locales/ja/config/cameras.json
@@ -1,22 +1,953 @@
{
"label": "カメラ設定",
"name": {
- "label": "カメラ名"
+ "label": "カメラ名",
+ "description": "カメラ名は必須です"
},
"enabled": {
"label": "有効",
"description": "有効"
},
"audio": {
- "label": "音声イベント",
+ "label": "音声検知",
"enabled": {
- "label": "音声検知を有効化"
+ "label": "音声検知を有効化",
+ "description": "このカメラの音声イベント検知を有効または無効にします。"
},
"min_volume": {
- "label": "最小ボリューム"
+ "label": "最小ボリューム",
+ "description": "音声検知を実行するために必要な最小RMS音量閾値。値を小さくすると感度が高くなります(例:200=高、500=中、1000=低)。"
},
"filters": {
- "label": "音声フィルタ"
+ "label": "音声フィルタ",
+ "description": "誤検知を減らすために使用される信頼度閾値などのフィルタ設定(オーディオタイプごと)。",
+ "threshold": {
+ "label": "音声の最低信頼度",
+ "description": "音声イベントとしてカウントするために必要な最低信頼度しきい値。"
+ }
+ },
+ "description": "このカメラの音声ベースのイベント検知設定。",
+ "max_not_heard": {
+ "label": "タイムアウト終了",
+ "description": "音声検知が終了するまでの残り秒数(設定されたオーディオタイプを除く)。"
+ },
+ "listen": {
+ "label": "リスニングタイプ",
+ "description": "検知対象の音声イベントの種類一覧(例:吠え声、火災報知器、悲鳴、会話、叫び声)。"
+ },
+ "enabled_in_config": {
+ "label": "元の音声状態",
+ "description": "静的設定ファイルで、音声検知が当初有効にされていたかどうかを示します。"
+ },
+ "num_threads": {
+ "label": "検知スレッド",
+ "description": "音声検知処理に使用するスレッド数。"
}
+ },
+ "friendly_name": {
+ "label": "表示名",
+ "description": "Frigate UIで使用されるカメラの表示名"
+ },
+ "audio_transcription": {
+ "label": "音声文字起こし",
+ "description": "イベントやリアルタイム字幕に使用される、ライブ音声およびスピーチ音声の文字起こし設定。",
+ "enabled": {
+ "label": "音声文字起こしを有効にする",
+ "description": "手動でトリガーされる音声イベントの文字起こしを有効または無効にします。"
+ },
+ "enabled_in_config": {
+ "label": "元の文字起こし状態"
+ },
+ "live_enabled": {
+ "label": "ライブ文字起こし",
+ "description": "音声を受信した時点で、リアルタイム文字起こしを有効にします。"
+ }
+ },
+ "birdseye": {
+ "label": "バードアイ",
+ "description": "複数のカメラ映像を1つのレイアウトに合成する「バードアイ」合成ビューの設定。",
+ "enabled": {
+ "label": "バードアイを有効にする",
+ "description": "バードアイビュー機能を有効または無効にします。"
+ },
+ "mode": {
+ "label": "トラッキングモード",
+ "description": "バードアイにカメラを含めるモード:「オブジェクト」「モーション」または「連続」。"
+ },
+ "order": {
+ "label": "位置",
+ "description": "バードアイレイアウトにおけるカメラの並び順を決定する数値。"
+ }
+ },
+ "detect": {
+ "label": "物体検知",
+ "description": "物体検知の実行やトラッカーの初期化に使用される、検知や検知ロールの設定。",
+ "enabled": {
+ "label": "物体検知を有効にする",
+ "description": "このカメラの物体検知機能を有効または無効にします。"
+ },
+ "height": {
+ "label": "高さを検知",
+ "description": "検知ストリームに使用するフレーム高さ(ピクセル)。ネイティブストリーム解像度を使用する場合は、空欄のままにしてください。"
+ },
+ "width": {
+ "label": "幅を検知",
+ "description": "検知ストリームで使用するフレーム幅 (ピクセル)。空欄でストリームのネイティブ解像度を使用。"
+ },
+ "fps": {
+ "label": "検知 FPS",
+ "description": "検知を実行する目標 FPS。低くするほど CPU 使用率が下がります(推奨値は 5、極めて高速な物体を追跡する場合のみ最大 10 まで上げてください)。"
+ },
+ "min_initialized": {
+ "label": "最小初期化フレーム数",
+ "description": "追跡オブジェクトを生成するために必要な連続検知ヒット数。値を大きくすると誤初期化が減ります。デフォルトは fps の半分。"
+ },
+ "max_disappeared": {
+ "label": "最大消失フレーム数",
+ "description": "追跡オブジェクトが消失したと判断するまでの未検知フレーム数。"
+ },
+ "stationary": {
+ "label": "静止オブジェクト設定",
+ "description": "一定時間静止しているオブジェクトを検知・管理するための設定。",
+ "interval": {
+ "label": "静止チェック間隔",
+ "description": "静止オブジェクトを確認するための検知を、何フレームおきに実行するか。"
+ },
+ "threshold": {
+ "label": "静止しきい値",
+ "description": "オブジェクトを静止状態とみなすために必要な位置変化のないフレーム数。"
+ },
+ "max_frames": {
+ "label": "最大追跡フレーム",
+ "description": "静止オブジェクトを破棄するまでの追跡フレーム数の上限。",
+ "default": {
+ "label": "デフォルト最大フレーム",
+ "description": "静止オブジェクトの追跡を停止するまでのデフォルト最大フレーム数。"
+ },
+ "objects": {
+ "label": "オブジェクト別最大フレーム",
+ "description": "静止オブジェクト追跡の最大フレーム数をオブジェクトごとに上書きします。"
+ }
+ },
+ "classifier": {
+ "label": "ビジュアル分類器を有効化",
+ "description": "バウンディングボックスが揺らいでも真に静止しているオブジェクトを検知するため、ビジュアル分類器を使用します。"
+ }
+ },
+ "annotation_offset": {
+ "label": "注釈オフセット",
+ "description": "タイムライン上のバウンディングボックスを録画と揃えるため、検知注釈を時間方向にずらすミリ秒数。正負どちらも指定可能。"
+ }
+ },
+ "mqtt": {
+ "label": "MQTT",
+ "description": "MQTT 画像配信の設定。",
+ "enabled": {
+ "label": "画像送信",
+ "description": "このカメラのオブジェクト画像スナップショットを MQTT トピックに配信する機能を有効にします。"
+ },
+ "timestamp": {
+ "label": "タイムスタンプを追加",
+ "description": "MQTT に配信する画像にタイムスタンプを重ねて表示します。"
+ },
+ "bounding_box": {
+ "label": "バウンディングボックスを追加",
+ "description": "MQTT に配信する画像にバウンディングボックスを描画します。"
+ },
+ "crop": {
+ "label": "画像を切り抜き",
+ "description": "MQTT に配信する画像を検知オブジェクトのバウンディングボックスで切り抜きます。"
+ },
+ "height": {
+ "label": "画像の高さ",
+ "description": "MQTT 配信時に画像をリサイズする高さ (ピクセル)。"
+ },
+ "required_zones": {
+ "label": "必須ゾーン",
+ "description": "MQTT 画像を配信するためにオブジェクトが進入する必要があるゾーン。"
+ },
+ "quality": {
+ "label": "JPEG 品質",
+ "description": "MQTT に配信する画像の JPEG 品質 (0-100)。"
+ }
+ },
+ "notifications": {
+ "label": "通知",
+ "enabled": {
+ "label": "通知を有効化",
+ "description": "このカメラの通知を有効または無効にします。"
+ },
+ "email": {
+ "label": "通知メールアドレス",
+ "description": "プッシュ通知用、または特定の通知プロバイダで必要となるメールアドレス。"
+ },
+ "cooldown": {
+ "label": "クールダウン期間",
+ "description": "受信者への通知連投を避けるための通知間隔(秒)。"
+ },
+ "enabled_in_config": {
+ "label": "元の通知状態",
+ "description": "元の静的設定で通知が有効化されていたかを示します。"
+ },
+ "description": "このカメラの通知を有効化・制御する設定。"
+ },
+ "ffmpeg": {
+ "label": "FFmpeg",
+ "description": "FFmpeg の設定。バイナリパス、引数、ハードウェアアクセラレーション、ロール別の出力引数を含みます。",
+ "path": {
+ "label": "FFmpeg パス",
+ "description": "使用する FFmpeg バイナリのパス、またはバージョンエイリアス(「5.0」または「7.0」)。"
+ },
+ "global_args": {
+ "label": "FFmpeg グローバル引数",
+ "description": "FFmpeg プロセスに渡されるグローバル引数。"
+ },
+ "hwaccel_args": {
+ "label": "ハードウェアアクセラレーション引数",
+ "description": "FFmpeg のハードウェアアクセラレーション引数。プロバイダ固有のプリセットの使用を推奨。"
+ },
+ "input_args": {
+ "label": "入力引数",
+ "description": "FFmpeg の入力ストリームに適用される引数。"
+ },
+ "output_args": {
+ "label": "出力引数",
+ "description": "detect や record など、FFmpeg のロール別に使用されるデフォルト出力引数。",
+ "detect": {
+ "label": "検知ロールの出力引数",
+ "description": "detect ロールのストリームに使用されるデフォルト出力引数。"
+ },
+ "record": {
+ "label": "録画ロールの出力引数",
+ "description": "record ロールのストリームに使用されるデフォルト出力引数。"
+ }
+ },
+ "retry_interval": {
+ "label": "FFmpeg 再試行間隔",
+ "description": "カメラストリームの失敗後、再接続を試みるまでの待機秒数。デフォルトは 10 秒。"
+ },
+ "apple_compatibility": {
+ "label": "Apple 互換性",
+ "description": "H.265 録画時に Apple プレーヤーとの互換性向上のため HEVC タグ付けを有効化します。"
+ },
+ "gpu": {
+ "label": "GPU インデックス",
+ "description": "ハードウェアアクセラレーションで使用するデフォルト GPU インデックス。"
+ },
+ "inputs": {
+ "label": "カメラ入力",
+ "description": "このカメラの入力ストリーム定義(パスとロール)のリスト。",
+ "path": {
+ "label": "入力パス",
+ "description": "カメラ入力ストリームの URL またはパス。"
+ },
+ "roles": {
+ "label": "入力ロール",
+ "description": "この入力ストリームのロール。"
+ },
+ "global_args": {
+ "label": "FFmpeg グローバル引数",
+ "description": "この入力ストリームに対する FFmpeg グローバル引数。"
+ },
+ "hwaccel_args": {
+ "label": "ハードウェアアクセラレーション引数",
+ "description": "この入力ストリームのハードウェアアクセラレーション引数。"
+ },
+ "input_args": {
+ "label": "入力引数",
+ "description": "このストリーム固有の入力引数。"
+ }
+ }
+ },
+ "live": {
+ "label": "ライブ再生",
+ "streams": {
+ "label": "ライブストリーム名",
+ "description": "設定済みのストリーム名と、ライブ再生で使用する restream/go2rtc 名のマッピング。"
+ },
+ "height": {
+ "label": "ライブの高さ",
+ "description": "Web UI で jsmpeg ライブストリームを描画する高さ (ピクセル)。検知ストリーム高さ以下である必要があります。"
+ },
+ "quality": {
+ "label": "ライブ品質",
+ "description": "jsmpeg ストリームのエンコード品質 (1 が最高、31 が最低)。"
+ },
+ "description": "ライブストリームの選択・解像度・品質を Web UI から制御する設定。"
+ },
+ "motion": {
+ "label": "モーション検知",
+ "enabled": {
+ "label": "モーション検知を有効化",
+ "description": "このカメラのモーション検知を有効または無効にします。"
+ },
+ "threshold": {
+ "label": "モーションしきい値",
+ "description": "モーション検出器が使用するピクセル差分しきい値。値を大きくすると感度が下がります (範囲 1-255)。"
+ },
+ "lightning_threshold": {
+ "label": "雷検知しきい値",
+ "description": "短時間の照明スパイクを検知して無視するしきい値(値が小さいほど感度が高く、0.3 〜 1.0 が目安)。これはモーション検知を完全に止めるものではなく、しきい値超過後に検出器が追加フレームの解析を停止するだけです。モーションベースの録画はこれらのイベント中も作成されます。"
+ },
+ "skip_motion_threshold": {
+ "label": "モーションスキップしきい値",
+ "description": "0.0 〜 1.0 の値を指定し、1 フレームでそれ以上の割合が変化した場合、検出器はモーションボックスを返さず即座に再キャリブレーションします。雷や嵐などの誤検知を減らし CPU を節約できますが、PTZ カメラのオート追跡などの実イベントを取りこぼす可能性があります。数 MB の録画を捨てるか、数本の短いクリップを確認するかのトレードオフです。無効化するには未設定 (None) のままにします。"
+ },
+ "improve_contrast": {
+ "label": "コントラスト強調",
+ "description": "モーション解析前にフレームのコントラストを強調して検知を補助します。"
+ },
+ "contour_area": {
+ "label": "輪郭面積",
+ "description": "モーション輪郭としてカウントするために必要な最小ピクセル数。"
+ },
+ "delta_alpha": {
+ "label": "デルタアルファ",
+ "description": "モーション計算のフレーム差分で使用されるアルファブレンディング係数。"
+ },
+ "frame_alpha": {
+ "label": "フレームアルファ",
+ "description": "モーション前処理でフレームをブレンドする際に使用するアルファ値。"
+ },
+ "frame_height": {
+ "label": "フレーム高さ",
+ "description": "モーション計算時にフレームをスケーリングする高さ (ピクセル)。"
+ },
+ "mask": {
+ "label": "マスク座標",
+ "description": "領域を含める/除外するモーションマスクポリゴンを定義する x,y 座標の順序付きリスト。"
+ },
+ "mqtt_off_delay": {
+ "label": "MQTT オフ遅延",
+ "description": "最後のモーション検知後、MQTT で「off」状態を発行するまでの待機秒数。"
+ },
+ "enabled_in_config": {
+ "label": "元のモーション状態",
+ "description": "元の静的設定でモーション検知が有効化されていたかを示します。"
+ },
+ "raw_mask": {
+ "label": "Raw マスク"
+ },
+ "description": "このカメラのモーション検知のデフォルト設定。"
+ },
+ "objects": {
+ "label": "オブジェクト",
+ "description": "追跡するラベルとオブジェクト別フィルタを含む、オブジェクト追跡のデフォルト設定。",
+ "track": {
+ "label": "追跡するオブジェクト",
+ "description": "このカメラで追跡するオブジェクトラベルのリスト。"
+ },
+ "filters": {
+ "label": "オブジェクトフィルタ",
+ "description": "誤検知を減らすために検知オブジェクトに適用するフィルタ(面積、比率、信頼度)。",
+ "min_area": {
+ "label": "最小オブジェクト面積",
+ "description": "このオブジェクト種別に必要なバウンディングボックスの最小面積(ピクセルまたは割合)。ピクセル (int) または割合 (0.000001 〜 0.99 の float) を指定可能。"
+ },
+ "max_area": {
+ "label": "最大オブジェクト面積",
+ "description": "このオブジェクト種別に許容されるバウンディングボックスの最大面積(ピクセルまたは割合)。ピクセル (int) または割合 (0.000001 〜 0.99 の float) を指定可能。"
+ },
+ "min_ratio": {
+ "label": "最小アスペクト比",
+ "description": "対象となるバウンディングボックスに必要な最小の幅/高さ比。"
+ },
+ "max_ratio": {
+ "label": "最大アスペクト比",
+ "description": "対象となるバウンディングボックスに許容される最大の幅/高さ比。"
+ },
+ "threshold": {
+ "label": "信頼度しきい値",
+ "description": "オブジェクトを真陽性とみなすために必要な平均検知信頼度。"
+ },
+ "min_score": {
+ "label": "最低信頼度",
+ "description": "オブジェクトをカウントするために必要な単一フレームでの最低検知信頼度。"
+ },
+ "mask": {
+ "label": "フィルタマスク",
+ "description": "フレーム内でこのフィルタが適用される範囲を定義するポリゴン座標。"
+ },
+ "raw_mask": {
+ "label": "Raw マスク"
+ }
+ },
+ "mask": {
+ "label": "オブジェクトマスク",
+ "description": "指定領域でオブジェクト検知を行わないようにするためのマスクポリゴン。"
+ },
+ "raw_mask": {
+ "label": "Raw マスク"
+ },
+ "genai": {
+ "label": "GenAI オブジェクト設定",
+ "description": "追跡オブジェクトの説明生成や、生成 AI へのフレーム送信に関する GenAI オプション。",
+ "enabled": {
+ "label": "GenAI を有効化",
+ "description": "追跡オブジェクトの説明を GenAI で生成する機能を既定で有効にします。"
+ },
+ "use_snapshot": {
+ "label": "スナップショットを使用",
+ "description": "GenAI 説明生成にサムネイルではなくオブジェクトスナップショットを使用します。"
+ },
+ "prompt": {
+ "label": "キャプションプロンプト",
+ "description": "GenAI で説明を生成する際に使用するデフォルトのプロンプトテンプレート。"
+ },
+ "object_prompts": {
+ "label": "オブジェクト別プロンプト",
+ "description": "特定のラベルに対する GenAI 出力をカスタマイズするためのオブジェクト別プロンプト。"
+ },
+ "objects": {
+ "label": "GenAI 対象オブジェクト",
+ "description": "GenAI に既定で送信するオブジェクトラベルのリスト。"
+ },
+ "required_zones": {
+ "label": "必須ゾーン",
+ "description": "GenAI 説明生成の対象となるためにオブジェクトが進入する必要があるゾーン。"
+ },
+ "debug_save_thumbnails": {
+ "label": "サムネイルを保存",
+ "description": "デバッグや確認のため、GenAI に送信したサムネイルを保存します。"
+ },
+ "send_triggers": {
+ "label": "GenAI 送信トリガー",
+ "description": "フレームを GenAI に送るタイミング(終了時、更新後など)を定義します。",
+ "tracked_object_end": {
+ "label": "終了時に送信",
+ "description": "追跡オブジェクトが終了した時点で GenAI にリクエストを送信します。"
+ },
+ "after_significant_updates": {
+ "label": "早期 GenAI トリガー",
+ "description": "追跡オブジェクトに対して指定回数の重要な更新があった後、GenAI にリクエストを送信します。"
+ }
+ },
+ "enabled_in_config": {
+ "label": "元の GenAI 状態",
+ "description": "元の静的設定で GenAI が有効化されていたかを示します。"
+ }
+ }
+ },
+ "record": {
+ "label": "録画",
+ "enabled": {
+ "label": "録画を有効化",
+ "description": "このカメラの録画を有効または無効にします。"
+ },
+ "expire_interval": {
+ "label": "録画クリーンアップ間隔",
+ "description": "期限切れ録画セグメントを削除するクリーンアップを実行する間隔 (分)。"
+ },
+ "continuous": {
+ "label": "常時保持",
+ "description": "追跡オブジェクトやモーションに関係なく録画を保持する日数。アラートと検知の録画のみを保持したい場合は 0 を指定します。",
+ "days": {
+ "label": "保持日数",
+ "description": "録画を保持する日数。"
+ }
+ },
+ "motion": {
+ "label": "モーション録画保持",
+ "description": "追跡オブジェクトに関係なくモーションでトリガーされた録画を保持する日数。アラートと検知の録画のみを保持したい場合は 0 を指定します。",
+ "days": {
+ "label": "保持日数",
+ "description": "録画を保持する日数。"
+ }
+ },
+ "detections": {
+ "label": "検知録画保持",
+ "description": "検知イベントの録画保持設定。前後の撮影時間を含みます。",
+ "pre_capture": {
+ "label": "イベント前秒数",
+ "description": "検知イベントの前に録画に含める秒数。"
+ },
+ "post_capture": {
+ "label": "イベント後秒数",
+ "description": "検知イベントの後に録画に含める秒数。"
+ },
+ "retain": {
+ "label": "イベント保持",
+ "description": "検知イベントの録画保持設定。",
+ "days": {
+ "label": "保持日数",
+ "description": "検知イベント録画を保持する日数。"
+ },
+ "mode": {
+ "label": "保持モード",
+ "description": "保持モード: all(全セグメントを保存)、motion(モーションがあるセグメントを保存)、active_objects(アクティブなオブジェクトを含むセグメントを保存)。"
+ }
+ }
+ },
+ "alerts": {
+ "label": "アラート録画保持",
+ "description": "アラートイベントの録画保持設定。前後の撮影時間を含みます。",
+ "pre_capture": {
+ "label": "イベント前秒数",
+ "description": "アラートイベントの前に録画に含める秒数。"
+ },
+ "post_capture": {
+ "label": "イベント後秒数",
+ "description": "アラートイベントの後に録画に含める秒数。"
+ },
+ "retain": {
+ "label": "イベント保持",
+ "description": "アラートイベントの録画保持設定。",
+ "days": {
+ "label": "保持日数",
+ "description": "アラートイベント録画を保持する日数。"
+ },
+ "mode": {
+ "label": "保持モード",
+ "description": "保持モード: all(全セグメントを保存)、motion(モーションがあるセグメントを保存)、active_objects(アクティブなオブジェクトを含むセグメントを保存)。"
+ }
+ }
+ },
+ "export": {
+ "label": "エクスポート設定",
+ "description": "タイムラプスやハードウェアアクセラレーションなど、録画をエクスポートする際に使用する設定。",
+ "hwaccel_args": {
+ "label": "エクスポート用 hwaccel 引数",
+ "description": "エクスポート/トランスコード処理で使用するハードウェアアクセラレーション引数。"
+ },
+ "max_concurrent": {
+ "label": "同時エクスポート数の上限",
+ "description": "同時に処理するエクスポートジョブの最大数。"
+ }
+ },
+ "preview": {
+ "label": "プレビュー設定",
+ "description": "UI に表示される録画プレビューの品質を制御する設定。",
+ "quality": {
+ "label": "プレビュー品質",
+ "description": "プレビュー品質レベル (very_low, low, medium, high, very_high)。"
+ }
+ },
+ "enabled_in_config": {
+ "label": "元の録画状態",
+ "description": "元の静的設定で録画が有効化されていたかを示します。"
+ },
+ "description": "このカメラの録画と保持に関する設定。"
+ },
+ "review": {
+ "label": "レビュー",
+ "alerts": {
+ "label": "アラート設定",
+ "description": "どの追跡オブジェクトがアラートを生成するか、およびアラートの保持方法に関する設定。",
+ "enabled": {
+ "label": "アラートを有効化",
+ "description": "このカメラのアラート生成を有効または無効にします。"
+ },
+ "labels": {
+ "label": "アラートラベル",
+ "description": "アラート対象となるオブジェクトラベルのリスト(例: car, person)。"
+ },
+ "required_zones": {
+ "label": "必須ゾーン",
+ "description": "アラートとみなされるためにオブジェクトが進入する必要があるゾーン。空欄ですべてのゾーンを許可。"
+ },
+ "enabled_in_config": {
+ "label": "元のアラート状態",
+ "description": "元の静的設定でアラートが有効化されていたかを記録します。"
+ },
+ "cutoff_time": {
+ "label": "アラート打ち切り時間",
+ "description": "アラートを引き起こすアクティビティが途絶えてからアラートを打ち切るまでの待機秒数。"
+ }
+ },
+ "detections": {
+ "label": "検知設定",
+ "description": "どの追跡オブジェクトが(アラートではない)検知を生成するか、および検知の保持方法に関する設定。",
+ "enabled": {
+ "label": "検知を有効化",
+ "description": "このカメラの検知イベントを有効または無効にします。"
+ },
+ "labels": {
+ "label": "検知ラベル",
+ "description": "検知イベントの対象となるオブジェクトラベルのリスト。"
+ },
+ "required_zones": {
+ "label": "必須ゾーン",
+ "description": "検知とみなされるためにオブジェクトが進入する必要があるゾーン。空欄ですべてのゾーンを許可。"
+ },
+ "cutoff_time": {
+ "label": "検知打ち切り時間",
+ "description": "検知を引き起こすアクティビティが途絶えてから検知を打ち切るまでの待機秒数。"
+ },
+ "enabled_in_config": {
+ "label": "元の検知状態",
+ "description": "元の静的設定で検知が有効化されていたかを記録します。"
+ }
+ },
+ "genai": {
+ "label": "GenAI 設定",
+ "description": "レビュー項目の説明やサマリーを生成 AI で作成する機能の制御。",
+ "enabled": {
+ "label": "GenAI 説明を有効化",
+ "description": "レビュー項目の GenAI 生成説明やサマリーを有効または無効にします。"
+ },
+ "alerts": {
+ "label": "アラートに GenAI を使用",
+ "description": "アラート項目の説明を GenAI で生成します。"
+ },
+ "detections": {
+ "label": "検知に GenAI を使用",
+ "description": "検知項目の説明を GenAI で生成します。"
+ },
+ "image_source": {
+ "label": "レビュー画像ソース",
+ "description": "GenAI に送信する画像のソース(「preview」または「recordings」)。「recordings」は高品質ですがトークン消費が増えます。"
+ },
+ "additional_concerns": {
+ "label": "追加の懸念事項",
+ "description": "このカメラのアクティビティ評価時に GenAI に考慮させる追加の懸念や注意事項のリスト。"
+ },
+ "debug_save_thumbnails": {
+ "label": "サムネイルを保存",
+ "description": "デバッグや確認のため、GenAI プロバイダに送信したサムネイルを保存します。"
+ },
+ "enabled_in_config": {
+ "label": "元の GenAI 状態",
+ "description": "元の静的設定で GenAI レビューが有効化されていたかを記録します。"
+ },
+ "preferred_language": {
+ "label": "希望言語",
+ "description": "GenAI プロバイダに生成応答で要求する言語。"
+ },
+ "activity_context_prompt": {
+ "label": "活動コンテキストプロンプト",
+ "description": "GenAI サマリーの文脈として、何が不審な活動で何がそうでないかを記述するカスタムプロンプト。"
+ }
+ },
+ "description": "このカメラの UI 表示・保存で使用するアラート、検知、GenAI レビューサマリーの設定。"
+ },
+ "snapshots": {
+ "label": "スナップショット",
+ "enabled": {
+ "label": "スナップショットを有効化",
+ "description": "このカメラのスナップショット保存を有効または無効にします。"
+ },
+ "timestamp": {
+ "label": "タイムスタンプ重ね合わせ",
+ "description": "API スナップショットにタイムスタンプを重ねて表示します。"
+ },
+ "bounding_box": {
+ "label": "バウンディングボックス重ね合わせ",
+ "description": "API スナップショットに追跡オブジェクトのバウンディングボックスを描画します。"
+ },
+ "crop": {
+ "label": "スナップショットを切り抜き",
+ "description": "API スナップショットを検知オブジェクトのバウンディングボックスで切り抜きます。"
+ },
+ "required_zones": {
+ "label": "必須ゾーン",
+ "description": "スナップショットを保存するためにオブジェクトが進入する必要があるゾーン。"
+ },
+ "height": {
+ "label": "スナップショット高さ",
+ "description": "API スナップショットをリサイズする高さ (ピクセル)。空欄で元のサイズを維持。"
+ },
+ "retain": {
+ "label": "スナップショット保持",
+ "description": "デフォルト保持日数とオブジェクト別上書きを含む、スナップショット保持設定。",
+ "default": {
+ "label": "デフォルト保持期間",
+ "description": "スナップショットを保持するデフォルト日数。"
+ },
+ "mode": {
+ "label": "保持モード",
+ "description": "保持モード: all(全セグメントを保存)、motion(モーションがあるセグメントを保存)、active_objects(アクティブなオブジェクトを含むセグメントを保存)。"
+ },
+ "objects": {
+ "label": "オブジェクト別保持",
+ "description": "オブジェクトごとのスナップショット保持日数の上書き。"
+ }
+ },
+ "quality": {
+ "label": "スナップショット品質",
+ "description": "保存するスナップショットのエンコード品質 (0-100)。"
+ },
+ "description": "このカメラの追跡オブジェクトに対する API 生成スナップショットの設定。"
+ },
+ "timestamp_style": {
+ "label": "タイムスタンプスタイル",
+ "position": {
+ "label": "タイムスタンプ位置",
+ "description": "画像内のタイムスタンプ位置 (tl/tr/bl/br)。"
+ },
+ "format": {
+ "label": "タイムスタンプ書式",
+ "description": "タイムスタンプに使用する日時書式文字列 (Python datetime 書式コード)。"
+ },
+ "color": {
+ "label": "タイムスタンプ色",
+ "description": "タイムスタンプ文字色の RGB 値 (各 0-255)。",
+ "red": {
+ "label": "赤",
+ "description": "タイムスタンプ色の赤成分 (0-255)。"
+ },
+ "green": {
+ "label": "緑",
+ "description": "タイムスタンプ色の緑成分 (0-255)。"
+ },
+ "blue": {
+ "label": "青",
+ "description": "タイムスタンプ色の青成分 (0-255)。"
+ }
+ },
+ "thickness": {
+ "label": "タイムスタンプ太さ",
+ "description": "タイムスタンプ文字の線の太さ。"
+ },
+ "effect": {
+ "label": "タイムスタンプエフェクト",
+ "description": "タイムスタンプ文字の視覚効果 (none, solid, shadow)。"
+ },
+ "description": "スナップショットおよびデバッグビューに適用されるタイムスタンプの表示設定。"
+ },
+ "semantic_search": {
+ "label": "セマンティック検索",
+ "triggers": {
+ "label": "トリガー",
+ "description": "カメラ別のセマンティック検索トリガーの動作と一致条件。",
+ "friendly_name": {
+ "label": "表示名",
+ "description": "このトリガーの UI 表示用の任意の名前。"
+ },
+ "enabled": {
+ "label": "このトリガーを有効化",
+ "description": "このセマンティック検索トリガーを有効または無効にします。"
+ },
+ "type": {
+ "label": "トリガー種別",
+ "description": "トリガー種別: 「thumbnail」(画像に対する一致)または「description」(テキストに対する一致)。"
+ },
+ "data": {
+ "label": "トリガー内容",
+ "description": "追跡オブジェクトと照合するテキストフレーズまたはサムネイル ID。"
+ },
+ "threshold": {
+ "label": "トリガーしきい値",
+ "description": "このトリガーを発火させるために必要な最低類似度スコア (0-1)。"
+ },
+ "actions": {
+ "label": "トリガーアクション",
+ "description": "トリガー一致時に実行するアクションのリスト (notification, sub_label, attribute)。"
+ }
+ },
+ "description": "オブジェクト埋め込みを構築・クエリして類似項目を見つけるセマンティック検索の設定。"
+ },
+ "face_recognition": {
+ "label": "顔認識",
+ "enabled": {
+ "label": "顔認識を有効化",
+ "description": "顔認識を有効または無効にします。"
+ },
+ "min_area": {
+ "label": "顔の最小面積",
+ "description": "認識を試みるために必要な顔ボックスの最小面積 (ピクセル)。"
+ },
+ "description": "このカメラの顔検知と顔認識の設定。"
+ },
+ "lpr": {
+ "label": "ナンバープレート認識",
+ "description": "ナンバープレート認識の設定。検知しきい値、書式整形、既知ナンバーなどを含みます。",
+ "enabled": {
+ "label": "LPR を有効化",
+ "description": "このカメラで LPR を有効または無効にします。"
+ },
+ "min_area": {
+ "label": "プレート最小面積",
+ "description": "認識を試みるために必要なプレート最小面積 (ピクセル)。"
+ },
+ "enhancement": {
+ "label": "強調レベル",
+ "description": "OCR 前にプレート切り出し画像に適用する強調レベル (0-10)。値を大きくしても常に改善するとは限らず、5 を超えると夜間プレートでのみ有効な場合があるため注意が必要です。"
+ },
+ "expire_time": {
+ "label": "失効秒数",
+ "description": "未検知ナンバーをトラッカーから失効させるまでの秒数(専用 LPR カメラのみ)。"
+ }
+ },
+ "profiles": {
+ "label": "プロファイル",
+ "description": "実行時に有効化できる部分上書きを持つ、名前付きの設定プロファイル。"
+ },
+ "onvif": {
+ "label": "ONVIF",
+ "description": "このカメラの ONVIF 接続および PTZ オート追跡の設定。",
+ "host": {
+ "label": "ONVIF ホスト",
+ "description": "このカメラの ONVIF サービスのホスト(オプションでスキーマも)。"
+ },
+ "port": {
+ "label": "ONVIF ポート",
+ "description": "ONVIF サービスのポート番号。"
+ },
+ "user": {
+ "label": "ONVIF ユーザー名",
+ "description": "ONVIF 認証用のユーザー名。ONVIF に admin ユーザーが必要なデバイスもあります。"
+ },
+ "password": {
+ "label": "ONVIF パスワード",
+ "description": "ONVIF 認証用のパスワード。"
+ },
+ "tls_insecure": {
+ "label": "TLS 検証を無効化",
+ "description": "ONVIF の TLS 検証をスキップし、ダイジェスト認証も無効化します(安全でないため、信頼できるネットワークでのみ使用)。"
+ },
+ "profile": {
+ "label": "ONVIF プロファイル",
+ "description": "PTZ 制御に使用する ONVIF メディアプロファイル(トークンまたは名前で指定)。未設定の場合、有効な PTZ 設定を持つ最初のプロファイルが自動選択されます。"
+ },
+ "autotracking": {
+ "label": "オートトラッキング",
+ "description": "PTZ カメラの動作で移動中のオブジェクトを自動追跡し、フレーム中央に保ちます。",
+ "enabled": {
+ "label": "オートトラッキングを有効化",
+ "description": "検知オブジェクトの PTZ オート追跡を有効または無効にします。"
+ },
+ "calibrate_on_startup": {
+ "label": "起動時にキャリブレーション",
+ "description": "追跡精度を向上させるため、起動時に PTZ モーター速度を測定します。キャリブレーション後に movement_weights が設定に書き込まれます。"
+ },
+ "zooming": {
+ "label": "ズームモード",
+ "description": "ズーム動作の制御: disabled (パン/チルトのみ)、absolute (互換性が最も高い)、relative (パン/チルト/ズーム同時)。"
+ },
+ "zoom_factor": {
+ "label": "ズーム倍率",
+ "description": "追跡対象オブジェクトのズームレベルを制御します。値が小さいほど広い範囲を保ち、大きいほどズームインしますが追跡を失う可能性があります。0.1 〜 0.75 の範囲で指定。"
+ },
+ "track": {
+ "label": "追跡対象オブジェクト",
+ "description": "オートトラッキングを発動させるオブジェクト種別のリスト。"
+ },
+ "required_zones": {
+ "label": "必須ゾーン",
+ "description": "オートトラッキング開始前にオブジェクトが進入する必要があるゾーン。"
+ },
+ "return_preset": {
+ "label": "復帰プリセット",
+ "description": "追跡終了後にカメラが戻る、ファームウェアに設定されている ONVIF プリセット名。"
+ },
+ "timeout": {
+ "label": "復帰タイムアウト",
+ "description": "追跡を失ってからカメラをプリセット位置に戻すまでの待機秒数。"
+ },
+ "movement_weights": {
+ "label": "動作重み",
+ "description": "カメラキャリブレーションによって自動生成される値。手動で変更しないでください。"
+ },
+ "enabled_in_config": {
+ "label": "元のオート追跡状態",
+ "description": "オートトラッキングが設定で有効化されていたかを追跡する内部フィールド。"
+ }
+ },
+ "ignore_time_mismatch": {
+ "label": "時刻差異を無視",
+ "description": "ONVIF 通信時に、カメラと Frigate サーバー間の時刻同期差異を無視します。"
+ }
+ },
+ "best_image_timeout": {
+ "label": "ベストイメージタイムアウト",
+ "description": "最高信頼度スコアの画像を取得するまでの待機時間。"
+ },
+ "type": {
+ "label": "カメラタイプ",
+ "description": "カメラタイプ"
+ },
+ "ui": {
+ "label": "カメラ UI",
+ "description": "UI 内でのこのカメラの表示順と表示設定。順序はデフォルトダッシュボードに影響します。より細かい制御にはカメラグループを使用してください。",
+ "order": {
+ "label": "UI 表示順",
+ "description": "UI 内でのカメラの並び順に使用される数値(デフォルトダッシュボードとリスト)。値が大きいほど後ろに表示されます。"
+ },
+ "dashboard": {
+ "label": "UI に表示",
+ "description": "このカメラを Frigate UI 全体に表示するかを切り替えます。無効化した場合、再表示するには設定ファイルを手動編集する必要があります。"
+ }
+ },
+ "webui_url": {
+ "label": "カメラ URL",
+ "description": "システムページからカメラに直接アクセスするための URL"
+ },
+ "zones": {
+ "label": "ゾーン",
+ "description": "ゾーンを使うとフレーム内の特定領域を定義でき、オブジェクトがその領域内にあるかを判定できます。",
+ "friendly_name": {
+ "label": "ゾーン名",
+ "description": "Frigate UI に表示されるユーザー向けのゾーン名。未設定の場合、ゾーン名の整形版が使用されます。"
+ },
+ "enabled": {
+ "label": "有効",
+ "description": "このゾーンを有効または無効にします。無効化したゾーンは実行時に無視されます。"
+ },
+ "enabled_in_config": {
+ "label": "ゾーンの元の状態を保持する。"
+ },
+ "filters": {
+ "label": "ゾーンフィルタ",
+ "description": "このゾーン内のオブジェクトに適用するフィルタ。誤検知を減らすか、ゾーン内に存在するとみなすオブジェクトを制限するために使用します。",
+ "min_area": {
+ "label": "最小オブジェクト面積",
+ "description": "このオブジェクト種別に必要なバウンディングボックスの最小面積(ピクセルまたは割合)。ピクセル (int) または割合 (0.000001 〜 0.99 の float) を指定可能。"
+ },
+ "max_area": {
+ "label": "最大オブジェクト面積",
+ "description": "このオブジェクト種別に許容されるバウンディングボックスの最大面積(ピクセルまたは割合)。ピクセル (int) または割合 (0.000001 〜 0.99 の float) を指定可能。"
+ },
+ "min_ratio": {
+ "label": "最小アスペクト比",
+ "description": "対象となるバウンディングボックスに必要な最小の幅/高さ比。"
+ },
+ "max_ratio": {
+ "label": "最大アスペクト比",
+ "description": "対象となるバウンディングボックスに許容される最大の幅/高さ比。"
+ },
+ "threshold": {
+ "label": "信頼度しきい値",
+ "description": "オブジェクトを真陽性とみなすために必要な平均検知信頼度。"
+ },
+ "min_score": {
+ "label": "最低信頼度",
+ "description": "オブジェクトをカウントするために必要な単一フレームでの最低検知信頼度。"
+ },
+ "mask": {
+ "label": "フィルタマスク",
+ "description": "フレーム内でこのフィルタが適用される範囲を定義するポリゴン座標。"
+ },
+ "raw_mask": {
+ "label": "Raw マスク"
+ }
+ },
+ "coordinates": {
+ "label": "座標",
+ "description": "ゾーン領域を定義するポリゴン座標。カンマ区切り文字列または座標文字列のリスト。座標は相対値 (0-1) または絶対値 (旧形式) で指定。"
+ },
+ "distances": {
+ "label": "実世界の距離",
+ "description": "ゾーン四辺形の各辺に対応する実世界の距離(任意)。速度や距離計算に使用します。設定する場合は必ず 4 つの値が必要です。"
+ },
+ "inertia": {
+ "label": "慣性フレーム数",
+ "description": "オブジェクトをゾーン内に存在するとみなすために、連続して検知される必要があるフレーム数。一時的な検知を除外するのに役立ちます。"
+ },
+ "loitering_time": {
+ "label": "うろつき秒数",
+ "description": "うろつきとみなされるためにオブジェクトがゾーン内にとどまる必要がある秒数。0 でうろつき検知を無効化。"
+ },
+ "speed_threshold": {
+ "label": "最低速度",
+ "description": "ゾーン内に存在するとみなされるためにオブジェクトに必要な最低速度(distances 設定時は実世界単位)。速度ベースのゾーントリガーに使用。"
+ },
+ "objects": {
+ "label": "トリガーオブジェクト",
+ "description": "このゾーンをトリガーできるオブジェクト種別のリスト(labelmap から)。文字列または文字列リストで指定。空ですべてのオブジェクトが対象。"
+ }
+ },
+ "enabled_in_config": {
+ "label": "元のカメラ状態",
+ "description": "カメラの元の状態を保持する。"
}
}
diff --git a/web/public/locales/ja/config/global.json b/web/public/locales/ja/config/global.json
index 2073a59d8c..3ccc593271 100644
--- a/web/public/locales/ja/config/global.json
+++ b/web/public/locales/ja/config/global.json
@@ -4,38 +4,1632 @@
"description": "有効にすると、トラブルシューティングのため機能を制限したセーフモードでFrigateを起動します。"
},
"environment_vars": {
- "label": "環境変数"
+ "label": "環境変数",
+ "description": "Home Assistant OS の Frigate プロセスに設定する環境変数のキー/値ペア。HAOS をご利用でない場合は、代わりに Docker の環境変数設定を使用してください。"
},
"audio": {
- "label": "音声イベント",
+ "label": "音声検知",
"enabled": {
- "label": "音声検知を有効化"
+ "label": "音声検知を有効化",
+ "description": "全カメラの音声イベント検知を有効または無効にします(カメラ別に上書き可能)。"
},
"min_volume": {
- "label": "最小ボリューム"
+ "label": "最小ボリューム",
+ "description": "音声検知を実行するために必要な最小RMS音量閾値。値を小さくすると感度が高くなります(例:200=高、500=中、1000=低)。"
},
"filters": {
- "label": "音声フィルタ"
- }
+ "label": "音声フィルタ",
+ "description": "誤検知を減らすために使用される信頼度閾値などのフィルタ設定(オーディオタイプごと)。",
+ "threshold": {
+ "label": "音声の最低信頼度",
+ "description": "音声イベントとしてカウントするために必要な最低信頼度しきい値。"
+ }
+ },
+ "max_not_heard": {
+ "label": "タイムアウト終了",
+ "description": "音声検知が終了するまでの残り秒数(設定されたオーディオタイプを除く)。"
+ },
+ "listen": {
+ "label": "リスニングタイプ",
+ "description": "検知対象の音声イベントの種類一覧(例:吠え声、火災報知器、悲鳴、会話、叫び声)。"
+ },
+ "enabled_in_config": {
+ "label": "元の音声状態",
+ "description": "静的設定ファイルで、音声検知が当初有効にされていたかどうかを示します。"
+ },
+ "num_threads": {
+ "label": "検知スレッド",
+ "description": "音声検知処理に使用するスレッド数。"
+ },
+ "description": "全カメラの音声ベースのイベント検知設定(カメラ別に上書き可能)。"
},
"logger": {
"default": {
- "label": "ログレベル"
+ "label": "ログレベル",
+ "description": "デフォルトのグローバルログの詳細度 (debug, info, warning, error)。"
},
"logs": {
- "label": "プロセス毎のログレベル"
- }
+ "label": "プロセス毎のログレベル",
+ "description": "コンポーネントごとのログレベルの上書きにより、特定のモジュールのログ詳細度を増減できます。"
+ },
+ "label": "ロギング",
+ "description": "デフォルトのログ詳細度とコンポーネントごとのログレベルの上書きを制御します。"
},
"auth": {
"label": "認証",
"enabled": {
- "label": "認証を有効化"
+ "label": "認証を有効化",
+ "description": "Frigate UI でネイティブ認証を有効にする。"
},
"reset_admin_password": {
- "label": "adminパスワードをリセット"
+ "label": "adminパスワードをリセット",
+ "description": "もし本当なら、起動時に管理者ユーザーのパスワードをリセットし、新しいパスワードをログに出力します。"
+ },
+ "description": "認証およびセッション関連の設定(Cookieやレート制限オプションを含む)。",
+ "cookie_name": {
+ "label": "JWT Cookie名",
+ "description": "ネイティブ認証用のJWTトークンを保存するために使用されるCookie名。"
+ },
+ "cookie_secure": {
+ "label": "Cookie のセキュリティフラグ",
+ "description": "認証Cookieにセキュアフラグを設定します。TLSを使用する場合はtrueにする必要があります。"
+ },
+ "session_length": {
+ "label": "セッションの期間",
+ "description": "JWTベースのセッション継続時間(秒単位)。"
+ },
+ "refresh_time": {
+ "label": "セッション更新ウィンドウ",
+ "description": "セッションの有効期限が切れるまで残り数秒になったら、セッションを元の期間に更新します。"
+ },
+ "failed_login_rate_limit": {
+ "label": "ログイン失敗回数の上限",
+ "description": "ログイン失敗時の試行回数を制限するルールを設けることで、総当たり攻撃を軽減する。"
+ },
+ "trusted_proxies": {
+ "label": "信頼できるプロキシ",
+ "description": "レート制限のためクライアントIPアドレスを特定する際に使用される、信頼できるプロキシIPのリスト。"
+ },
+ "hash_iterations": {
+ "label": "ハッシュ反復処理",
+ "description": "ユーザーパスワードのハッシュ化に使用するPBKDF2-SHA256の反復回数。"
+ },
+ "roles": {
+ "label": "ロールのマッピング",
+ "description": "ロールをカメラリストに割り当てます。リストが空の場合、そのロールのユーザーは全てのカメラにアクセスできます。"
+ },
+ "admin_first_time_login": {
+ "label": "初回管理者フラグ",
+ "description": "この設定が「true」の場合、ログインページにヘルプリンクが表示され、管理者パスワードのリセット後にログインする方法がユーザーに案内されることがあります。 "
}
},
"version": {
- "label": "現在の設定バージョン"
+ "label": "現在の設定バージョン",
+ "description": "移行やフォーマット変更の検知に役立つ、アクティブな設定の数値または文字列バージョン。"
+ },
+ "audio_transcription": {
+ "label": "音声文字起こし",
+ "description": "イベントやリアルタイム字幕に使用される、ライブ音声およびスピーチ音声の文字起こし設定。",
+ "live_enabled": {
+ "label": "ライブ文字起こし",
+ "description": "音声を受信した時点で、リアルタイム文字起こしを有効にします。"
+ },
+ "enabled": {
+ "label": "音声文字起こしを有効にする",
+ "description": "全カメラの自動音声文字起こしを有効または無効にします(カメラ別に上書き可能)。"
+ },
+ "language": {
+ "label": "文字起こし言語",
+ "description": "文字起こし/翻訳に使用する言語コード(例: 英語は 'en')。対応言語コードは https://whisper-api.com/docs/languages/ を参照。"
+ },
+ "device": {
+ "label": "文字起こしデバイス",
+ "description": "文字起こしモデルを実行するデバイスキー(CPU/GPU)。現在 GPU は NVIDIA CUDA のみサポート。"
+ },
+ "model_size": {
+ "label": "モデルサイズ",
+ "description": "オフライン音声イベント文字起こしに使用するモデルサイズ。"
+ }
+ },
+ "birdseye": {
+ "label": "バードアイ",
+ "description": "複数のカメラ映像を1つのレイアウトに合成する「バードアイ」合成ビューの設定。",
+ "enabled": {
+ "label": "バードアイを有効にする",
+ "description": "バードアイビュー機能を有効または無効にします。"
+ },
+ "mode": {
+ "label": "トラッキングモード",
+ "description": "バードアイにカメラを含めるモード:「オブジェクト」「モーション」または「連続」。"
+ },
+ "order": {
+ "label": "位置",
+ "description": "バードアイレイアウトにおけるカメラの並び順を決定する数値。"
+ },
+ "restream": {
+ "label": "RTSP リストリーム",
+ "description": "バードアイ出力を RTSP フィードとしてリストリームします。有効化するとバードアイが常時動作します。"
+ },
+ "width": {
+ "label": "幅",
+ "description": "合成されたバードアイフレームの出力幅(ピクセル)。"
+ },
+ "height": {
+ "label": "高さ",
+ "description": "合成されたバードアイフレームの出力高さ(ピクセル)。"
+ },
+ "quality": {
+ "label": "エンコード品質",
+ "description": "バードアイ mpeg1 フィードのエンコード品質(1 が最高、31 が最低)。"
+ },
+ "inactivity_threshold": {
+ "label": "非アクティブしきい値",
+ "description": "この秒数だけ非アクティブが続くと、カメラはバードアイに表示されなくなります。"
+ },
+ "layout": {
+ "label": "レイアウト",
+ "description": "バードアイ合成のレイアウトオプション。",
+ "scaling_factor": {
+ "label": "スケーリング係数",
+ "description": "レイアウト計算器が使用するスケーリング係数(範囲 1.0 〜 5.0)。"
+ },
+ "max_cameras": {
+ "label": "最大カメラ数",
+ "description": "バードアイに同時表示するカメラの最大数。最新のカメラが表示されます。"
+ }
+ },
+ "idle_heartbeat_fps": {
+ "label": "アイドル時ハートビート FPS",
+ "description": "アイドル時に最後の合成バードアイフレームを再送するフレームレート。0 で無効。"
+ }
+ },
+ "database": {
+ "label": "データベース",
+ "description": "Frigateが追跡対象や録画メタデータを保存するために使用するSQLiteデータベースの設定。",
+ "path": {
+ "label": "データベースパス",
+ "description": "FrigateのSQLiteデータベースファイルが保存されるファイルシステムパス。"
+ }
+ },
+ "detect": {
+ "label": "物体検知",
+ "description": "物体検知の実行やトラッカーの初期化に使用される、検知や検知ロールの設定。",
+ "enabled": {
+ "label": "物体検知を有効にする",
+ "description": "全カメラの物体検知を有効または無効にします(カメラ別に上書き可能)。"
+ },
+ "height": {
+ "label": "高さを検知",
+ "description": "検知ストリームに使用するフレーム高さ(ピクセル)。ネイティブストリーム解像度を使用する場合は、空欄のままにしてください。"
+ },
+ "width": {
+ "label": "幅を検知",
+ "description": "検知ストリームで使用するフレーム幅 (ピクセル)。空欄でストリームのネイティブ解像度を使用。"
+ },
+ "fps": {
+ "label": "検知 FPS",
+ "description": "検知を実行する目標 FPS。低くするほど CPU 使用率が下がります(推奨値は 5、極めて高速な物体を追跡する場合のみ最大 10 まで上げてください)。"
+ },
+ "min_initialized": {
+ "label": "最小初期化フレーム数",
+ "description": "追跡オブジェクトを生成するために必要な連続検知ヒット数。値を大きくすると誤初期化が減ります。デフォルトは fps の半分。"
+ },
+ "max_disappeared": {
+ "label": "最大消失フレーム数",
+ "description": "追跡オブジェクトが消失したと判断するまでの未検知フレーム数。"
+ },
+ "stationary": {
+ "label": "静止オブジェクト設定",
+ "description": "一定時間静止しているオブジェクトを検知・管理するための設定。",
+ "interval": {
+ "label": "静止チェック間隔",
+ "description": "静止オブジェクトを確認するための検知を、何フレームおきに実行するか。"
+ },
+ "threshold": {
+ "label": "静止しきい値",
+ "description": "オブジェクトを静止状態とみなすために必要な位置変化のないフレーム数。"
+ },
+ "max_frames": {
+ "label": "最大追跡フレーム",
+ "description": "静止オブジェクトを破棄するまでの追跡フレーム数の上限。",
+ "default": {
+ "label": "デフォルト最大フレーム",
+ "description": "静止オブジェクトの追跡を停止するまでのデフォルト最大フレーム数。"
+ },
+ "objects": {
+ "label": "オブジェクト別最大フレーム",
+ "description": "静止オブジェクト追跡の最大フレーム数をオブジェクトごとに上書きします。"
+ }
+ },
+ "classifier": {
+ "label": "ビジュアル分類器を有効化",
+ "description": "バウンディングボックスが揺らいでも真に静止しているオブジェクトを検知するため、ビジュアル分類器を使用します。"
+ }
+ },
+ "annotation_offset": {
+ "label": "注釈オフセット",
+ "description": "タイムライン上のバウンディングボックスを録画と揃えるため、検知注釈を時間方向にずらすミリ秒数。正負どちらも指定可能。"
+ }
+ },
+ "go2rtc": {
+ "label": "go2rtc",
+ "description": "ライブストリーム中継および変換に利用される、統合型go2rtcリストリーミングサービスの設定。"
+ },
+ "mqtt": {
+ "label": "MQTT",
+ "description": "テレメトリ、スナップショット、およびイベントの詳細をMQTTブローカーに接続して公開するための設定。",
+ "enabled": {
+ "label": "MQTTを有効にする",
+ "description": "状態、イベント、スナップショットの MQTT 連携を有効または無効にします。"
+ },
+ "host": {
+ "label": "MQTT ホスト",
+ "description": "MQTT ブローカーのホスト名または IP アドレス。"
+ },
+ "port": {
+ "label": "MQTT ポート",
+ "description": "MQTT ブローカーのポート(プレーン MQTT は通常 1883)。"
+ },
+ "topic_prefix": {
+ "label": "トピックプレフィックス",
+ "description": "Frigate の全 MQTT トピックに付与するプレフィックス。複数インスタンスを起動する場合は重複しないようにします。"
+ },
+ "client_id": {
+ "label": "クライアント ID",
+ "description": "MQTT ブローカー接続時に使用するクライアント識別子。インスタンスごとに一意である必要があります。"
+ },
+ "stats_interval": {
+ "label": "統計送信間隔",
+ "description": "システムとカメラの統計を MQTT に発行する間隔(秒)。"
+ },
+ "user": {
+ "label": "MQTT ユーザー名",
+ "description": "任意の MQTT ユーザー名。環境変数やシークレット経由でも指定可能。"
+ },
+ "password": {
+ "label": "MQTT パスワード",
+ "description": "任意の MQTT パスワード。環境変数やシークレット経由でも指定可能。"
+ },
+ "tls_ca_certs": {
+ "label": "TLS CA 証明書",
+ "description": "ブローカーへの TLS 接続用 CA 証明書のパス(自己署名証明書用)。"
+ },
+ "tls_client_cert": {
+ "label": "クライアント証明書",
+ "description": "TLS 相互認証用のクライアント証明書パス。クライアント証明書を使う場合は user/password を設定しないでください。"
+ },
+ "tls_client_key": {
+ "label": "クライアント鍵",
+ "description": "クライアント証明書の秘密鍵パス。"
+ },
+ "tls_insecure": {
+ "label": "TLS 検証を無効化",
+ "description": "ホスト名検証をスキップして安全でない TLS 接続を許可します(非推奨)。"
+ },
+ "qos": {
+ "label": "MQTT QoS",
+ "description": "MQTT パブリッシュ/サブスクライブの QoS レベル(0、1、または 2)。"
+ }
+ },
+ "telemetry": {
+ "label": "テレメトリ",
+ "description": "GPU やネットワーク帯域監視を含む、システムテレメトリと統計のオプション。",
+ "network_interfaces": {
+ "label": "ネットワークインターフェイス",
+ "description": "帯域統計を監視するネットワークインターフェイス名のプレフィックスのリスト。"
+ },
+ "stats": {
+ "label": "システム統計",
+ "description": "各種システム・GPU 統計の収集を有効/無効にするオプション。",
+ "amd_gpu_stats": {
+ "label": "AMD GPU 統計",
+ "description": "AMD GPU が存在する場合、AMD GPU 統計の収集を有効にします。"
+ },
+ "intel_gpu_stats": {
+ "label": "Intel GPU 統計",
+ "description": "Intel GPU が存在する場合、Intel GPU 統計の収集を有効にします。"
+ },
+ "network_bandwidth": {
+ "label": "ネットワーク帯域",
+ "description": "カメラの ffmpeg プロセスと検出器のプロセスごとのネットワーク帯域監視を有効にします(capabilities が必要)。"
+ },
+ "intel_gpu_device": {
+ "label": "Intel GPU デバイス",
+ "description": "複数の Intel GPU が存在する場合、特定のデバイスに統計を絞り込むための PCI バスアドレスまたは DRM デバイスパス(例: /dev/dri/card1)。"
+ }
+ },
+ "version_check": {
+ "label": "バージョンチェック",
+ "description": "新しい Frigate のバージョンが利用可能かを外部にチェックする機能を有効にします。"
+ }
+ },
+ "notifications": {
+ "label": "通知",
+ "description": "全カメラの通知を有効化・制御する設定(カメラ別に上書き可能)。",
+ "enabled": {
+ "label": "通知を有効化",
+ "description": "全カメラの通知を有効または無効にします(カメラ別に上書き可能)。"
+ },
+ "email": {
+ "label": "通知メールアドレス",
+ "description": "プッシュ通知用、または特定の通知プロバイダで必要となるメールアドレス。"
+ },
+ "cooldown": {
+ "label": "クールダウン期間",
+ "description": "受信者への通知連投を避けるための通知間隔(秒)。"
+ },
+ "enabled_in_config": {
+ "label": "元の通知状態",
+ "description": "元の静的設定で通知が有効化されていたかを示します。"
+ }
+ },
+ "networking": {
+ "label": "ネットワーキング",
+ "description": "Frigate エンドポイントの IPv6 有効化など、ネットワーク関連の設定。",
+ "ipv6": {
+ "label": "IPv6 設定",
+ "description": "Frigate ネットワークサービス向けの IPv6 固有設定。",
+ "enabled": {
+ "label": "IPv6 を有効化",
+ "description": "適用可能な Frigate サービス(API および UI)で IPv6 サポートを有効にします。"
+ }
+ },
+ "listen": {
+ "label": "待ち受けポート設定",
+ "description": "内部・外部の待ち受けポートの設定。上級者向け。多くの場合 Docker compose ファイルの ports セクションを変更することを推奨します。",
+ "internal": {
+ "label": "内部ポート",
+ "description": "Frigate の内部待ち受けポート(デフォルト 5000)。"
+ },
+ "external": {
+ "label": "外部ポート",
+ "description": "Frigate の外部待ち受けポート(デフォルト 8971)。"
+ }
+ }
+ },
+ "proxy": {
+ "label": "プロキシ",
+ "description": "認証済みユーザーヘッダーを渡すリバースプロキシの背後で Frigate を運用する際の設定。",
+ "header_map": {
+ "label": "ヘッダーマッピング",
+ "description": "プロキシベース認証のため、受信したプロキシヘッダーを Frigate のユーザー・ロールフィールドにマッピングします。",
+ "user": {
+ "label": "ユーザーヘッダー",
+ "description": "上流プロキシから提供される認証済みユーザー名を含むヘッダー。"
+ },
+ "role": {
+ "label": "ロールヘッダー",
+ "description": "上流プロキシから提供される認証済みユーザーのロールまたはグループを含むヘッダー。"
+ },
+ "role_map": {
+ "label": "ロールマッピング",
+ "description": "上流のグループ値を Frigate のロールにマッピングします(例: admin グループを admin ロールに)。"
+ }
+ },
+ "logout_url": {
+ "label": "ログアウト URL",
+ "description": "プロキシ経由でログアウトする際にユーザーをリダイレクトする URL。"
+ },
+ "auth_secret": {
+ "label": "プロキシシークレット",
+ "description": "信頼できるプロキシかを検証するため、X-Proxy-Secret ヘッダーと照合する任意のシークレット。"
+ },
+ "default_role": {
+ "label": "デフォルトロール",
+ "description": "ロールマッピングが適用されない場合に、プロキシ認証されたユーザーに割り当てられるデフォルトのロール。"
+ },
+ "separator": {
+ "label": "区切り文字",
+ "description": "プロキシヘッダーで複数値を渡す際に分割に使用する文字。"
+ }
+ },
+ "tls": {
+ "label": "TLS",
+ "description": "Frigate の Web エンドポイント(ポート 8971)の TLS 設定。",
+ "enabled": {
+ "label": "TLS を有効化",
+ "description": "設定された TLS ポートで Frigate の Web UI と API の TLS を有効にします。"
+ }
+ },
+ "ui": {
+ "label": "UI",
+ "description": "タイムゾーン、日時のフォーマット、単位など、ユーザーインターフェイスに関する設定。",
+ "timezone": {
+ "label": "タイムゾーン",
+ "description": "UI 全体に表示する任意のタイムゾーン(未設定時はブラウザのローカル時刻)。"
+ },
+ "time_format": {
+ "label": "時刻表記",
+ "description": "UI で使用する時刻表記(browser、12hour、または 24hour)。"
+ },
+ "date_style": {
+ "label": "日付スタイル",
+ "description": "UI で使用する日付スタイル(full、long、medium、short)。"
+ },
+ "time_style": {
+ "label": "時刻スタイル",
+ "description": "UI で使用する時刻スタイル(full、long、medium、short)。"
+ },
+ "unit_system": {
+ "label": "単位系",
+ "description": "UI と MQTT で使用する表示単位系(metric または imperial)。"
+ }
+ },
+ "detectors": {
+ "label": "検出器ハードウェア",
+ "description": "物体検出器(CPU、GPU、ONNX バックエンド)と検出器固有のモデル設定。",
+ "type": {
+ "label": "タイプ"
+ },
+ "model": {
+ "label": "検出器固有モデル設定",
+ "description": "検出器固有のモデル設定オプション(パス、入力サイズなど)。",
+ "path": {
+ "label": "カスタム物体検知モデルパス",
+ "description": "カスタム検知モデルファイルへのパス(または Frigate+ モデルの場合は plus://)。"
+ },
+ "labelmap_path": {
+ "label": "カスタム検出器のラベルマップ",
+ "description": "検出器で数値クラスを文字列ラベルに対応付けるラベルマップファイルのパス。"
+ },
+ "width": {
+ "label": "物体検知モデル入力幅",
+ "description": "モデル入力テンソルの幅(ピクセル)。"
+ },
+ "height": {
+ "label": "物体検知モデル入力高さ",
+ "description": "モデル入力テンソルの高さ(ピクセル)。"
+ },
+ "labelmap": {
+ "label": "ラベルマップカスタマイズ",
+ "description": "標準ラベルマップに統合する上書きや再マッピングのエントリ。"
+ },
+ "attributes_map": {
+ "label": "オブジェクトラベル→属性ラベルのマップ",
+ "description": "メタデータを付与するためのオブジェクトラベル→属性ラベルのマッピング(例: 'car' -> ['license_plate'])。"
+ },
+ "input_tensor": {
+ "label": "モデル入力テンソル形状",
+ "description": "モデルが期待するテンソル形式: 'nhwc' または 'nchw'。"
+ },
+ "input_pixel_format": {
+ "label": "モデル入力ピクセルカラー形式",
+ "description": "モデルが期待するピクセルカラースペース: 'rgb'、'bgr'、または 'yuv'。"
+ },
+ "input_dtype": {
+ "label": "モデル入力データ型",
+ "description": "モデル入力テンソルのデータ型(例: 'float32')。"
+ },
+ "model_type": {
+ "label": "物体検知モデル種別",
+ "description": "一部の検出器が最適化に使用する検知モデルのアーキテクチャ種別(ssd, yolox, yolonas)。"
+ }
+ },
+ "model_path": {
+ "label": "検出器固有モデルパス",
+ "description": "選択した検出器が必要とする検出器モデルバイナリのファイルパス。"
+ },
+ "axengine": {
+ "label": "AXEngine NPU",
+ "description": "AXEngine ランタイム経由でコンパイル済み .axmodel ファイルを実行する AXERA AX650N/AX8850N NPU 検出器。"
+ },
+ "cpu": {
+ "label": "CPU",
+ "description": "ハードウェアアクセラレーションなしにホスト CPU で TensorFlow Lite モデルを実行する CPU TFLite 検出器。非推奨。",
+ "num_threads": {
+ "label": "検知スレッド数",
+ "description": "CPU ベースの推論に使用するスレッド数。"
+ }
+ },
+ "deepstack": {
+ "label": "DeepStack",
+ "description": "リモートの DeepStack HTTP API に画像を送信して推論する DeepStack/CodeProject.AI 検出器。非推奨。",
+ "api_url": {
+ "label": "DeepStack API URL",
+ "description": "DeepStack API の URL。"
+ },
+ "api_timeout": {
+ "label": "DeepStack API タイムアウト(秒)",
+ "description": "DeepStack API リクエストの最大許容時間。"
+ },
+ "api_key": {
+ "label": "DeepStack API キー(必要な場合)",
+ "description": "認証付き DeepStack サービス用の任意の API キー。"
+ }
+ },
+ "degirum": {
+ "label": "DeGirum",
+ "description": "DeGirum クラウドまたはローカル推論サービス経由でモデルを実行する DeGirum 検出器。",
+ "location": {
+ "label": "推論ロケーション",
+ "description": "DeGirum 推論エンジンのロケーション(例: '@cloud'、'127.0.0.1')。"
+ },
+ "zoo": {
+ "label": "モデル Zoo",
+ "description": "DeGirum モデル zoo のパスまたは URL。"
+ },
+ "token": {
+ "label": "DeGirum クラウドトークン",
+ "description": "DeGirum クラウドアクセス用のトークン。"
+ }
+ },
+ "edgetpu": {
+ "label": "EdgeTPU",
+ "description": "EdgeTPU デリゲートを使用して Coral EdgeTPU 用にコンパイルされた TensorFlow Lite モデルを実行する EdgeTPU 検出器。",
+ "device": {
+ "label": "デバイス種別",
+ "description": "EdgeTPU 推論に使用するデバイス(例: 'usb'、'pci')。"
+ }
+ },
+ "hailo8l": {
+ "label": "Hailo-8/Hailo-8L",
+ "description": "HEF モデルと HailoRT SDK を使用して Hailo ハードウェア上で推論する Hailo-8/Hailo-8L 検出器。",
+ "device": {
+ "label": "デバイス種別",
+ "description": "Hailo 推論に使用するデバイス(例: 'PCIe'、'M.2')。"
+ }
+ },
+ "memryx": {
+ "label": "MemryX",
+ "description": "MemryX アクセラレータ上でコンパイル済み DFP モデルを実行する MemryX MX3 検出器。",
+ "device": {
+ "label": "デバイスパス",
+ "description": "MemryX 推論に使用するデバイス(例: 'PCIe')。"
+ }
+ },
+ "onnx": {
+ "label": "ONNX",
+ "description": "ONNX モデルを実行する ONNX 検出器。利用可能な場合は CUDA/ROCm/OpenVINO などのアクセラレーションバックエンドを使用します。",
+ "device": {
+ "label": "デバイス種別",
+ "description": "ONNX 推論に使用するデバイス(例: 'AUTO'、'CPU'、'GPU')。"
+ }
+ },
+ "openvino": {
+ "label": "OpenVINO",
+ "description": "AMD・Intel CPU、Intel GPU、Intel VPU ハードウェア用の OpenVINO 検出器。",
+ "device": {
+ "label": "デバイス種別",
+ "description": "OpenVINO 推論に使用するデバイス(例: 'CPU'、'GPU'、'NPU')。"
+ }
+ },
+ "rknn": {
+ "label": "RKNN",
+ "description": "Rockchip NPU 用の RKNN 検出器。Rockchip ハードウェア上でコンパイル済み RKNN モデルを実行します。",
+ "num_cores": {
+ "label": "使用するNPUコア数。",
+ "description": "使用する NPU コア数(0 で自動)。"
+ }
+ },
+ "synaptics": {
+ "label": "Synaptics",
+ "description": "Synaptics ハードウェア上で Synap SDK を使い .synap 形式のモデルを実行する Synaptics NPU 検出器。"
+ },
+ "teflon_tfl": {
+ "label": "Teflon",
+ "description": "対応 GPU での推論を高速化するため Mesa Teflon delegate ライブラリを使用する TFLite 用の Teflon delegate 検出器。"
+ },
+ "tensorrt": {
+ "label": "TensorRT",
+ "description": "シリアライズ済み TensorRT エンジンを使用して Nvidia Jetson デバイス上で推論を高速化する TensorRT 検出器。",
+ "device": {
+ "label": "GPU デバイスインデックス",
+ "description": "使用する GPU デバイスインデックス。"
+ }
+ },
+ "zmq": {
+ "label": "ZMQ IPC",
+ "description": "ZeroMQ IPC エンドポイント経由で外部プロセスに推論をオフロードする ZMQ IPC 検出器。",
+ "endpoint": {
+ "label": "ZMQ IPC エンドポイント",
+ "description": "接続する ZMQ エンドポイント。"
+ },
+ "request_timeout_ms": {
+ "label": "ZMQ リクエストタイムアウト(ミリ秒)",
+ "description": "ZMQ リクエストのタイムアウト(ミリ秒)。"
+ },
+ "linger_ms": {
+ "label": "ZMQ ソケット linger(ミリ秒)",
+ "description": "ソケットの linger 期間(ミリ秒)。"
+ }
+ }
+ },
+ "model": {
+ "label": "検知モデル",
+ "description": "カスタム物体検知モデルとその入力形状を設定します。",
+ "path": {
+ "label": "カスタム物体検知モデルパス",
+ "description": "カスタム検知モデルファイルへのパス(または Frigate+ モデルの場合は plus://)。"
+ },
+ "labelmap_path": {
+ "label": "カスタム検出器のラベルマップ",
+ "description": "検出器で数値クラスを文字列ラベルに対応付けるラベルマップファイルのパス。"
+ },
+ "width": {
+ "label": "物体検知モデル入力幅",
+ "description": "モデル入力テンソルの幅(ピクセル)。"
+ },
+ "height": {
+ "label": "物体検知モデル入力高さ",
+ "description": "モデル入力テンソルの高さ(ピクセル)。"
+ },
+ "labelmap": {
+ "label": "ラベルマップカスタマイズ",
+ "description": "標準ラベルマップに統合する上書きや再マッピングのエントリ。"
+ },
+ "attributes_map": {
+ "label": "オブジェクトラベル→属性ラベルのマップ",
+ "description": "メタデータを付与するためのオブジェクトラベル→属性ラベルのマッピング(例: 'car' -> ['license_plate'])。"
+ },
+ "input_tensor": {
+ "label": "モデル入力テンソル形状",
+ "description": "モデルが期待するテンソル形式: 'nhwc' または 'nchw'。"
+ },
+ "input_pixel_format": {
+ "label": "モデル入力ピクセルカラー形式",
+ "description": "モデルが期待するピクセルカラースペース: 'rgb'、'bgr'、または 'yuv'。"
+ },
+ "input_dtype": {
+ "label": "モデル入力データ型",
+ "description": "モデル入力テンソルのデータ型(例: 'float32')。"
+ },
+ "model_type": {
+ "label": "物体検知モデル種別",
+ "description": "一部の検出器が最適化に使用する検知モデルのアーキテクチャ種別(ssd, yolox, yolonas)。"
+ }
+ },
+ "genai": {
+ "label": "生成AI設定",
+ "description": "オブジェクトの説明やレビューサマリーを生成するために統合された生成 AI プロバイダの設定。",
+ "api_key": {
+ "label": "API キー",
+ "description": "一部のプロバイダで必要となる API キー(環境変数経由でも設定可能)。"
+ },
+ "base_url": {
+ "label": "ベース URL",
+ "description": "セルフホスト型または互換プロバイダ向けのベース URL(例: Ollama インスタンス)。"
+ },
+ "model": {
+ "label": "モデル",
+ "description": "説明やサマリーの生成にプロバイダから使用するモデル。"
+ },
+ "provider": {
+ "label": "プロバイダ",
+ "description": "使用する生成 AI プロバイダ(例: ollama、gemini、openai)。"
+ },
+ "roles": {
+ "label": "ロール",
+ "description": "GenAI のロール(chat、descriptions、embeddings)。1 ロールにつき 1 プロバイダ。"
+ },
+ "provider_options": {
+ "label": "プロバイダオプション",
+ "description": "GenAI クライアントに渡すプロバイダ固有の追加オプション。"
+ },
+ "runtime_options": {
+ "label": "ランタイムオプション",
+ "description": "推論呼び出しごとにプロバイダに渡されるランタイムオプション。"
+ }
+ },
+ "ffmpeg": {
+ "label": "FFmpeg",
+ "description": "FFmpeg の設定。バイナリパス、引数、ハードウェアアクセラレーション、ロール別の出力引数を含みます。",
+ "path": {
+ "label": "FFmpeg パス",
+ "description": "使用する FFmpeg バイナリのパス、またはバージョンエイリアス(「5.0」または「7.0」)。"
+ },
+ "global_args": {
+ "label": "FFmpeg グローバル引数",
+ "description": "FFmpeg プロセスに渡されるグローバル引数。"
+ },
+ "hwaccel_args": {
+ "label": "ハードウェアアクセラレーション引数",
+ "description": "FFmpeg のハードウェアアクセラレーション引数。プロバイダ固有のプリセットの使用を推奨。"
+ },
+ "input_args": {
+ "label": "入力引数",
+ "description": "FFmpeg の入力ストリームに適用される引数。"
+ },
+ "output_args": {
+ "label": "出力引数",
+ "description": "detect や record など、FFmpeg のロール別に使用されるデフォルト出力引数。",
+ "detect": {
+ "label": "検知ロールの出力引数",
+ "description": "detect ロールのストリームに使用されるデフォルト出力引数。"
+ },
+ "record": {
+ "label": "録画ロールの出力引数",
+ "description": "record ロールのストリームに使用されるデフォルト出力引数。"
+ }
+ },
+ "retry_interval": {
+ "label": "FFmpeg 再試行間隔",
+ "description": "カメラストリームの失敗後、再接続を試みるまでの待機秒数。デフォルトは 10 秒。"
+ },
+ "apple_compatibility": {
+ "label": "Apple 互換性",
+ "description": "H.265 録画時に Apple プレーヤーとの互換性向上のため HEVC タグ付けを有効化します。"
+ },
+ "gpu": {
+ "label": "GPU インデックス",
+ "description": "ハードウェアアクセラレーションで使用するデフォルト GPU インデックス。"
+ },
+ "inputs": {
+ "label": "カメラ入力",
+ "description": "このカメラの入力ストリーム定義(パスとロール)のリスト。",
+ "path": {
+ "label": "入力パス",
+ "description": "カメラ入力ストリームの URL またはパス。"
+ },
+ "roles": {
+ "label": "入力ロール",
+ "description": "この入力ストリームのロール。"
+ },
+ "global_args": {
+ "label": "FFmpeg グローバル引数",
+ "description": "この入力ストリームに対する FFmpeg グローバル引数。"
+ },
+ "hwaccel_args": {
+ "label": "ハードウェアアクセラレーション引数",
+ "description": "この入力ストリームのハードウェアアクセラレーション引数。"
+ },
+ "input_args": {
+ "label": "入力引数",
+ "description": "このストリーム固有の入力引数。"
+ }
+ }
+ },
+ "live": {
+ "label": "ライブ再生",
+ "description": "jsmpeg ライブストリームの解像度と品質を制御する設定。go2rtc を使用してリストリームしているカメラのライブビューには影響しません。",
+ "streams": {
+ "label": "ライブストリーム名",
+ "description": "設定済みのストリーム名と、ライブ再生で使用する restream/go2rtc 名のマッピング。"
+ },
+ "height": {
+ "label": "ライブの高さ",
+ "description": "Web UI で jsmpeg ライブストリームを描画する高さ (ピクセル)。検知ストリーム高さ以下である必要があります。"
+ },
+ "quality": {
+ "label": "ライブ品質",
+ "description": "jsmpeg ストリームのエンコード品質 (1 が最高、31 が最低)。"
+ }
+ },
+ "motion": {
+ "label": "モーション検知",
+ "description": "全カメラに適用されるモーション検知のデフォルト設定(カメラ別に上書き可能)。",
+ "enabled": {
+ "label": "モーション検知を有効化",
+ "description": "全カメラのモーション検知を有効または無効にします(カメラ別に上書き可能)。"
+ },
+ "threshold": {
+ "label": "モーションしきい値",
+ "description": "モーション検出器が使用するピクセル差分しきい値。値を大きくすると感度が下がります (範囲 1-255)。"
+ },
+ "lightning_threshold": {
+ "label": "雷検知しきい値",
+ "description": "短時間の照明スパイクを検知して無視するしきい値(値が小さいほど感度が高く、0.3 〜 1.0 が目安)。これはモーション検知を完全に止めるものではなく、しきい値超過後に検出器が追加フレームの解析を停止するだけです。モーションベースの録画はこれらのイベント中も作成されます。"
+ },
+ "skip_motion_threshold": {
+ "label": "モーションスキップしきい値",
+ "description": "0.0 〜 1.0 の値を指定し、1 フレームでそれ以上の割合が変化した場合、検出器はモーションボックスを返さず即座に再キャリブレーションします。雷や嵐などの誤検知を減らし CPU を節約できますが、PTZ カメラのオート追跡などの実イベントを取りこぼす可能性があります。数 MB の録画を捨てるか、数本の短いクリップを確認するかのトレードオフです。無効化するには未設定 (None) のままにします。"
+ },
+ "improve_contrast": {
+ "label": "コントラスト強調",
+ "description": "モーション解析前にフレームのコントラストを強調して検知を補助します。"
+ },
+ "contour_area": {
+ "label": "輪郭面積",
+ "description": "モーション輪郭としてカウントするために必要な最小ピクセル数。"
+ },
+ "delta_alpha": {
+ "label": "デルタアルファ",
+ "description": "モーション計算のフレーム差分で使用されるアルファブレンディング係数。"
+ },
+ "frame_alpha": {
+ "label": "フレームアルファ",
+ "description": "モーション前処理でフレームをブレンドする際に使用するアルファ値。"
+ },
+ "frame_height": {
+ "label": "フレーム高さ",
+ "description": "モーション計算時にフレームをスケーリングする高さ (ピクセル)。"
+ },
+ "mask": {
+ "label": "マスク座標",
+ "description": "領域を含める/除外するモーションマスクポリゴンを定義する x,y 座標の順序付きリスト。"
+ },
+ "mqtt_off_delay": {
+ "label": "MQTT オフ遅延",
+ "description": "最後のモーション検知後、MQTT で「off」状態を発行するまでの待機秒数。"
+ },
+ "enabled_in_config": {
+ "label": "元のモーション状態",
+ "description": "元の静的設定でモーション検知が有効化されていたかを示します。"
+ },
+ "raw_mask": {
+ "label": "Raw マスク"
+ }
+ },
+ "objects": {
+ "label": "オブジェクト",
+ "description": "追跡するラベルとオブジェクト別フィルタを含む、オブジェクト追跡のデフォルト設定。",
+ "track": {
+ "label": "追跡するオブジェクト",
+ "description": "全カメラで追跡するオブジェクトラベルのリスト(カメラ別に上書き可能)。"
+ },
+ "filters": {
+ "label": "オブジェクトフィルタ",
+ "description": "誤検知を減らすために検知オブジェクトに適用するフィルタ(面積、比率、信頼度)。",
+ "min_area": {
+ "label": "最小オブジェクト面積",
+ "description": "このオブジェクト種別に必要なバウンディングボックスの最小面積(ピクセルまたは割合)。ピクセル (int) または割合 (0.000001 〜 0.99 の float) を指定可能。"
+ },
+ "max_area": {
+ "label": "最大オブジェクト面積",
+ "description": "このオブジェクト種別に許容されるバウンディングボックスの最大面積(ピクセルまたは割合)。ピクセル (int) または割合 (0.000001 〜 0.99 の float) を指定可能。"
+ },
+ "min_ratio": {
+ "label": "最小アスペクト比",
+ "description": "対象となるバウンディングボックスに必要な最小の幅/高さ比。"
+ },
+ "max_ratio": {
+ "label": "最大アスペクト比",
+ "description": "対象となるバウンディングボックスに許容される最大の幅/高さ比。"
+ },
+ "threshold": {
+ "label": "信頼度しきい値",
+ "description": "オブジェクトを真陽性とみなすために必要な平均検知信頼度。"
+ },
+ "min_score": {
+ "label": "最低信頼度",
+ "description": "オブジェクトをカウントするために必要な単一フレームでの最低検知信頼度。"
+ },
+ "mask": {
+ "label": "フィルタマスク",
+ "description": "フレーム内でこのフィルタが適用される範囲を定義するポリゴン座標。"
+ },
+ "raw_mask": {
+ "label": "Raw マスク"
+ }
+ },
+ "mask": {
+ "label": "オブジェクトマスク",
+ "description": "指定領域でオブジェクト検知を行わないようにするためのマスクポリゴン。"
+ },
+ "raw_mask": {
+ "label": "Raw マスク"
+ },
+ "genai": {
+ "label": "GenAI オブジェクト設定",
+ "description": "追跡オブジェクトの説明生成や、生成 AI へのフレーム送信に関する GenAI オプション。",
+ "enabled": {
+ "label": "GenAI を有効化",
+ "description": "追跡オブジェクトの説明を GenAI で生成する機能を既定で有効にします。"
+ },
+ "use_snapshot": {
+ "label": "スナップショットを使用",
+ "description": "GenAI 説明生成にサムネイルではなくオブジェクトスナップショットを使用します。"
+ },
+ "prompt": {
+ "label": "キャプションプロンプト",
+ "description": "GenAI で説明を生成する際に使用するデフォルトのプロンプトテンプレート。"
+ },
+ "object_prompts": {
+ "label": "オブジェクト別プロンプト",
+ "description": "特定のラベルに対する GenAI 出力をカスタマイズするためのオブジェクト別プロンプト。"
+ },
+ "objects": {
+ "label": "GenAI 対象オブジェクト",
+ "description": "GenAI に既定で送信するオブジェクトラベルのリスト。"
+ },
+ "required_zones": {
+ "label": "必須ゾーン",
+ "description": "GenAI 説明生成の対象となるためにオブジェクトが進入する必要があるゾーン。"
+ },
+ "debug_save_thumbnails": {
+ "label": "サムネイルを保存",
+ "description": "デバッグや確認のため、GenAI に送信したサムネイルを保存します。"
+ },
+ "send_triggers": {
+ "label": "GenAI 送信トリガー",
+ "description": "フレームを GenAI に送るタイミング(終了時、更新後など)を定義します。",
+ "tracked_object_end": {
+ "label": "終了時に送信",
+ "description": "追跡オブジェクトが終了した時点で GenAI にリクエストを送信します。"
+ },
+ "after_significant_updates": {
+ "label": "早期 GenAI トリガー",
+ "description": "追跡オブジェクトに対して指定回数の重要な更新があった後、GenAI にリクエストを送信します。"
+ }
+ },
+ "enabled_in_config": {
+ "label": "元の GenAI 状態",
+ "description": "元の静的設定で GenAI が有効化されていたかを示します。"
+ }
+ },
+ "filters_attribute": {
+ "label": "属性フィルタ",
+ "description": "誤検知を減らすために検知された属性に適用するフィルタ(面積、比率、信頼度)。",
+ "min_area": {
+ "label": "属性の最小面積",
+ "description": "この属性に必要なバウンディングボックスの最小面積(ピクセルまたは割合)。ピクセル (int) または割合 (0.000001 〜 0.99 の float) を指定可能。"
+ },
+ "max_area": {
+ "label": "属性の最大面積",
+ "description": "この属性に許容されるバウンディングボックスの最大面積(ピクセルまたは割合)。ピクセル (int) または割合 (0.000001 〜 0.99 の float) を指定可能。"
+ },
+ "min_ratio": {
+ "label": "最小アスペクト比",
+ "description": "対象となるバウンディングボックスに必要な最小の幅/高さ比。"
+ },
+ "max_ratio": {
+ "label": "最大アスペクト比",
+ "description": "対象となるバウンディングボックスに許容される最大の幅/高さ比。"
+ },
+ "threshold": {
+ "label": "信頼度しきい値",
+ "description": "属性を真陽性とみなすために必要な平均検知信頼度。"
+ },
+ "min_score": {
+ "label": "最低信頼度",
+ "description": "この属性を親オブジェクトに関連付けるために必要な単一フレームでの最低検知信頼度。"
+ },
+ "mask": {
+ "label": "フィルタマスク",
+ "description": "フレーム内でこのフィルタが適用される範囲を定義するポリゴン座標。"
+ },
+ "raw_mask": {
+ "label": "Raw マスク"
+ }
+ }
+ },
+ "record": {
+ "label": "録画",
+ "description": "カメラ別の上書きがない限り全カメラに適用される、録画と保持の設定。",
+ "enabled": {
+ "label": "録画を有効化",
+ "description": "全カメラの録画を有効または無効にします(カメラ別に上書き可能)。"
+ },
+ "expire_interval": {
+ "label": "録画クリーンアップ間隔",
+ "description": "期限切れ録画セグメントを削除するクリーンアップを実行する間隔 (分)。"
+ },
+ "continuous": {
+ "label": "常時保持",
+ "description": "追跡オブジェクトやモーションに関係なく録画を保持する日数。アラートと検知の録画のみを保持したい場合は 0 を指定します。",
+ "days": {
+ "label": "保持日数",
+ "description": "録画を保持する日数。"
+ }
+ },
+ "motion": {
+ "label": "モーション録画保持",
+ "description": "追跡オブジェクトに関係なくモーションでトリガーされた録画を保持する日数。アラートと検知の録画のみを保持したい場合は 0 を指定します。",
+ "days": {
+ "label": "保持日数",
+ "description": "録画を保持する日数。"
+ }
+ },
+ "detections": {
+ "label": "検知録画保持",
+ "description": "検知イベントの録画保持設定。前後の撮影時間を含みます。",
+ "pre_capture": {
+ "label": "イベント前秒数",
+ "description": "検知イベントの前に録画に含める秒数。"
+ },
+ "post_capture": {
+ "label": "イベント後秒数",
+ "description": "検知イベントの後に録画に含める秒数。"
+ },
+ "retain": {
+ "label": "イベント保持",
+ "description": "検知イベントの録画保持設定。",
+ "days": {
+ "label": "保持日数",
+ "description": "検知イベント録画を保持する日数。"
+ },
+ "mode": {
+ "label": "保持モード",
+ "description": "保持モード: all(全セグメントを保存)、motion(モーションがあるセグメントを保存)、active_objects(アクティブなオブジェクトを含むセグメントを保存)。"
+ }
+ }
+ },
+ "alerts": {
+ "label": "アラート録画保持",
+ "description": "アラートイベントの録画保持設定。前後の撮影時間を含みます。",
+ "pre_capture": {
+ "label": "イベント前秒数",
+ "description": "アラートイベントの前に録画に含める秒数。"
+ },
+ "post_capture": {
+ "label": "イベント後秒数",
+ "description": "アラートイベントの後に録画に含める秒数。"
+ },
+ "retain": {
+ "label": "イベント保持",
+ "description": "アラートイベントの録画保持設定。",
+ "days": {
+ "label": "保持日数",
+ "description": "アラートイベント録画を保持する日数。"
+ },
+ "mode": {
+ "label": "保持モード",
+ "description": "保持モード: all(全セグメントを保存)、motion(モーションがあるセグメントを保存)、active_objects(アクティブなオブジェクトを含むセグメントを保存)。"
+ }
+ }
+ },
+ "export": {
+ "label": "エクスポート設定",
+ "description": "タイムラプスやハードウェアアクセラレーションなど、録画をエクスポートする際に使用する設定。",
+ "hwaccel_args": {
+ "label": "エクスポート用 hwaccel 引数",
+ "description": "エクスポート/トランスコード処理で使用するハードウェアアクセラレーション引数。"
+ },
+ "max_concurrent": {
+ "label": "同時エクスポート数の上限",
+ "description": "同時に処理するエクスポートジョブの最大数。"
+ }
+ },
+ "preview": {
+ "label": "プレビュー設定",
+ "description": "UI に表示される録画プレビューの品質を制御する設定。",
+ "quality": {
+ "label": "プレビュー品質",
+ "description": "プレビュー品質レベル (very_low, low, medium, high, very_high)。"
+ }
+ },
+ "enabled_in_config": {
+ "label": "元の録画状態",
+ "description": "元の静的設定で録画が有効化されていたかを示します。"
+ }
+ },
+ "review": {
+ "label": "レビュー",
+ "description": "UI 表示と保存で使用するアラート、検知、GenAI レビューサマリーを制御する設定。",
+ "alerts": {
+ "label": "アラート設定",
+ "description": "どの追跡オブジェクトがアラートを生成するか、およびアラートの保持方法に関する設定。",
+ "enabled": {
+ "label": "アラートを有効化",
+ "description": "全カメラのアラート生成を有効または無効にします(カメラ別に上書き可能)。"
+ },
+ "labels": {
+ "label": "アラートラベル",
+ "description": "アラート対象となるオブジェクトラベルのリスト(例: car, person)。"
+ },
+ "required_zones": {
+ "label": "必須ゾーン",
+ "description": "アラートとみなされるためにオブジェクトが進入する必要があるゾーン。空欄ですべてのゾーンを許可。"
+ },
+ "enabled_in_config": {
+ "label": "元のアラート状態",
+ "description": "元の静的設定でアラートが有効化されていたかを記録します。"
+ },
+ "cutoff_time": {
+ "label": "アラート打ち切り時間",
+ "description": "アラートを引き起こすアクティビティが途絶えてからアラートを打ち切るまでの待機秒数。"
+ }
+ },
+ "detections": {
+ "label": "検知設定",
+ "description": "どの追跡オブジェクトが(アラートではない)検知を生成するか、および検知の保持方法に関する設定。",
+ "enabled": {
+ "label": "検知を有効化",
+ "description": "全カメラの検知イベントを有効または無効にします(カメラ別に上書き可能)。"
+ },
+ "labels": {
+ "label": "検知ラベル",
+ "description": "検知イベントの対象となるオブジェクトラベルのリスト。"
+ },
+ "required_zones": {
+ "label": "必須ゾーン",
+ "description": "検知とみなされるためにオブジェクトが進入する必要があるゾーン。空欄ですべてのゾーンを許可。"
+ },
+ "cutoff_time": {
+ "label": "検知打ち切り時間",
+ "description": "検知を引き起こすアクティビティが途絶えてから検知を打ち切るまでの待機秒数。"
+ },
+ "enabled_in_config": {
+ "label": "元の検知状態",
+ "description": "元の静的設定で検知が有効化されていたかを記録します。"
+ }
+ },
+ "genai": {
+ "label": "GenAI 設定",
+ "description": "レビュー項目の説明やサマリーを生成 AI で作成する機能の制御。",
+ "enabled": {
+ "label": "GenAI 説明を有効化",
+ "description": "レビュー項目の GenAI 生成説明やサマリーを有効または無効にします。"
+ },
+ "alerts": {
+ "label": "アラートに GenAI を使用",
+ "description": "アラート項目の説明を GenAI で生成します。"
+ },
+ "detections": {
+ "label": "検知に GenAI を使用",
+ "description": "検知項目の説明を GenAI で生成します。"
+ },
+ "image_source": {
+ "label": "レビュー画像ソース",
+ "description": "GenAI に送信する画像のソース(「preview」または「recordings」)。「recordings」は高品質ですがトークン消費が増えます。"
+ },
+ "additional_concerns": {
+ "label": "追加の懸念事項",
+ "description": "このカメラのアクティビティ評価時に GenAI に考慮させる追加の懸念や注意事項のリスト。"
+ },
+ "debug_save_thumbnails": {
+ "label": "サムネイルを保存",
+ "description": "デバッグや確認のため、GenAI プロバイダに送信したサムネイルを保存します。"
+ },
+ "enabled_in_config": {
+ "label": "元の GenAI 状態",
+ "description": "元の静的設定で GenAI レビューが有効化されていたかを記録します。"
+ },
+ "preferred_language": {
+ "label": "希望言語",
+ "description": "GenAI プロバイダに生成応答で要求する言語。"
+ },
+ "activity_context_prompt": {
+ "label": "活動コンテキストプロンプト",
+ "description": "GenAI サマリーの文脈として、何が不審な活動で何がそうでないかを記述するカスタムプロンプト。"
+ }
+ }
+ },
+ "snapshots": {
+ "label": "スナップショット",
+ "description": "全カメラの追跡オブジェクトに対する API 生成スナップショットの設定(カメラ別に上書き可能)。",
+ "enabled": {
+ "label": "スナップショットを有効化",
+ "description": "全カメラのスナップショット保存を有効または無効にします(カメラ別に上書き可能)。"
+ },
+ "timestamp": {
+ "label": "タイムスタンプ重ね合わせ",
+ "description": "API スナップショットにタイムスタンプを重ねて表示します。"
+ },
+ "bounding_box": {
+ "label": "バウンディングボックス重ね合わせ",
+ "description": "API スナップショットに追跡オブジェクトのバウンディングボックスを描画します。"
+ },
+ "crop": {
+ "label": "スナップショットを切り抜き",
+ "description": "API スナップショットを検知オブジェクトのバウンディングボックスで切り抜きます。"
+ },
+ "required_zones": {
+ "label": "必須ゾーン",
+ "description": "スナップショットを保存するためにオブジェクトが進入する必要があるゾーン。"
+ },
+ "height": {
+ "label": "スナップショット高さ",
+ "description": "API スナップショットをリサイズする高さ (ピクセル)。空欄で元のサイズを維持。"
+ },
+ "retain": {
+ "label": "スナップショット保持",
+ "description": "デフォルト保持日数とオブジェクト別上書きを含む、スナップショット保持設定。",
+ "default": {
+ "label": "デフォルト保持期間",
+ "description": "スナップショットを保持するデフォルト日数。"
+ },
+ "mode": {
+ "label": "保持モード",
+ "description": "保持モード: all(全セグメントを保存)、motion(モーションがあるセグメントを保存)、active_objects(アクティブなオブジェクトを含むセグメントを保存)。"
+ },
+ "objects": {
+ "label": "オブジェクト別保持",
+ "description": "オブジェクトごとのスナップショット保持日数の上書き。"
+ }
+ },
+ "quality": {
+ "label": "スナップショット品質",
+ "description": "保存するスナップショットのエンコード品質 (0-100)。"
+ }
+ },
+ "timestamp_style": {
+ "label": "タイムスタンプスタイル",
+ "description": "デバッグビューとスナップショットの映像内に表示されるタイムスタンプのスタイル設定。",
+ "position": {
+ "label": "タイムスタンプ位置",
+ "description": "画像内のタイムスタンプ位置 (tl/tr/bl/br)。"
+ },
+ "format": {
+ "label": "タイムスタンプ書式",
+ "description": "タイムスタンプに使用する日時書式文字列 (Python datetime 書式コード)。"
+ },
+ "color": {
+ "label": "タイムスタンプ色",
+ "description": "タイムスタンプ文字色の RGB 値 (各 0-255)。",
+ "red": {
+ "label": "赤",
+ "description": "タイムスタンプ色の赤成分 (0-255)。"
+ },
+ "green": {
+ "label": "緑",
+ "description": "タイムスタンプ色の緑成分 (0-255)。"
+ },
+ "blue": {
+ "label": "青",
+ "description": "タイムスタンプ色の青成分 (0-255)。"
+ }
+ },
+ "thickness": {
+ "label": "タイムスタンプ太さ",
+ "description": "タイムスタンプ文字の線の太さ。"
+ },
+ "effect": {
+ "label": "タイムスタンプエフェクト",
+ "description": "タイムスタンプ文字の視覚効果 (none, solid, shadow)。"
+ }
+ },
+ "classification": {
+ "label": "オブジェクト分類",
+ "description": "オブジェクトラベルの精緻化や状態分類に使用する分類モデルの設定。",
+ "bird": {
+ "label": "鳥分類設定",
+ "description": "鳥分類モデル固有の設定。",
+ "enabled": {
+ "label": "鳥分類",
+ "description": "鳥分類を有効または無効にします。"
+ },
+ "threshold": {
+ "label": "最低スコア",
+ "description": "鳥分類を受け入れるために必要な最低分類スコア。"
+ }
+ },
+ "custom": {
+ "label": "カスタム分類モデル",
+ "description": "オブジェクトまたは状態検知に使用するカスタム分類モデルの設定。",
+ "enabled": {
+ "label": "モデルを有効化",
+ "description": "カスタム分類モデルを有効または無効にします。"
+ },
+ "name": {
+ "label": "モデル名",
+ "description": "使用するカスタム分類モデルの識別子。"
+ },
+ "threshold": {
+ "label": "スコアしきい値",
+ "description": "分類状態を変更するために使用するスコアしきい値。"
+ },
+ "save_attempts": {
+ "label": "保存試行数",
+ "description": "最近の分類 UI に表示するために保存する分類試行数。"
+ },
+ "object_config": {
+ "objects": {
+ "label": "分類対象オブジェクト",
+ "description": "オブジェクト分類を実行するオブジェクト種別のリスト。"
+ },
+ "classification_type": {
+ "label": "分類種別",
+ "description": "適用する分類種別: 'sub_label'(サブラベルを追加)またはサポートされる他の種別。"
+ }
+ },
+ "state_config": {
+ "cameras": {
+ "label": "分類対象カメラ",
+ "description": "状態分類を実行するためのカメラ別クロップと設定。",
+ "crop": {
+ "label": "分類クロップ",
+ "description": "このカメラで分類を実行する際に使用するクロップ座標。"
+ }
+ },
+ "motion": {
+ "label": "モーション時に実行",
+ "description": "true にすると、指定したクロップ内でモーションが検知されたときに分類を実行します。"
+ },
+ "interval": {
+ "label": "分類間隔",
+ "description": "状態分類の周期的な実行間隔(秒)。"
+ }
+ }
+ }
+ },
+ "semantic_search": {
+ "label": "セマンティック検索",
+ "description": "オブジェクト埋め込みを構築・クエリして類似項目を見つけるセマンティック検索の設定。",
+ "enabled": {
+ "label": "セマンティック検索を有効化",
+ "description": "セマンティック検索機能を有効または無効にします。"
+ },
+ "reindex": {
+ "label": "起動時に再インデックス",
+ "description": "過去の追跡オブジェクトを埋め込みデータベースに完全再インデックスします。"
+ },
+ "model": {
+ "label": "セマンティック検索モデルまたは GenAI プロバイダ名",
+ "description": "セマンティック検索に使用する埋め込みモデル(例: 'jinav1')、または embeddings ロールを持つ GenAI プロバイダ名。"
+ },
+ "model_size": {
+ "label": "モデルサイズ",
+ "description": "モデルサイズを選択。'small' は CPU で動作、'large' は通常 GPU が必要。"
+ },
+ "device": {
+ "label": "デバイス",
+ "description": "特定のデバイスを対象にする上書き設定 (詳細は https://onnxruntime.ai/docs/execution-providers/ を参照)"
+ },
+ "triggers": {
+ "label": "トリガー",
+ "description": "カメラ別のセマンティック検索トリガーの動作と一致条件。",
+ "friendly_name": {
+ "label": "表示名",
+ "description": "このトリガーの UI 表示用の任意の名前。"
+ },
+ "enabled": {
+ "label": "このトリガーを有効化",
+ "description": "このセマンティック検索トリガーを有効または無効にします。"
+ },
+ "type": {
+ "label": "トリガー種別",
+ "description": "トリガー種別: 「thumbnail」(画像に対する一致)または「description」(テキストに対する一致)。"
+ },
+ "data": {
+ "label": "トリガー内容",
+ "description": "追跡オブジェクトと照合するテキストフレーズまたはサムネイル ID。"
+ },
+ "threshold": {
+ "label": "トリガーしきい値",
+ "description": "このトリガーを発火させるために必要な最低類似度スコア (0-1)。"
+ },
+ "actions": {
+ "label": "トリガーアクション",
+ "description": "トリガー一致時に実行するアクションのリスト (notification, sub_label, attribute)。"
+ }
+ }
+ },
+ "face_recognition": {
+ "label": "顔認識",
+ "description": "全カメラの顔検知と顔認識の設定(カメラ別に上書き可能)。",
+ "enabled": {
+ "label": "顔認識を有効化",
+ "description": "全カメラの顔認識を有効または無効にします(カメラ別に上書き可能)。"
+ },
+ "model_size": {
+ "label": "モデルサイズ",
+ "description": "顔埋め込みに使用するモデルサイズ(small/large)。large は GPU が必要な場合があります。"
+ },
+ "unknown_score": {
+ "label": "未知顔スコアしきい値",
+ "description": "この距離しきい値を下回る場合、顔は潜在的な一致とみなされます(値が大きいほど厳格)。"
+ },
+ "detection_threshold": {
+ "label": "検知しきい値",
+ "description": "顔検知を有効とみなすために必要な最低検知信頼度。"
+ },
+ "recognition_threshold": {
+ "label": "認識しきい値",
+ "description": "2 つの顔を一致とみなす顔埋め込みの距離しきい値。"
+ },
+ "min_area": {
+ "label": "顔の最小面積",
+ "description": "認識を試みるために必要な顔ボックスの最小面積 (ピクセル)。"
+ },
+ "min_faces": {
+ "label": "最低顔認識数",
+ "description": "認識されたサブラベルを人物に適用する前に必要な、最低顔認識回数。"
+ },
+ "save_attempts": {
+ "label": "保存試行数",
+ "description": "最近の認識 UI に保持する顔認識試行数。"
+ },
+ "blur_confidence_filter": {
+ "label": "ボケ補正による信頼度調整",
+ "description": "画像のボケに基づいて信頼度スコアを調整し、低品質な顔の誤検知を減らします。"
+ },
+ "device": {
+ "label": "デバイス",
+ "description": "特定のデバイスを対象にする上書き設定 (詳細は https://onnxruntime.ai/docs/execution-providers/ を参照)"
+ }
+ },
+ "lpr": {
+ "label": "ナンバープレート認識",
+ "description": "ナンバープレート認識の設定。検知しきい値、書式整形、既知ナンバーなどを含みます。",
+ "enabled": {
+ "label": "LPR を有効化",
+ "description": "全カメラのナンバープレート認識を有効または無効にします(カメラ別に上書き可能)。"
+ },
+ "model_size": {
+ "label": "モデルサイズ",
+ "description": "テキスト検知/認識に使用するモデルサイズ。多くのユーザーは 'small' を使用してください。"
+ },
+ "detection_threshold": {
+ "label": "検知しきい値",
+ "description": "プレート候補に対する OCR を開始する検知信頼度しきい値。"
+ },
+ "min_area": {
+ "label": "プレート最小面積",
+ "description": "認識を試みるために必要なプレート最小面積 (ピクセル)。"
+ },
+ "recognition_threshold": {
+ "label": "認識しきい値",
+ "description": "認識されたプレート文字列をサブラベルとして付与するために必要な信頼度しきい値。"
+ },
+ "min_plate_length": {
+ "label": "プレート最小文字数",
+ "description": "有効とみなすために認識されたプレートに必要な最小文字数。"
+ },
+ "format": {
+ "label": "プレート書式正規表現",
+ "description": "認識されたプレート文字列を期待する書式と照合する任意の正規表現。"
+ },
+ "match_distance": {
+ "label": "マッチ距離",
+ "description": "検知されたプレートと既知のプレートを比較する際に許容する文字相違数。"
+ },
+ "known_plates": {
+ "label": "既知のプレート",
+ "description": "特別に追跡またはアラートするプレートまたは正規表現のリスト。"
+ },
+ "enhancement": {
+ "label": "強調レベル",
+ "description": "OCR 前にプレート切り出し画像に適用する強調レベル (0-10)。値を大きくしても常に改善するとは限らず、5 を超えると夜間プレートでのみ有効な場合があるため注意が必要です。"
+ },
+ "debug_save_plates": {
+ "label": "デバッグ用プレート保存",
+ "description": "LPR 性能のデバッグ用にプレート切り出し画像を保存します。"
+ },
+ "device": {
+ "label": "デバイス",
+ "description": "特定のデバイスを対象にする上書き設定 (詳細は https://onnxruntime.ai/docs/execution-providers/ を参照)"
+ },
+ "replace_rules": {
+ "label": "置換ルール",
+ "description": "照合前に検知プレート文字列を正規化するための正規表現置換ルール。",
+ "pattern": {
+ "label": "正規表現パターン"
+ },
+ "replacement": {
+ "label": "置換文字列"
+ }
+ },
+ "expire_time": {
+ "label": "失効秒数",
+ "description": "未検知ナンバーをトラッカーから失効させるまでの秒数(専用 LPR カメラのみ)。"
+ }
+ },
+ "camera_groups": {
+ "label": "カメラグループ",
+ "description": "UI でカメラを整理するための名前付きカメラグループの設定。",
+ "cameras": {
+ "label": "カメラリスト",
+ "description": "このグループに含まれるカメラ名の配列。"
+ },
+ "icon": {
+ "label": "グループアイコン",
+ "description": "UI でカメラグループを表すアイコン。"
+ },
+ "order": {
+ "label": "並び順",
+ "description": "UI でカメラグループを並べる数値順。値が大きいほど後ろに表示されます。"
+ }
+ },
+ "profiles": {
+ "label": "プロファイル",
+ "description": "表示名付きの名前付きプロファイル定義。カメラ別プロファイルはここで定義された名前を参照する必要があります。",
+ "friendly_name": {
+ "label": "表示名",
+ "description": "UI に表示されるこのプロファイルの表示名。"
+ }
+ },
+ "active_profile": {
+ "label": "アクティブプロファイル",
+ "description": "現在アクティブなプロファイル名。実行時のみ有効で YAML には保存されません。"
+ },
+ "camera_mqtt": {
+ "label": "MQTT",
+ "description": "MQTT 画像配信の設定。",
+ "enabled": {
+ "label": "画像送信",
+ "description": "このカメラのオブジェクト画像スナップショットを MQTT トピックに配信する機能を有効にします。"
+ },
+ "timestamp": {
+ "label": "タイムスタンプを追加",
+ "description": "MQTT に配信する画像にタイムスタンプを重ねて表示します。"
+ },
+ "bounding_box": {
+ "label": "バウンディングボックスを追加",
+ "description": "MQTT に配信する画像にバウンディングボックスを描画します。"
+ },
+ "crop": {
+ "label": "画像を切り抜き",
+ "description": "MQTT に配信する画像を検知オブジェクトのバウンディングボックスで切り抜きます。"
+ },
+ "height": {
+ "label": "画像の高さ",
+ "description": "MQTT 配信時に画像をリサイズする高さ (ピクセル)。"
+ },
+ "required_zones": {
+ "label": "必須ゾーン",
+ "description": "MQTT 画像を配信するためにオブジェクトが進入する必要があるゾーン。"
+ },
+ "quality": {
+ "label": "JPEG 品質",
+ "description": "MQTT に配信する画像の JPEG 品質 (0-100)。"
+ }
+ },
+ "camera_ui": {
+ "label": "カメラ UI",
+ "description": "UI 内でのこのカメラの表示順と表示設定。順序はデフォルトダッシュボードに影響します。より細かい制御にはカメラグループを使用してください。",
+ "order": {
+ "label": "UI 表示順",
+ "description": "UI 内でのカメラの並び順に使用される数値(デフォルトダッシュボードとリスト)。値が大きいほど後ろに表示されます。"
+ },
+ "dashboard": {
+ "label": "UI に表示",
+ "description": "このカメラを Frigate UI 全体に表示するかを切り替えます。無効化した場合、再表示するには設定ファイルを手動編集する必要があります。"
+ }
+ },
+ "onvif": {
+ "label": "ONVIF",
+ "description": "このカメラの ONVIF 接続および PTZ オート追跡の設定。",
+ "host": {
+ "label": "ONVIF ホスト",
+ "description": "このカメラの ONVIF サービスのホスト(オプションでスキーマも)。"
+ },
+ "port": {
+ "label": "ONVIF ポート",
+ "description": "ONVIF サービスのポート番号。"
+ },
+ "user": {
+ "label": "ONVIF ユーザー名",
+ "description": "ONVIF 認証用のユーザー名。ONVIF に admin ユーザーが必要なデバイスもあります。"
+ },
+ "password": {
+ "label": "ONVIF パスワード",
+ "description": "ONVIF 認証用のパスワード。"
+ },
+ "tls_insecure": {
+ "label": "TLS 検証を無効化",
+ "description": "ONVIF の TLS 検証をスキップし、ダイジェスト認証も無効化します(安全でないため、信頼できるネットワークでのみ使用)。"
+ },
+ "profile": {
+ "label": "ONVIF プロファイル",
+ "description": "PTZ 制御に使用する ONVIF メディアプロファイル(トークンまたは名前で指定)。未設定の場合、有効な PTZ 設定を持つ最初のプロファイルが自動選択されます。"
+ },
+ "autotracking": {
+ "label": "オートトラッキング",
+ "description": "PTZ カメラの動作で移動中のオブジェクトを自動追跡し、フレーム中央に保ちます。",
+ "enabled": {
+ "label": "オートトラッキングを有効化",
+ "description": "検知オブジェクトの PTZ オート追跡を有効または無効にします。"
+ },
+ "calibrate_on_startup": {
+ "label": "起動時にキャリブレーション",
+ "description": "追跡精度を向上させるため、起動時に PTZ モーター速度を測定します。キャリブレーション後に movement_weights が設定に書き込まれます。"
+ },
+ "zooming": {
+ "label": "ズームモード",
+ "description": "ズーム動作の制御: disabled (パン/チルトのみ)、absolute (互換性が最も高い)、relative (パン/チルト/ズーム同時)。"
+ },
+ "zoom_factor": {
+ "label": "ズーム倍率",
+ "description": "追跡対象オブジェクトのズームレベルを制御します。値が小さいほど広い範囲を保ち、大きいほどズームインしますが追跡を失う可能性があります。0.1 〜 0.75 の範囲で指定。"
+ },
+ "track": {
+ "label": "追跡対象オブジェクト",
+ "description": "オートトラッキングを発動させるオブジェクト種別のリスト。"
+ },
+ "required_zones": {
+ "label": "必須ゾーン",
+ "description": "オートトラッキング開始前にオブジェクトが進入する必要があるゾーン。"
+ },
+ "return_preset": {
+ "label": "復帰プリセット",
+ "description": "追跡終了後にカメラが戻る、ファームウェアに設定されている ONVIF プリセット名。"
+ },
+ "timeout": {
+ "label": "復帰タイムアウト",
+ "description": "追跡を失ってからカメラをプリセット位置に戻すまでの待機秒数。"
+ },
+ "movement_weights": {
+ "label": "動作重み",
+ "description": "カメラキャリブレーションによって自動生成される値。手動で変更しないでください。"
+ },
+ "enabled_in_config": {
+ "label": "元のオート追跡状態",
+ "description": "オートトラッキングが設定で有効化されていたかを追跡する内部フィールド。"
+ }
+ },
+ "ignore_time_mismatch": {
+ "label": "時刻差異を無視",
+ "description": "ONVIF 通信時に、カメラと Frigate サーバー間の時刻同期差異を無視します。"
+ }
}
}
diff --git a/web/public/locales/ja/config/groups.json b/web/public/locales/ja/config/groups.json
index 7d00539482..8e829ea167 100644
--- a/web/public/locales/ja/config/groups.json
+++ b/web/public/locales/ja/config/groups.json
@@ -2,7 +2,7 @@
"audio": {
"global": {
"sensitivity": "グローバル感度",
- "detection": "グローバル検出"
+ "detection": "グローバル検知"
},
"cameras": {
"detection": "検知",
@@ -12,12 +12,19 @@
"timestamp_style": {
"cameras": {
"appearance": "外観"
+ },
+ "global": {
+ "appearance": "全体の外観"
}
},
"motion": {
"cameras": {
"sensitivity": "感度",
"algorithm": "アルゴリズム"
+ },
+ "global": {
+ "sensitivity": "グローバル感度",
+ "algorithm": "グローバルアルゴリズム"
}
},
"detect": {
@@ -42,7 +49,25 @@
},
"record": {
"global": {
- "events": "グローバルイベント"
+ "events": "グローバルイベント",
+ "retention": "グローバルリテンション"
+ },
+ "cameras": {
+ "retention": "リテンション",
+ "events": "イベント"
+ }
+ },
+ "snapshots": {
+ "global": {
+ "display": "グローバル表示"
+ },
+ "cameras": {
+ "display": "表示"
+ }
+ },
+ "ffmpeg": {
+ "cameras": {
+ "cameraFfmpeg": "カメラ固有のFFmpeg引数"
}
}
}
diff --git a/web/public/locales/ja/config/validation.json b/web/public/locales/ja/config/validation.json
index 5b67869a7e..9f862a31ed 100644
--- a/web/public/locales/ja/config/validation.json
+++ b/web/public/locales/ja/config/validation.json
@@ -2,5 +2,34 @@
"pattern": "無効なフォーマット",
"required": "この項目は必須です",
"type": "無効な値タイプ",
- "format": "無効なフォーマット"
+ "format": "無効なフォーマット",
+ "minimum": "{{limit}} 以上である必要があります",
+ "maximum": "{{limit}} 以下でなければなりません",
+ "exclusiveMinimum": "{{limit}} より大きい値である必要があります",
+ "exclusiveMaximum": "{{limit}} 未満でなければなりません",
+ "minLength": "{{limit}} 文字以上入力してください",
+ "maxLength": "最大 {{limit}} 文字までです",
+ "minItems": "{{limit}} 個以上のアイテムが必要です",
+ "maxItems": "アイテムは最大 {{limit}} 個までです",
+ "enum": "許可された値のいずれかである必要があります",
+ "const": "値が期待される定数と一致しません",
+ "uniqueItems": "全てのアイテムは一意である必要があります",
+ "additionalProperties": "不明なプロパティは使用できません",
+ "oneOf": "許可されたスキーマのうち、いずれか一つに完全一致する必要があります",
+ "anyOf": "許可されたスキーマのうち、少なくとも1つに一致する必要があります",
+ "proxy": {
+ "header_map": {
+ "roleHeaderRequired": "ロールのマッピングを設定する際は、ロールヘッダーが必要です。"
+ }
+ },
+ "ffmpeg": {
+ "inputs": {
+ "rolesUnique": "各ロールは、1つの入力ストリームにのみ割り当てることができます。",
+ "detectRequired": "少なくとも1つの入力ストリームに「detect」ロールを割り当てる必要があります。",
+ "hwaccelDetectOnly": "ハードウェアアクセラレーション引数を定義できるのは、detect ロールを持つ入力ストリームのみです。"
+ }
+ },
+ "detect": {
+ "dimensionMustBeEven": "偶数を指定してください。"
+ }
}
diff --git a/web/public/locales/ja/objects.json b/web/public/locales/ja/objects.json
index c3e41af3f4..10fa2b8b92 100644
--- a/web/public/locales/ja/objects.json
+++ b/web/public/locales/ja/objects.json
@@ -116,5 +116,14 @@
"nzpost": "NZPost",
"postnord": "PostNord",
"gls": "GLS",
- "dpd": "DPD"
+ "dpd": "DPD",
+ "canada_post": "カナダポスト",
+ "royal_mail": "ロイヤルメール",
+ "school_bus": "スクールバス",
+ "skunk": "スカンク",
+ "kangaroo": "カンガルー",
+ "baby": "赤ちゃん",
+ "baby_stroller": "ベビーカー",
+ "rickshaw": "人力車",
+ "rodent": "齧歯類"
}
diff --git a/web/public/locales/ja/views/chat.json b/web/public/locales/ja/views/chat.json
new file mode 100644
index 0000000000..4215a317c7
--- /dev/null
+++ b/web/public/locales/ja/views/chat.json
@@ -0,0 +1,72 @@
+{
+ "documentTitle": "チャット - Frigate",
+ "title": "Frigate チャット",
+ "subtitle": "カメラ管理と分析のためのAIアシスタント",
+ "placeholder": "何でも聞いてください…",
+ "error": "問題が発生しました。もう一度お試しください。",
+ "processing": "処理中...",
+ "toolsUsed": "使用済み: {{tools}}",
+ "showTools": "ツールを表示 ({{count}})",
+ "hideTools": "ツールを非表示",
+ "call": "呼び出す",
+ "result": "結果",
+ "arguments": "引数:",
+ "response": "回答:",
+ "attachment_chip_label": "{{camera}} の {{label}}",
+ "attachment_chip_remove": "添付ファイルを削除",
+ "open_in_explore": "エクスプローラーで開く",
+ "attach_event_aria": "イベント {{eventId}} を添付する",
+ "attachment_picker_paste_label": "またはイベントIDを貼り付け",
+ "attachment_picker_attach": "添付",
+ "attachment_picker_placeholder": "イベントを添付",
+ "quick_reply_find_similar": "類似の目撃情報を探す",
+ "quick_reply_tell_me_more": "これについてもっと詳しく教えてください",
+ "quick_reply_when_else": "他にいつ目撃されたか?",
+ "quick_reply_find_similar_text": "これと似た目撃情報を探す。",
+ "quick_reply_tell_me_more_text": "これについてもっと詳しく教えてください。",
+ "quick_reply_when_else_text": "これと同じようなことが他にありましたか?",
+ "anchor": "参照",
+ "similarity_score": "類似性",
+ "no_similar_objects_found": "類似するオブジェクトは見つかりませんでした。",
+ "semantic_search_required": "類似オブジェクトを検索するには、セマンティック検索を有効にする必要があります。",
+ "send": "送信",
+ "suggested_requests": "質問してみてください:",
+ "starting_requests": {
+ "show_recent_events": "最近のイベントを表示",
+ "show_camera_status": "カメラの状態を表示",
+ "recap": "留守中に何が起きた?",
+ "watch_camera": "カメラの動きを監視"
+ },
+ "starting_requests_prompts": {
+ "show_recent_events": "直近1時間のイベントを見せて",
+ "show_camera_status": "現在のカメラの状態はどうなっていますか?",
+ "recap": "留守中に何が起きた?",
+ "watch_camera": "玄関のカメラを監視して、誰か来たら教えて"
+ },
+ "new_chat": "新しいチャット",
+ "settings": {
+ "title": "チャット設定",
+ "show_stats": {
+ "title": "統計を表示",
+ "desc": "チャット応答の生成速度とコンテキストサイズを表示します。",
+ "while_generating": "生成中のみ",
+ "always": "常に表示"
+ },
+ "auto_scroll": {
+ "title": "自動スクロール",
+ "desc": "新しいメッセージが届いたら自動でスクロールします。"
+ }
+ },
+ "stats": {
+ "context": "{{tokens}} トークン",
+ "tokens_per_second": "{{rate}} t/s"
+ },
+ "reasoning": {
+ "active": "推論中…",
+ "show": "推論を表示",
+ "hide": "推論を非表示"
+ },
+ "thinking": {
+ "toggle": "思考の表示を切替"
+ }
+}
diff --git a/web/public/locales/ja/views/classificationModel.json b/web/public/locales/ja/views/classificationModel.json
index 1801353900..91ff04c3bb 100644
--- a/web/public/locales/ja/views/classificationModel.json
+++ b/web/public/locales/ja/views/classificationModel.json
@@ -12,14 +12,15 @@
},
"toast": {
"success": {
- "deletedImage_other": "削除された画像",
+ "deletedImage_other": "{{count}} 枚の画像を削除しました",
"categorizedImage": "画像の分類に成功しました",
"trainedModel": "モデルを正常に学習させました。",
"trainingModel": "モデルのトレーニングを正常に開始しました。",
- "deletedCategory_other": "クラスを削除しました",
+ "deletedCategory_other": "{{count}} 件のクラスを削除しました",
"deletedModel_other": "{{count}} 件のモデルを削除しました",
"updatedModel": "モデル設定を更新しました",
- "renamedCategory": "クラス名を {{name}} に変更しました"
+ "renamedCategory": "クラス名を {{name}} に変更しました",
+ "reclassifiedImage": "画像の再分類に成功しました"
},
"error": {
"deleteImageFailed": "削除に失敗しました: {{errorMessage}}",
@@ -29,19 +30,20 @@
"trainingFailed": "モデルの学習に失敗しました。Frigate のログを確認してください。",
"trainingFailedToStart": "モデルの学習を開始できませんでした: {{errorMessage}}",
"updateModelFailed": "モデルの更新に失敗しました: {{errorMessage}}",
- "renameCategoryFailed": "クラス名の変更に失敗しました: {{errorMessage}}"
+ "renameCategoryFailed": "クラス名の変更に失敗しました: {{errorMessage}}",
+ "reclassifyFailed": "画像の再分類に失敗しました:{{errorMessage}}"
}
},
"train": {
- "titleShort": "Classifications,最近の分類結果を選択,,False,train.aria,,",
+ "titleShort": "最近の分類",
"title": "最近の分類結果",
"aria": "最近の分類結果を選択"
},
"wizard": {
"step1": {
- "typeObject": "Classification",
- "typeState": "Classification",
- "description": "状態モデルは固定カメラ領域の状態変化(例:ドアの開閉)を監視し、オブジェクトモデルは検出されたオブジェクトに分類(例:既知の動物や配達員など)を追加します。",
+ "typeObject": "オブジェクト",
+ "typeState": "状態",
+ "description": "状態モデルは固定カメラ領域の状態変化(例:ドアの開閉)を監視し、オブジェクトモデルは検知されたオブジェクトに分類(例:既知の動物や配達員など)を追加します。",
"name": "名前",
"namePlaceholder": "モデル名を入力...",
"type": "タイプ",
@@ -56,7 +58,7 @@
"states": "状態",
"classesTip": "クラスについて",
"classesStateDesc": "カメラ領域の状態を定義します。例: ガレージドアの「開」「閉」。",
- "classesObjectDesc": "検出されたオブジェクトを分類するための、異なるカテゴリを定義します。例:人物の分類として「delivery_person」「resident」「stranger」など。",
+ "classesObjectDesc": "検知されたオブジェクトを分類するための、異なるカテゴリを定義します。例:人物の分類として「delivery_person」「resident」「stranger」など。",
"classPlaceholder": "クラス名を入力...",
"errors": {
"nameRequired": "モデル名は必須です",
@@ -111,11 +113,16 @@
"missingStatesWarning": {
"title": "状態の例が不足しています",
"description": "最良の結果を得るため、すべての状態の例を選択することを推奨します。すべてを選択しなくても続行できますが、全状態に画像が揃うまでモデルは学習されません。続行後、「最近の分類」から不足分を分類し、学習を行ってください。"
+ },
+ "refreshExamples": "新しい例を生成",
+ "refreshConfirm": {
+ "title": "新しい例を生成しますか?",
+ "description": "この操作により、新しい画像セットが生成され、選択済みのものも含めすべての選択がクリアされます。すべてのクラスについて、再度例を選び直す必要があります。"
}
}
},
"details": {
- "scoreInfo": "このスコアは、このオブジェクトに対するすべての検出結果の分類信頼度の平均を表します。",
+ "scoreInfo": "このスコアは、このオブジェクトに対するすべての検知結果の分類信頼度の平均を表します。",
"none": "なし",
"unknown": "不明"
},
@@ -170,7 +177,7 @@
"noModels": {
"object": {
"title": "オブジェクト分類モデルがありません",
- "description": "検出されたオブジェクトを分類するためのカスタムモデルを作成します。",
+ "description": "検知されたオブジェクトを分類するためのカスタムモデルを作成します。",
"buttonText": "オブジェクトモデルを作成"
},
"state": {
@@ -178,5 +185,7 @@
"description": "特定のカメラ領域の状態変化を監視・分類するためのカスタムモデルを作成します。",
"buttonText": "状態モデルを作成"
}
- }
+ },
+ "reclassifyImageAs": "画像を次として再分類:",
+ "reclassifyImage": "画像を再分類"
}
diff --git a/web/public/locales/ja/views/events.json b/web/public/locales/ja/views/events.json
index 544412974f..13dc92bdf3 100644
--- a/web/public/locales/ja/views/events.json
+++ b/web/public/locales/ja/views/events.json
@@ -1,12 +1,12 @@
{
- "detections": "検出",
+ "detections": "検知",
"motion": {
"label": "モーション",
"only": "モーションのみ"
},
"alerts": "アラート",
"empty": {
- "detection": "レビューする検出はありません",
+ "detection": "レビューする検知はありません",
"alert": "レビューするアラートはありません",
"motion": "モーションデータは見つかりません",
"recordingsDisabled": {
@@ -16,7 +16,9 @@
},
"camera": "カメラ",
"allCameras": "全カメラ",
- "timeline": "タイムライン",
+ "timeline": {
+ "label": "タイムライン"
+ },
"timeline.aria": "タイムラインを選択",
"events": {
"label": "イベント",
@@ -25,7 +27,9 @@
},
"documentTitle": "レビュー - Frigate",
"recordings": {
- "documentTitle": "録画 - Frigate"
+ "documentTitle": "録画 - Frigate",
+ "invalidSharedLink": "解析エラーのため、タイムスタンプ付きの録画リンクを開くことができません。",
+ "invalidSharedCamera": "不明または未承認のカメラのため、タイムスタンプ付き録画のリンクを開くことができません。"
},
"calendarFilter": {
"last24Hours": "直近24時間"
@@ -36,9 +40,9 @@
"label": "新しいレビュー項目を表示",
"button": "レビューすべき新規項目"
},
- "selected_one": "{{count}} 件選択",
- "selected_other": "{{count}} 件選択",
- "detected": "検出",
+ "selected_one": "{{count}} 選択済み",
+ "selected_other": "{{count}} 選択済み",
+ "detected": "検知",
"suspiciousActivity": "不審なアクティビティ",
"threateningActivity": "脅威となるアクティビティ",
"zoomIn": "ズームイン",
@@ -63,5 +67,28 @@
"select_all": "すべて",
"normalActivity": "通常",
"needsReview": "要確認",
- "securityConcern": "セキュリティ上の懸念"
+ "securityConcern": "セキュリティ上の懸念",
+ "motionSearch": {
+ "menuItem": "モーション検索",
+ "openMenu": "カメラオプション"
+ },
+ "motionPreviews": {
+ "menuItem": "モーションプレビューを表示",
+ "title": "モーションプレビュー: {{camera}}",
+ "mobileSettingsTitle": "モーションプレビュー設定",
+ "mobileSettingsDesc": "再生速度と暗転具合を調整し、確認したい日付を選んでモーションのみのクリップを再生します。",
+ "dim": "暗転",
+ "dimAria": "暗転の強さを調整",
+ "dimDesc": "暗転を強くするとモーション領域が見やすくなります。",
+ "speed": "速度",
+ "speedAria": "プレビューの再生速度を選択",
+ "speedDesc": "プレビュークリップの再生速度を選びます。",
+ "back": "戻る",
+ "empty": "利用可能なプレビューがありません",
+ "noPreview": "プレビューを利用できません",
+ "seekAria": "{{camera}} のプレーヤーを {{time}} までシーク",
+ "filter": "フィルター",
+ "filterDesc": "領域を選択すると、その領域でモーションが発生したクリップのみが表示されます。",
+ "filterClear": "クリア"
+ }
}
diff --git a/web/public/locales/ja/views/explore.json b/web/public/locales/ja/views/explore.json
index 35265cc506..6b9ab4c46e 100644
--- a/web/public/locales/ja/views/explore.json
+++ b/web/public/locales/ja/views/explore.json
@@ -11,7 +11,7 @@
"viewInExplore": "探索で表示"
},
"tips": {
- "mismatch_other": "利用不可のオブジェクトが {{count}} 件、このレビュー項目に含まれています。これらはアラートまたは検出の条件を満たしていないか、既にクリーンアップ/削除されています。",
+ "mismatch_other": "利用不可のオブジェクトが {{count}} 件、このレビュー項目に含まれています。これらはアラートまたは検知の条件を満たしていないか、既にクリーンアップ/削除されています。",
"hasMissingObjects": "次のラベルの追跡オブジェクトを保存したい場合は設定を調整してください: {{objects}} "
},
"toast": {
@@ -83,7 +83,8 @@
"attributes": "分類属性",
"title": {
"label": "タイトル"
- }
+ },
+ "scoreInfo": "スコア情報"
},
"exploreMore": "{{label}} のオブジェクトをさらに探索",
"exploreIsUnavailable": {
@@ -219,12 +220,22 @@
},
"hideObjectDetails": {
"label": "オブジェクトの移動経路を非表示"
+ },
+ "debugReplay": {
+ "label": "デバッグリプレイ",
+ "aria": "この追跡オブジェクトをデバッグリプレイビューで表示"
+ },
+ "more": {
+ "aria": "その他"
}
},
"dialog": {
"confirmDelete": {
"title": "削除の確認",
- "desc": "この追跡オブジェクトを削除すると、スナップショット、保存された埋め込み、および関連する追跡詳細項目が削除されます。履歴ビューの録画映像は削除されません 。 続行してもよろしいですか?"
+ "desc": "この追跡オブジェクトを削除すると、スナップショット、保存された埋め込み、および関連する追跡詳細項目が削除されます。履歴ビューの録画映像は削除されません。 続行してもよろしいですか?"
+ },
+ "toast": {
+ "error": "この追跡オブジェクトの削除に失敗しました: {{errorMessage}}"
}
},
"noTrackedObjects": "追跡オブジェクトは見つかりませんでした",
@@ -257,22 +268,25 @@
"count": "{{second}} 件中 {{first}} 件目",
"trackedPoint": "追跡ポイント",
"lifecycleItemDesc": {
- "visible": "{{label}} が検出されました",
+ "visible": "{{label}} が検知されました",
"entered_zone": "{{label}} が {{zones}} に入りました",
"active": "{{label}} がアクティブになりました",
"stationary": "{{label}} が静止状態になりました",
"attribute": {
- "faceOrLicense_plate": "{{label}} に {{attribute}} が検出されました",
+ "faceOrLicense_plate": "{{label}} に {{attribute}} が検知されました",
"other": "{{label}} は {{attribute}} と認識されました"
},
"gone": "{{label}} が離脱しました",
- "heard": "{{label}} の音が検出されました",
- "external": "{{label}} が検出されました",
+ "heard": "{{label}} の音が検知されました",
+ "external": "{{label}} が検知されました",
"header": {
"zones": "ゾーン",
"ratio": "比率",
"area": "面積",
- "score": "スコア"
+ "score": "スコア",
+ "computedScore": "計算スコア",
+ "topScore": "トップスコア",
+ "toggleAdvancedScores": "詳細スコアを切替"
}
},
"annotationSettings": {
@@ -283,7 +297,7 @@
},
"offset": {
"label": "注釈オフセット",
- "millisecondsToOffset": "検出アノテーションをオフセットするミリ秒数です。デフォルト: 0 ",
+ "millisecondsToOffset": "検知アノテーションをオフセットするミリ秒数です。デフォルト: 0 ",
"toast": {
"success": "{{camera}} のアノテーションオフセットが設定ファイルに保存されました。"
},
diff --git a/web/public/locales/ja/views/exports.json b/web/public/locales/ja/views/exports.json
index b32c8c62f4..5cff546459 100644
--- a/web/public/locales/ja/views/exports.json
+++ b/web/public/locales/ja/views/exports.json
@@ -1,23 +1,128 @@
{
"documentTitle": "エクスポート - Frigate",
- "noExports": "書き出しは見つかりません",
+ "noExports": "エクスポートが見つかりません",
"search": "検索",
- "deleteExport": "書き出しを削除",
+ "deleteExport": {
+ "label": "エクスポートを削除"
+ },
"deleteExport.desc": "{{exportName}} を削除してもよろしいですか?",
"editExport": {
- "title": "書き出し名を変更",
- "desc": "この書き出しの新しい名前を入力してください。",
- "saveExport": "書き出しを保存"
+ "title": "エクスポート名を変更",
+ "desc": "このエクスポートの新しい名前を入力してください。",
+ "saveExport": "エクスポートを保存"
},
"toast": {
"error": {
- "renameExportFailed": "書き出し名の変更に失敗しました: {{errorMessage}}"
+ "renameExportFailed": "エクスポート名の変更に失敗しました: {{errorMessage}}",
+ "assignCaseFailed": "ケース割り当ての更新に失敗しました: {{errorMessage}}",
+ "caseSaveFailed": "ケースの保存に失敗しました: {{errorMessage}}",
+ "caseDeleteFailed": "ケースの削除に失敗しました: {{errorMessage}}"
}
},
"tooltip": {
"shareExport": "エクスポートを共有",
"downloadVideo": "動画をダウンロード",
"editName": "名前を編集",
- "deleteExport": "エクスポートを削除"
+ "deleteExport": "エクスポートを削除",
+ "assignToCase": "ケースに追加",
+ "removeFromCase": "ケースから削除"
+ },
+ "headings": {
+ "cases": "ケース",
+ "uncategorizedExports": "未分類のエクスポート"
+ },
+ "toolbar": {
+ "newCase": "新しいケース",
+ "addExport": "エクスポートに追加",
+ "editCase": "ケースを編集",
+ "deleteCase": "ケースを削除"
+ },
+ "deleteCase": {
+ "label": "ケースを削除",
+ "desc": "本当に {{caseName}} を削除しますか ?",
+ "descKeepExports": "エクスポートは、分類されていないエクスポートとして引き続き利用可能です。",
+ "descDeleteExports": "この場合、すべてのエクスポートは完全に削除されます。",
+ "deleteExports": "エクスポートも削除する"
+ },
+ "caseDialog": {
+ "title": "ケースに追加",
+ "description": "既存のケースを選択するか、新しいケースを作成してください。",
+ "selectLabel": "ケース",
+ "newCaseOption": "新しいケースを作成",
+ "nameLabel": "ケース名",
+ "descriptionLabel": "説明"
+ },
+ "caseCard": {
+ "emptyCase": "まだエクスポートされていません"
+ },
+ "jobCard": {
+ "defaultName": "{{camera}} エクスポート",
+ "queued": "キューに追加しました",
+ "running": "実行中",
+ "preparing": "準備中",
+ "copying": "コピー中",
+ "encoding": "エンコード中",
+ "encodingRetry": "エンコード中 (再試行)",
+ "finalizing": "終了処理中"
+ },
+ "caseView": {
+ "noDescription": "説明がありません",
+ "exportCount_one": "1 件のエクスポート",
+ "exportCount_other": "{{count}} エクスポート",
+ "cameraCount_other": "{{count}} 台のカメラ",
+ "showMore": "さらに表示",
+ "showLess": "表示を減らす",
+ "emptyTitle": "このケースは空です",
+ "emptyDescription": "既存の分類されていないエクスポートを追加して、ケースを整理しましょう。",
+ "emptyDescriptionNoExports": "まだ追加可能な未分類のエクスポートはありません。",
+ "createdAt": "作成日 {{value}}",
+ "cameraCount_one": "1 台のカメラ"
+ },
+ "caseEditor": {
+ "createTitle": "ケースを作成",
+ "editTitle": "ケースを編集",
+ "namePlaceholder": "ケース名",
+ "descriptionPlaceholder": "このケースに関するメモや背景情報を追加する"
+ },
+ "addExportDialog": {
+ "title": "{{caseName}} にエクスポートを追加",
+ "searchPlaceholder": "未分類のエクスポートを検索",
+ "empty": "この検索条件に一致する未分類のエクスポートはありません。",
+ "addButton_one": "1 件のエクスポートを追加",
+ "addButton_other": "{{count}} 件のエクスポートを追加",
+ "adding": "追加中..."
+ },
+ "selected_one": "{{count}} 選択済み",
+ "selected_other": "{{count}} 選択済み",
+ "bulkActions": {
+ "addToCase": "ケースに追加",
+ "moveToCase": "ケースに移動",
+ "removeFromCase": "ケースから削除",
+ "delete": "削除",
+ "deleteNow": "今すぐ削除"
+ },
+ "bulkDelete": {
+ "title": "エクスポートを削除",
+ "desc_one": "{{count}} 件のエクスポートを削除してもよろしいですか?",
+ "desc_other": "{{count}} 件のエクスポートを削除してもよろしいですか?"
+ },
+ "bulkRemoveFromCase": {
+ "title": "ケースから削除",
+ "desc_one": "このケースから {{count}} 件のエクスポートを削除しますか?",
+ "desc_other": "このケースから {{count}} 件のエクスポートを削除しますか?",
+ "descKeepExports": "エクスポートは未分類に移動されます。",
+ "descDeleteExports": "エクスポートは完全に削除されます。",
+ "deleteExports": "代わりにエクスポートを削除する"
+ },
+ "bulkToast": {
+ "success": {
+ "delete": "エクスポートの削除に成功しました",
+ "reassign": "ケース割り当ての更新に成功しました",
+ "remove": "ケースからエクスポートを正常に削除しました"
+ },
+ "error": {
+ "deleteFailed": "エクスポートの削除に失敗しました: {{errorMessage}}",
+ "reassignFailed": "ケース割り当ての更新に失敗しました: {{errorMessage}}"
+ }
}
}
diff --git a/web/public/locales/ja/views/faceLibrary.json b/web/public/locales/ja/views/faceLibrary.json
index fdf43a65cf..8a79b4ae4e 100644
--- a/web/public/locales/ja/views/faceLibrary.json
+++ b/web/public/locales/ja/views/faceLibrary.json
@@ -17,7 +17,7 @@
"documentTitle": "顔データベース - Frigate",
"uploadFaceImage": {
"title": "顔画像をアップロード",
- "desc": "顔を検出するために画像をアップロードし、{{pageToggle}} に追加します"
+ "desc": "顔を検知するために画像をアップロードし、{{pageToggle}} に追加します"
},
"collections": "コレクション",
"createFaceLibrary": {
@@ -39,7 +39,11 @@
"title": "過去の学習",
"aria": "過去の学習を選択",
"empty": "最近の顔認識の試行はありません",
- "titleShort": "Classifications,最近の分類結果を選択,,False,train.aria,,"
+ "titleShort": "最近の分類",
+ "emptyNoLibrary": {
+ "title": "顔画像をアップロード",
+ "description": "顔認識を機能させるには、ライブラリに少なくとも 1 つの顔を追加する必要があります。"
+ }
},
"selectFace": "顔を選択",
"deleteFaceLibrary": {
@@ -82,7 +86,8 @@
"deletedName_other": "{{count}} 件の顔を削除しました。",
"renamedFace": "顔の名前を {{name}} に変更しました",
"trainedFace": "顔の学習が完了しました。",
- "updatedFaceScore": "顔のスコアを {{name}} ({{score}})に更新しました。"
+ "updatedFaceScore": "顔のスコアを {{name}} ({{score}})に更新しました。",
+ "reclassifiedFace": "顔を再分類しました。"
},
"error": {
"uploadingImageFailed": "画像のアップロードに失敗しました: {{errorMessage}}",
@@ -91,7 +96,10 @@
"deleteNameFailed": "名前の削除に失敗しました: {{errorMessage}}",
"renameFaceFailed": "顔の名前変更に失敗しました: {{errorMessage}}",
"trainFailed": "学習に失敗しました: {{errorMessage}}",
- "updateFaceScoreFailed": "顔スコアの更新に失敗しました: {{errorMessage}}"
+ "updateFaceScoreFailed": "顔スコアの更新に失敗しました: {{errorMessage}}",
+ "reclassifyFailed": "顔の再分類に失敗しました: {{errorMessage}}"
}
- }
+ },
+ "reclassifyFaceAs": "顔を再分類する:",
+ "reclassifyFace": "顔の再分類"
}
diff --git a/web/public/locales/ja/views/live.json b/web/public/locales/ja/views/live.json
index fe73c1d08e..0fe4e7b99f 100644
--- a/web/public/locales/ja/views/live.json
+++ b/web/public/locales/ja/views/live.json
@@ -1,5 +1,7 @@
{
- "documentTitle": "ライブ - Frigate",
+ "documentTitle": {
+ "default": "ライブ - Frigate"
+ },
"documentTitle.withCamera": "{{camera}} - ライブ - Frigate",
"lowBandwidthMode": "低帯域モード",
"twoWayTalk": {
@@ -15,7 +17,8 @@
"clickMove": {
"label": "フレーム内をクリックしてカメラを中央に移動",
"enable": "クリック移動を有効化",
- "disable": "クリック移動を無効化"
+ "disable": "クリック移動を無効化",
+ "enableWithZoom": "クリックで移動、ドラッグでズームを有効にする"
},
"left": {
"label": "PTZ カメラを左へ移動"
@@ -55,27 +58,30 @@
},
"camera": {
"enable": "カメラを有効化",
- "disable": "カメラを無効化"
+ "disable": "カメラを無効化",
+ "turnOn": "カメラをオンにする",
+ "turnOff": "カメラをオフにする"
},
"muteCameras": {
"enable": "全カメラをミュート",
"disable": "全カメラのミュートを解除"
},
"detect": {
- "enable": "検出を有効化",
- "disable": "検出を無効化"
+ "enable": "検知を有効化",
+ "disable": "検知を無効化"
},
"recording": {
"enable": "録画を有効化",
- "disable": "録画を無効化"
+ "disable": "録画を無効化",
+ "disabledInConfig": "このカメラでは、まず「設定」で録画機能を有効にする必要があります。"
},
"snapshots": {
"enable": "スナップショットを有効化",
"disable": "スナップショットを無効化"
},
"audioDetect": {
- "enable": "音声検出を有効化",
- "disable": "音声検出を無効化"
+ "enable": "音声検知を有効化",
+ "disable": "音声検知を無効化"
},
"transcription": {
"enable": "ライブ音声文字起こしを有効化",
@@ -138,18 +144,19 @@
"tips": "プレーヤーが非表示でもストリーミングを継続するにはこのオプションを有効にします。"
},
"debug": {
- "picker": "デバッグモードではストリームの選択はできません。デバッグビューは常に 検出ロールに割り当てられたストリームを使用します。"
+ "picker": "デバッグモードではストリームの選択はできません。デバッグビューは常に 検知ロールに割り当てられたストリームを使用します。"
}
},
"cameraSettings": {
"title": "{{camera}} の設定",
"cameraEnabled": "カメラ有効",
- "objectDetection": "物体検出",
+ "objectDetection": "物体検知",
"recording": "録画",
"snapshots": "スナップショット",
- "audioDetection": "音声検出",
+ "audioDetection": "音声検知",
"transcription": "音声文字起こし",
- "autotracking": "オートトラッキング"
+ "autotracking": "オートトラッキング",
+ "camera": "カメラ"
},
"history": {
"label": "履歴映像を表示"
diff --git a/web/public/locales/ja/views/motionSearch.json b/web/public/locales/ja/views/motionSearch.json
new file mode 100644
index 0000000000..cc648822e2
--- /dev/null
+++ b/web/public/locales/ja/views/motionSearch.json
@@ -0,0 +1,78 @@
+{
+ "documentTitle": "モーション検索 - Frigate",
+ "title": "モーション検索",
+ "description": "関心領域を定義する多角形を描画し、その領域内で動きの変化を検索する時間範囲を指定します。",
+ "selectCamera": "モーション検索を読み込み中です",
+ "dialog": {
+ "title": "モーション検索",
+ "cameraLabel": "カメラ",
+ "previewAlt": "{{camera}} のカメラプレビュー"
+ },
+ "startSearch": "検索開始",
+ "searchStarted": "検索を開始しました",
+ "searchCancelled": "検索がキャンセルされました",
+ "cancelSearch": "キャンセル",
+ "searching": "検索中です。",
+ "searchComplete": "検索完了",
+ "noResultsYet": "選択した領域内の動きの変化を検索します",
+ "noChangesFound": "選択した領域でピクセルの変化は検知されませんでした",
+ "changesFound_other": "{{count}} 件の動きの変化が見つかりました",
+ "framesProcessed": "{{count}} フレームを処理しました",
+ "jumpToTime": "この時間に移動",
+ "results": "結果",
+ "showSegmentHeatmap": "ヒートマップ",
+ "newSearch": "新規検索",
+ "clearResults": "結果をクリア",
+ "clearROI": "ポリゴンをクリア",
+ "polygonControls": {
+ "points_other": "{{count}} ポイント",
+ "undo": "直前のポイントを元に戻す",
+ "reset": "ポリゴンをリセット",
+ "drawMode": "描画",
+ "moveMode": "移動"
+ },
+ "motionHeatmapLabel": "モーションヒートマップ",
+ "timeRange": {
+ "title": "検索範囲",
+ "start": "開始時間",
+ "end": "終了時間"
+ },
+ "settings": {
+ "title": "検索設定",
+ "parallelMode": "並列モード",
+ "parallelModeDesc": "複数の録画範囲を同時にスキャンする(処理が高速化されますが、デコードリソースをより多く消費します)",
+ "threshold": "感度しきい値",
+ "thresholdDesc": "値を小さくするとより小さな変化も検知します (1-255)",
+ "minArea": "最小変化面積",
+ "minAreaDesc": "単一移動領域の最小サイズ(関心領域に対するパーセンテージ)",
+ "frameSkip": "フレームスキップ",
+ "frameSkipDesc": "N フレームごとに処理します。カメラのフレームレートと同じ値にすると 1 秒あたり 1 フレーム処理されます(例: 5 FPS のカメラなら 5、30 FPS なら 30)。値を大きくすると高速になりますが、短時間のモーションを取りこぼす可能性があります。",
+ "maxResults": "最大結果数",
+ "maxResultsDesc": "この件数のタイムスタンプにヒットした時点でスキャンを停止します"
+ },
+ "errors": {
+ "noCamera": "カメラを選択してください",
+ "noROI": "関心領域を描画してください",
+ "noTimeRange": "時間範囲を選択してください",
+ "invalidTimeRange": "終了時刻は開始時刻より後である必要があります",
+ "searchFailed": "検索に失敗しました: {{message}}",
+ "polygonTooSmall": "ポリゴンには少なくとも 3 つの点が必要です",
+ "unknown": "不明なエラー"
+ },
+ "changePercentage": "{{percentage}}% 変化",
+ "metrics": {
+ "title": "検索メトリクス",
+ "segmentsScanned": "スキャンしたセグメント",
+ "segmentsProcessed": "処理済み",
+ "segmentsSkippedInactive": "スキップ (アクティビティなし)",
+ "segmentsSkippedHeatmap": "スキップ (関心領域と重なりなし)",
+ "fallbackFullRange": "全範囲スキャンへのフォールバック",
+ "framesDecoded": "デコードしたフレーム",
+ "wallTime": "検索時間",
+ "segmentErrors": "セグメントエラー",
+ "seconds": "{{seconds}} 秒",
+ "minutesSeconds": "{{minutes}} 分 {{seconds}} 秒",
+ "scanSummary": "{{segments}} セグメント · {{time}}"
+ },
+ "scanning": "スキャン中 {{time}}"
+}
diff --git a/web/public/locales/ja/views/replay.json b/web/public/locales/ja/views/replay.json
new file mode 100644
index 0000000000..b975486100
--- /dev/null
+++ b/web/public/locales/ja/views/replay.json
@@ -0,0 +1,59 @@
+{
+ "title": "デバッグリプレイ",
+ "description": "デバッグ用にカメラの録画をリプレイします。オブジェクトリストには検知されたオブジェクトの遅延サマリーが表示され、「メッセージ」タブにはリプレイ映像からのFrigate内部メッセージのストリームが表示されます。",
+ "websocket_messages": "メッセージ",
+ "dialog": {
+ "title": "デバッグリプレイを開始",
+ "description": "オブジェクトの検知やトラッキングの問題をデバッグするために、過去の映像をループ再生する一時的なリプレイカメラを作成します。このリプレイカメラは、ソースカメラ(元カメラ)と同じ検知設定を引き継ぎます。開始する時間範囲を選択してください。",
+ "camera": "ソースカメラ",
+ "timeRange": "期間",
+ "preset": {
+ "1m": "直近1分間",
+ "5m": "直近5分間",
+ "timeline": "タイムラインから選択",
+ "custom": "カスタム"
+ },
+ "startButton": "リプレイ開始",
+ "selectFromTimeline": "選択",
+ "starting": "リプレイを開始しています...",
+ "startLabel": "開始",
+ "endLabel": "終了",
+ "toast": {
+ "error": "デバッグリプレイの開始に失敗しました: {{error}}",
+ "alreadyActive": "リプレイセッションはすでに実行中です",
+ "stopError": "デバッグリプレイの停止に失敗しました: {{error}}",
+ "goToReplay": "リプレイへ移動"
+ }
+ },
+ "page": {
+ "noSession": "アクティブなデバッグリプレイセッションはありません",
+ "noSessionDesc": "履歴ビューからデバッグリプレイを開始するには、ツールバーの「アクション」ボタンをクリックし、「デバッグリプレイ」を選択してください。",
+ "goToRecordings": "履歴画面へ",
+ "preparingClip": "クリップを準備しています…",
+ "preparingClipDesc": "Frigateは選択された時間範囲の録画を結合しています。指定した期間が長い場合、処理に1分ほどかかる場合があります。",
+ "startingCamera": "デバッグリプレイを開始しています…",
+ "startError": {
+ "title": "デバッグリプレイの開始に失敗しました",
+ "back": "履歴へ戻る"
+ },
+ "sourceCamera": "ソースカメラ",
+ "replayCamera": "リプレイカメラ",
+ "initializingReplay": "デバッグリプレイを初期化しています...",
+ "stoppingReplay": "デバッグリプレイを停止しています...",
+ "stopReplay": "リプレイを停止",
+ "confirmStop": {
+ "title": "デバッグリプレイを停止しますか?",
+ "description": "セッションを停止し、すべての一時データを削除します。よろしいですか?",
+ "confirm": "リプレイ停止",
+ "cancel": "キャンセル"
+ },
+ "activity": "アクティビティ",
+ "objects": "オブジェクト一覧",
+ "audioDetections": "音声検知",
+ "noActivity": "アクティビティは検知されませんでした",
+ "activeTracking": "アクティブトラッキング",
+ "noActiveTracking": "アクティブトラッキングなし",
+ "configuration": "設定",
+ "configurationDesc": "デバッグリプレイカメラのモーション検知およびオブジェクトトラッキング設定を微調整します。ここで行った変更はFrigateの設定ファイルには保存されません。"
+ }
+}
diff --git a/web/public/locales/ja/views/settings.json b/web/public/locales/ja/views/settings.json
index 324fec9642..0222eba9f1 100644
--- a/web/public/locales/ja/views/settings.json
+++ b/web/public/locales/ja/views/settings.json
@@ -13,13 +13,16 @@
"cameraManagement": "カメラ設定 - Frigate",
"cameraReview": "カメラレビュー設定 - Frigate",
"maintenance": "メンテナンス - Frigate",
- "profiles": "プロファイル - Frigate"
+ "profiles": "プロファイル - Frigate",
+ "globalConfig": "グローバル設定 - Frigate",
+ "cameraConfig": "カメラ設定 - Frigate",
+ "detectorsAndModel": "検出器とモデル - Frigate"
},
"menu": {
"ui": "UI",
"enrichments": "高度解析",
"cameras": "カメラ設定",
- "masksAndZones": "マスク/ゾーン",
+ "masksAndZones": "マスク / ゾーン",
"motionTuner": "モーションチューナー",
"triggers": "トリガー",
"debug": "デバッグ",
@@ -28,10 +31,65 @@
"frigateplus": "Frigate+",
"cameraManagement": "管理",
"cameraReview": "レビュー",
- "roles": "区分",
- "general": "一般",
+ "roles": "ロール",
+ "general": "全般",
"globalConfig": "グローバル設定",
- "system": "システム"
+ "system": "システム",
+ "integrations": "連携",
+ "uiSettings": "UI設定",
+ "profiles": "プロファイル",
+ "globalDetect": "物体検知",
+ "globalRecording": "録画",
+ "globalSnapshots": "スナップショット",
+ "globalFfmpeg": "FFmpeg",
+ "globalMotion": "モーション検知",
+ "globalObjects": "オブジェクト",
+ "globalReview": "レビュー",
+ "globalAudioEvents": "音声検知",
+ "globalLivePlayback": "ライブ再生",
+ "globalTimestampStyle": "タイムスタンプスタイル",
+ "systemDatabase": "データベース",
+ "systemTls": "TLS",
+ "systemAuthentication": "認証",
+ "systemNetworking": "ネットワーキング",
+ "systemProxy": "プロキシ",
+ "systemUi": "UI",
+ "systemLogging": "ロギング",
+ "systemEnvironmentVariables": "環境変数",
+ "systemTelemetry": "テレメトリ",
+ "systemBirdseye": "バードアイ",
+ "systemFfmpeg": "FFmpeg",
+ "systemDetectorsAndModel": "検出器とモデル",
+ "systemMqtt": "MQTT",
+ "systemGo2rtcStreams": "go2rtc ストリーム",
+ "integrationSemanticSearch": "セマンティック検索",
+ "integrationGenerativeAi": "生成AI",
+ "integrationFaceRecognition": "顔認識",
+ "integrationLpr": "ナンバープレート認識",
+ "integrationObjectClassification": "オブジェクト分類",
+ "integrationAudioTranscription": "音声文字起こし",
+ "cameraDetect": "物体検知",
+ "cameraFfmpeg": "FFmpeg",
+ "cameraRecording": "録画",
+ "cameraSnapshots": "スナップショット",
+ "cameraMotion": "モーション検知",
+ "cameraObjects": "オブジェクト",
+ "cameraConfigReview": "レビュー",
+ "cameraAudioEvents": "音声検知",
+ "cameraAudioTranscription": "音声文字起こし",
+ "cameraNotifications": "通知",
+ "cameraLivePlayback": "ライブ再生",
+ "cameraBirdseye": "バードアイ",
+ "cameraFaceRecognition": "顔認識",
+ "cameraLpr": "ナンバープレート認識",
+ "cameraMqttConfig": "MQTT",
+ "cameraOnvif": "ONVIF",
+ "cameraUi": "カメラ UI",
+ "cameraTimestampStyle": "タイムスタンプスタイル",
+ "cameraMqtt": "カメラ MQTT",
+ "maintenance": "メンテナンス",
+ "mediaSync": "メディア同期",
+ "regionGrid": "リージョングリッド"
},
"dialog": {
"unsavedChanges": {
@@ -61,7 +119,7 @@
},
"liveFallbackTimeout": {
"label": "ライブプレイヤーのフォールバック タイムアウト",
- "desc": "カメラの高画質ライブストリームが利用できない場合、指定した秒数後に低帯域モードへ切り替えます。デフォルト:3 秒"
+ "desc": "カメラの高画質ライブストリームが利用できない場合、指定した秒数後に低帯域モードへ切り替えます。デフォルトは 3 秒。"
}
},
"storedLayouts": {
@@ -93,7 +151,7 @@
"toast": {
"success": {
"clearStoredLayout": "{{cameraName}} の保存済みレイアウトをクリアしました",
- "clearStreamingSettings": "すべてのカメラグループのストリーミング設定をクリアしました。"
+ "clearStreamingSettings": "全カメラグループのストリーミング設定をクリアしました。"
},
"error": {
"clearStoredLayoutFailed": "保存済みレイアウトのクリアに失敗しました: {{errorMessage}}",
@@ -113,7 +171,7 @@
"desc": "Frigate のセマンティック検索では、画像そのもの、ユーザー定義のテキスト説明、または自動生成された説明を用いて、レビュー項目内の追跡オブジェクトを検索できます。",
"reindexNow": {
"label": "今すぐ再インデックス",
- "desc": "再インデックスは、すべての追跡オブジェクトの埋め込みを再生成します。バックグラウンドで実行され、追跡オブジェクト数によっては CPU を使い切り、相応の時間がかかる場合があります。",
+ "desc": "インデックスの再構築を行うと、追跡対象のすべてのオブジェクトの埋め込みが再生成されます。この処理はバックグラウンドで実行され、追跡対象のオブジェクトの数によってはCPU使用率が最大になり、かなりの時間がかかる場合があります。",
"confirmTitle": "再インデックスの確認",
"confirmDesc": "すべての追跡オブジェクトの埋め込みを再インデックスしますか?この処理はバックグラウンドで実行されますが、CPU を使い切り、時間がかかる場合があります。進行状況は[探索]ページで確認できます。",
"confirmButton": "再インデックス",
@@ -152,7 +210,7 @@
},
"licensePlateRecognition": {
"title": "ナンバープレート認識",
- "desc": "車両のナンバープレートを認識し、検出文字列を recognized_license_plate フィールドへ、または既知の名称を car タイプのオブジェクトの sub_label として自動追加できます。一般的な用途として、私道に入ってくる車や道路を通過する車のナンバー読み取りがあります。"
+ "desc": "車両のナンバープレートを認識し、検知文字列を recognized_license_plate フィールドへ、または既知の名称を car タイプのオブジェクトの sub_label として自動追加できます。一般的な用途として、私道に入ってくる車や道路を通過する車のナンバー読み取りがあります。"
},
"restart_required": "再起動が必要です(高度解析設定を変更)",
"toast": {
@@ -244,7 +302,7 @@
}
},
"motionMaskLabel": "モーションマスク {{number}}",
- "objectMaskLabel": "オブジェクトマスク {{number}}({{label}})",
+ "objectMaskLabel": "オブジェクトマスク {{number}}",
"form": {
"zoneName": {
"error": {
@@ -298,6 +356,21 @@
"zone": "ゾーン",
"motion_mask": "モーションマスク",
"object_mask": "オブジェクトマスク"
+ },
+ "revertOverride": {
+ "title": "ベース設定に戻す",
+ "desc": "{{type}} {{name}} のプロファイル上書きを削除し、ベース設定に戻します。"
+ }
+ },
+ "id": {
+ "error": {
+ "mustNotBeEmpty": "ID は空にできません。",
+ "alreadyExists": "この ID のマスクはこのカメラに既に存在します。"
+ }
+ },
+ "name": {
+ "error": {
+ "mustNotBeEmpty": "名前は空にできません。"
}
}
},
@@ -313,7 +386,7 @@
"point_other": "{{count}} 点",
"clickDrawPolygon": "画像上をクリックして多角形を描画します。",
"name": {
- "title": "名称",
+ "title": "名前",
"inputPlaceHolder": "名前を入力…",
"tips": "名前は2文字以上で、少なくとも1文字のアルファベットを含み、このカメラ上の他のゾーン名やカメラ名と同一であってはなりません。"
},
@@ -350,43 +423,53 @@
},
"toast": {
"success": "ゾーン({{zoneName}})を保存しました。"
+ },
+ "enabled": {
+ "title": "有効",
+ "description": "このゾーンを設定ファイルで有効化するかどうか。無効化すると MQTT からも有効化できず、実行時には無視されます。"
}
},
"motionMasks": {
"label": "モーションマスク",
"documentTitle": "モーションマスクを編集 - Frigate",
"desc": {
- "title": "モーションマスクは、望ましくない種類の動きで検出がトリガーされるのを防ぎます。過度なマスクはオブジェクト追跡を困難にします。",
+ "title": "モーションマスクは、望ましくない種類の動きで検知がトリガーされるのを防ぎます。過度なマスクはオブジェクト追跡を困難にします。",
"documentation": "ドキュメント"
},
"add": "新しいモーションマスク",
"edit": "モーションマスクを編集",
"context": {
- "title": "モーションマスクは、望ましくない動き(例: 木の枝、カメラのタイムスタンプ)で検出がトリガーされるのを防ぐために使用します。ごく控えめ に使用してください。過度なマスクはオブジェクト追跡を困難にします。"
+ "title": "モーションマスクは、望ましくない動き(例: 木の枝、カメラのタイムスタンプ)で検知がトリガーされるのを防ぐために使用します。ごく控えめ に使用してください。過度なマスクはオブジェクト追跡を困難にします。"
},
"point_other": "{{count}} 点",
"clickDrawPolygon": "画像上をクリックして多角形を描画します。",
"polygonAreaTooLarge": {
"title": "モーションマスクがカメラフレームの {{polygonArea}}% を覆っています。大きなモーションマスクは推奨されません。",
- "tips": "モーションマスクはオブジェクトの検出自体を防ぎません。代わりに必須ゾーンを使用してください。"
+ "tips": "モーションマスクはオブジェクトの検知自体を防ぎません。代わりに必須ゾーンを使用してください。"
},
"toast": {
"success": {
"title": "{{polygonName}} を保存しました。",
"noName": "モーションマスクを保存しました。"
}
+ },
+ "defaultName": "モーションマスク {{number}}",
+ "name": {
+ "title": "名前",
+ "description": "このモーションマスクの任意の表示名です。",
+ "placeholder": "名前を入力..."
}
},
"objectMasks": {
"label": "オブジェクトマスク",
"documentTitle": "オブジェクトマスクを編集 - Frigate",
"desc": {
- "title": "オブジェクトフィルタマスクは、位置に基づいて特定のオブジェクトタイプの誤検出を除外するために使用します。",
+ "title": "オブジェクトフィルタマスクは、位置に基づいて特定のオブジェクトタイプの誤検知を除外するために使用します。",
"documentation": "ドキュメント"
},
"add": "オブジェクトマスクを追加",
"edit": "オブジェクトマスクを編集",
- "context": "オブジェクトフィルタマスクは、位置に基づいて特定のオブジェクトタイプの誤検出を除外するために使用します。",
+ "context": "オブジェクトフィルタマスクは、位置に基づいて特定のオブジェクトタイプの誤検知を除外するために使用します。",
"point_other": "{{count}} 点",
"clickDrawPolygon": "画像上をクリックして多角形を描画します。",
"objects": {
@@ -399,14 +482,29 @@
"title": "{{polygonName}} を保存しました。",
"noName": "オブジェクトマスクを保存しました。"
}
+ },
+ "name": {
+ "title": "名前",
+ "description": "このオブジェクトマスクの任意の表示名です。",
+ "placeholder": "名前を入力..."
+ }
+ },
+ "disabledInConfig": "この項目は設定ファイルで無効化されています",
+ "addDisabledProfile": "まずベース設定に追加してから、プロファイルで上書きしてください",
+ "profileBase": "(ベース)",
+ "profileOverride": "(上書き)",
+ "masks": {
+ "enabled": {
+ "title": "有効",
+ "description": "このマスクを設定ファイルで有効化するかどうか。無効化すると MQTT からも有効化できず、実行時には無視されます。"
}
}
},
"motionDetectionTuner": {
- "title": "モーション検出チューナー",
+ "title": "モーション検知チューナー",
"unsavedChanges": "未保存のモーションチューナーの変更({{camera}})",
"desc": {
- "title": "Frigate は、フレーム内に物体検出で確認すべき動きがあるかの一次チェックとしてモーション検出を使用します。",
+ "title": "Frigate は、フレーム内に物体検知で確認すべき動きがあるかの一次チェックとしてモーション検知を使用します。",
"documentation": "モーション調整ガイドを読む"
},
"Threshold": {
@@ -427,15 +525,15 @@
},
"debug": {
"title": "デバッグ",
- "detectorDesc": "Frigate は検出器({{detectors}})を使用して、カメラの映像ストリーム内のオブジェクトを検出します。",
- "desc": "デバッグビューは、追跡オブジェクトとその統計をリアルタイムに表示します。オブジェクト一覧には、検出オブジェクトの時差サマリが表示されます。",
+ "detectorDesc": "Frigate は検出器({{detectors}})を使用して、カメラの映像ストリーム内のオブジェクトを検知します。",
+ "desc": "デバッグビューは、追跡オブジェクトとその統計をリアルタイムに表示します。オブジェクト一覧には、検知オブジェクトの時差サマリが表示されます。",
"openCameraWebUI": "{{camera}} の Web UI を開く",
"debugging": "デバッグ",
"objectList": "オブジェクト一覧",
"noObjects": "オブジェクトなし",
"audio": {
"title": "音声",
- "noAudioDetections": "音声検出なし",
+ "noAudioDetections": "音声検知なし",
"score": "スコア",
"currentRMS": "現在の RMS",
"currentdbFS": "現在の dBFS"
@@ -445,7 +543,7 @@
"desc": "追跡オブジェクトの周囲にバウンディングボックスを表示します",
"colors": {
"label": "オブジェクトのボックス色",
- "info": "起動時に、各オブジェクトラベルへ異なる色が割り当てられます 細い濃青線は、現在時点では未検出であることを示します 細い灰線は、静止していると検出されたことを示します 太線は、(有効時)オートトラッキングの対象であることを示します "
+ "info": "起動時に、各オブジェクトラベルへ異なる色が割り当てられます 細い濃青線は、現在時点では未検知であることを示します 細い灰線は、静止していると検知されたことを示します 太線は、(有効時)オートトラッキングの対象であることを示します "
}
},
"timestamp": {
@@ -462,8 +560,8 @@
},
"motion": {
"title": "モーションボックス",
- "desc": "モーションが検出された領域のボックスを表示します",
- "tips": "モーションボックス
現在モーションが検出されている領域に赤いボックスが重ねて表示されます
"
+ "desc": "モーションが検知された領域のボックスを表示します",
+ "tips": "モーションボックス
現在モーションが検知されている領域に赤いボックスが重ねて表示されます
"
},
"regions": {
"title": "領域",
@@ -594,7 +692,7 @@
"admin": "管理者",
"adminDesc": "すべての機能にフルアクセス。",
"viewer": "閲覧者",
- "viewerDesc": "ライブ、レビュー、探索、書き出しに限定。",
+ "viewerDesc": "ライブ、レビュー、探索、エクスポートに限定。",
"customDesc": "特定のカメラアクセスを持つカスタムロール。"
}
}
@@ -619,7 +717,7 @@
"createRole": "ロール {{role}} を作成しました",
"updateCameras": "ロール {{role}} のカメラを更新しました",
"deleteRole": "ロール {{role}} を削除しました",
- "userRolesUpdated_other": "このロールに割り当てられていた {{count}} ユーザーは「viewer」に更新され、すべてのカメラへの閲覧アクセスが付与されました。"
+ "userRolesUpdated_other": "このロールに割り当てられていた {{count}} ユーザーは「viewer」に更新され、全カメラへの閲覧アクセスが付与されました。"
},
"error": {
"createRoleFailed": "ロールの作成に失敗しました: {{errorMessage}}",
@@ -639,7 +737,7 @@
},
"deleteRole": {
"title": "ロールを削除",
- "desc": "この操作は元に戻せません。ロールは完全に削除され、このロールを持っていたユーザーは「viewer」ロールに再割り当てされ、すべてのカメラへの閲覧アクセスが付与されます。",
+ "desc": "この操作は元に戻せません。ロールは完全に削除され、このロールを持っていたユーザーは「viewer」ロールに再割り当てされ、全カメラへの閲覧アクセスが付与されます。",
"warn": "{{role}} を削除してもよろしいですか?",
"deleting": "削除中…"
},
@@ -668,7 +766,8 @@
},
"notificationUnavailable": {
"title": "通知は利用できません",
- "desc": "Web プッシュ通知にはセキュアコンテキスト(https://…)が必要です。これはブラウザの制限です。通知を利用するには、セキュアに Frigate へアクセスしてください。"
+ "desc": "Web プッシュ通知にはセキュアコンテキスト(https://…)が必要です。これはブラウザの制限です。通知を利用するには、セキュアに Frigate へアクセスしてください。",
+ "descPwa": "iOSでは、Frigateをホーム画面に追加した場合にのみ、Webプッシュ通知を利用できます。共有 メニューを開き、ホーム画面に追加 を選択してから、新しいアイコンからFrigateを起動し、このデバイスを通知対象として登録してください。"
},
"globalSettings": {
"title": "グローバル設定",
@@ -724,8 +823,8 @@
},
"snapshotConfig": {
"title": "スナップショット設定",
- "desc": "Frigate+ への送信には、設定でスナップショットと clean_copy スナップショットの両方を有効にする必要があります。",
- "cleanCopyWarning": "一部のカメラではスナップショットは有効ですが、クリーンコピーが無効です。これらのカメラから Frigate+ へ画像を送信するには、スナップショット設定で clean_copy を有効にしてください。",
+ "desc": "Frigate+ への送信には、設定でスナップショットを有効にする必要があります。",
+ "cleanCopyWarning": "一部のカメラではスナップショット機能が無効になっています",
"table": {
"camera": "カメラ",
"snapshots": "スナップショット",
@@ -747,14 +846,30 @@
"error": "モデル情報の読み込みに失敗しました",
"availableModels": "利用可能なモデル",
"loadingAvailableModels": "利用可能なモデルを読み込み中…",
- "modelSelect": "ここで Frigate+ 上の利用可能なモデルを選択できます。現在の検出器構成と互換性のあるモデルのみ選択可能です。"
+ "modelSelect": "ここで Frigate+ 上の利用可能なモデルを選択できます。現在の検出器構成と互換性のあるモデルのみ選択可能です。",
+ "noModelLoaded": "Frigate+ モデルは現在読み込まれていません。",
+ "selectModel": "モデルを選択",
+ "noModelsAvailable": "利用可能なモデルがありません",
+ "filter": {
+ "ariaLabel": "モデルをタイプで絞り込み",
+ "baseModels": "ベースモデル",
+ "fineTunedModels": "ファインチューニング済みモデル"
+ }
},
"unsavedChanges": "未保存の Frigate+ 設定の変更",
"restart_required": "再起動が必要です(Frigate+ モデルを変更)",
"toast": {
"success": "Frigate+ 設定を保存しました。変更を適用するには Frigate を再起動してください。",
"error": "設定変更の保存に失敗しました: {{errorMessage}}"
- }
+ },
+ "description": "Frigate+ はサブスクリプションサービスで、独自データで学習させたカスタム物体検知モデルなど、Frigate インスタンスに追加機能を提供します。Frigate+ モデルの設定はここから管理できます。",
+ "cardTitles": {
+ "api": "API",
+ "currentModel": "現在のモデル",
+ "otherModels": "その他のモデル",
+ "configuration": "設定"
+ },
+ "changeInDetectorsAndModel": "モデルを変更"
},
"triggers": {
"documentTitle": "トリガー",
@@ -764,7 +879,7 @@
},
"addTrigger": "トリガーを追加",
"table": {
- "name": "名称",
+ "name": "名前",
"type": "タイプ",
"content": "コンテンツ",
"threshold": "しきい値",
@@ -799,7 +914,7 @@
},
"form": {
"name": {
- "title": "名称",
+ "title": "名前",
"placeholder": "トリガー名を入力",
"error": {
"minLength": "この項目は2文字以上で入力してください。",
@@ -814,15 +929,15 @@
"type": {
"title": "タイプ",
"placeholder": "トリガータイプを選択",
- "description": "類似した追跡オブジェクトの説明が検出されたときにトリガー",
- "thumbnail": "類似した追跡オブジェクトのサムネイルが検出されたときにトリガー"
+ "description": "類似した追跡オブジェクトの説明が検知されたときにトリガー",
+ "thumbnail": "類似した追跡オブジェクトのサムネイルが検知されたときにトリガー"
},
"content": {
"title": "コンテンツ",
"imagePlaceholder": "サムネイルを選択",
"textPlaceholder": "テキストを入力",
"imageDesc": "最新のサムネイル100件のみが表示されます。目的のサムネイルが見つからない場合は、探索で過去のオブジェクトを確認し、そこのメニューからトリガーを設定してください。",
- "textDesc": "類似する追跡オブジェクトの説明が検出されたときにこのアクションをトリガーするためのテキストを入力します。",
+ "textDesc": "類似する追跡オブジェクトの説明が検知されたときにこのアクションをトリガーするためのテキストを入力します。",
"error": {
"required": "コンテンツは必須です。"
}
@@ -836,7 +951,7 @@
"desc": "このトリガーの類似度しきい値を設定します。値が高いほど、より近い一致が必要になります。"
},
"actions": {
- "title": "アクション",
+ "title": "操作",
"desc": "デフォルトでは、Frigate はすべてのトリガーに対して MQTT メッセージを送信します。サブラベルは、トリガー名をオブジェクトのラベルに追加します。属性(Attributes)は、追跡オブジェクトのメタデータとは別に保存される検索可能なメタデータです。",
"error": {
"min": "少なくとも1つのアクションを選択してください。"
@@ -937,7 +1052,7 @@
"quality": "品質",
"selectQuality": "品質を選択",
"roleLabels": {
- "detect": "オブジェクト検出",
+ "detect": "物体検知",
"record": "録画",
"audio": "音声"
},
@@ -949,12 +1064,12 @@
"notConnected": "未接続",
"featuresTitle": "機能",
"go2rtc": "カメラへの接続数を削減",
- "detectRoleWarning": "続行するには、少なくとも 1 つのストリームに「検出」ロールが必要です。",
+ "detectRoleWarning": "続行するには、少なくとも 1 つのストリームに「検知」ロールが必要です。",
"rolesPopover": {
"title": "ストリーム ロール",
- "detect": "オブジェクト検出用のメイン フィードです。",
+ "detect": "物体検知用のメイン フィードです。",
"record": "設定に基づいて映像フィードのセグメントを保存します。",
- "audio": "音声ベース検出用のフィードです。"
+ "audio": "音声ベース検知用のフィードです。"
},
"featuresPopover": {
"title": "ストリーム機能",
@@ -975,8 +1090,8 @@
},
"testResultLabels": {
"resolution": "解像度",
- "video": "ビデオ",
- "audio": "オーディオ",
+ "video": "映像",
+ "audio": "音声",
"fps": "FPS"
},
"commonErrors": {
@@ -984,7 +1099,7 @@
"testFailed": "ストリームテストに失敗しました: {{error}}"
},
"step1": {
- "description": "カメラの詳細を入力し、カメラを自動検出するか、メーカーを手動で選択してください。",
+ "description": "カメラの詳細を入力し、カメラを自動検知するか、メーカーを手動で選択してください。",
"cameraName": "カメラ名",
"cameraNamePlaceholder": "例: front_door または Back Yard Overview",
"host": "ホスト/IP アドレス",
@@ -1016,23 +1131,23 @@
"brands": {
"reolink-rtsp": "Reolink の RTSP は推奨されません。カメラ設定で http を有効にし、カメラウィザードを再起動することを推奨します。"
},
- "customUrlRtspRequired": "カスタム URL は「rtsp://」で始まる必要があります。非 RTSP カメラ ストリームの場合は手動構成が必要です。"
+ "customUrlRtspRequired": "カスタムURLは「rtsp://」または「rtsps://」で始まる必要があります。RTSP以外のカメラストリームについては、手動での設定が必要です。"
},
"docs": {
"reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras"
},
"connectionSettings": "接続設定",
- "detectionMethod": "ストリーム検出方法",
+ "detectionMethod": "ストリーム検知方法",
"onvifPort": "ONVIF ポート",
"probeMode": "カメラをプローブ",
"manualMode": "手動選択",
"useDigestAuth": "ダイジェスト認証を使用",
"useDigestAuthDescription": "ONVIF に HTTP ダイジェスト認証を使用します。一部のカメラでは、通常の管理者ユーザーではなく専用の ONVIF ユーザー名/パスワードが必要な場合があります。",
- "detectionMethodDescription": "(対応している場合)ONVIF を使用してカメラを自動設定し、カメラのストリーム URL を検出するか、カメラのブランドを手動で選択して事前定義された URL を使用します。カスタム RTSP URL を入力する場合は、手動設定を選択し、「その他」を選んでください。",
+ "detectionMethodDescription": "(対応している場合)ONVIF を使用してカメラを自動設定し、カメラのストリーム URL を検知するか、カメラのブランドを手動で選択して事前定義された URL を使用します。カスタム RTSP URL を入力する場合は、手動設定を選択し、「その他」を選んでください。",
"onvifPortDescription": "ONVIF に対応しているカメラの場合、通常は 80 または 8080 です。"
},
"step2": {
- "description": "選択した検出方法に応じて、カメラから利用可能なストリームを自動検出するか、手動で設定してください。",
+ "description": "選択した検知方法に応じて、カメラから利用可能なストリームを自動検知するか、手動で設定してください。",
"streamsTitle": "カメラストリーム",
"addStream": "ストリームを追加",
"addAnotherStream": "ストリームをさらに追加",
@@ -1132,9 +1247,9 @@
"videoCodecGood": "ビデオ コーデックは {{codec}} です。",
"audioCodecGood": "オーディオ コーデックは {{codec}} です。",
"resolutionHigh": "解像度 {{resolution}} はリソース使用量が増加する可能性があります。",
- "resolutionLow": "解像度 {{resolution}} は小さなオブジェクトを確実に検出するには低すぎる可能性があります。",
+ "resolutionLow": "解像度 {{resolution}} は小さなオブジェクトを確実に検知するには低すぎる可能性があります。",
"audioCodecRecordError": "録画で音声をサポートするには AAC オーディオ コーデックが必要です。",
- "audioCodecRequired": "音声検出をサポートするには音声ストリームが必要です。",
+ "audioCodecRequired": "音声検知をサポートするには音声ストリームが必要です。",
"restreamingWarning": "録画用ストリームでカメラへの接続数を削減すると、CPU 使用率がわずかに増加する場合があります。",
"brands": {
"reolink-rtsp": "Reolink の RTSP は推奨されません。カメラのファームウェア設定で HTTP を有効にし、ウィザードを再起動してください。",
@@ -1146,7 +1261,8 @@
"hikvision": {
"substreamWarning": "サブストリーム 1 は低解像度に固定されています。多くの Hikvision カメラは追加のサブストリームをサポートしており、カメラ設定で有効化する必要があります。利用可能であればそれらのストリームを使用することを推奨します。"
},
- "noAudioWarning": "このストリームでは音声が検出されていません。録画には音声が含まれません。"
+ "noAudioWarning": "このストリームでは音声が検知されていません。録画には音声が含まれません。",
+ "resolutionUnknown": "このストリームの解像度を取得できませんでした。設定画面または設定ファイルで検知解像度を手動で指定してください。"
},
"ffmpegModuleDescription": "何度か試してもストリームが読み込まれない場合は、このオプションを有効にしてください。有効にすると、Frigate は go2rtc と併用して ffmpeg モジュールを使用します。一部のカメラストリームでは、互換性が向上する場合があります。"
}
@@ -1159,7 +1275,30 @@
"backToSettings": "カメラ設定に戻る",
"streams": {
"title": "カメラの有効化/無効化",
- "desc": "Frigate を再起動するまで一時的にカメラを無効化します。無効化すると、このカメラのストリーム処理は完全に停止し、検出・録画・デバッグは利用できません。 注: これは go2rtc のリストリームを無効にはしません。 "
+ "desc": "Frigate を再起動するまで一時的にカメラを無効化します。無効化すると、このカメラのストリーム処理は完全に停止し、検出・録画・デバッグは利用できません。 注: これは go2rtc のリストリームを無効にはしません。 ",
+ "label": "カメラの状態",
+ "description": "各カメラの動作状態を設定します。オン : ストリームを通常通り処理します。オフ : 処理を一時停止します。Frigate の再起動後は保持されません。無効 : 処理を停止し、設定ファイルに保存されます。再有効化には再起動が必要です。注: 無効化しても go2rtc のリストリームには影響しません。 ハンドルをドラッグすると、ライブダッシュボードやカメラ選択ドロップダウンなど、UI 内のアクティブなカメラの並び順を変更できます。",
+ "disabledSubheading": "設定で無効化されています",
+ "status": {
+ "on": "オン",
+ "off": "オフ",
+ "disabled": "無効"
+ },
+ "enableSuccess": "{{cameraName}} を有効にしました。適用するには Frigate を再起動してください。",
+ "disableSuccess": "{{cameraName}} を無効化し、設定に保存しました。",
+ "reorderHandle": "ドラッグで並び替え",
+ "saving": "保存中…",
+ "saved": "保存しました",
+ "details": {
+ "edit": "カメラ詳細を編集",
+ "title": "カメラ詳細を編集",
+ "description": "このカメラの表示名と外部 URL を更新します。Frigate UI 全体で使用されます。",
+ "friendlyNameLabel": "表示名",
+ "friendlyNameHelp": "Frigate UI 全体でこのカメラに表示される名前です。空欄にするとカメラ ID が使用されます。",
+ "webuiUrlLabel": "カメラ Web UI の URL",
+ "webuiUrlHelp": "デバッグビューからカメラの Web UI に直接アクセスするための URL です。空欄にするとリンクが無効になります。",
+ "webuiUrlInvalid": "有効な URL を入力してください (例: https://example.com)。"
+ }
},
"cameraConfig": {
"add": "カメラを追加",
@@ -1189,6 +1328,117 @@
"toast": {
"success": "カメラ {{cameraName}} を保存しました"
}
+ },
+ "description": "カメラの追加・編集・削除、各カメラの状態管理、プロファイル別やカメラタイプ別の上書き設定を行えます。ストリーム、検知、モーションなどカメラ固有の設定は、カメラ設定セクション内の各項目から行ってください。",
+ "deleteCamera": "カメラを削除",
+ "deleteCameraDialog": {
+ "title": "カメラを削除",
+ "description": "カメラを削除すると、そのカメラのすべての録画、追跡オブジェクト、設定が完全に削除されます。このカメラに関連する go2rtc ストリームは手動で削除が必要な場合があります。",
+ "selectPlaceholder": "カメラを選択...",
+ "confirmTitle": "本当によろしいですか?",
+ "confirmWarning": "{{cameraName}} の削除は元に戻せません。",
+ "deleteExports": "このカメラのエクスポートも削除する",
+ "confirmButton": "完全に削除",
+ "success": "カメラ {{cameraName}} を削除しました",
+ "error": "カメラ {{cameraName}} の削除に失敗しました"
+ },
+ "profiles": {
+ "title": "プロファイル別カメラ上書き設定",
+ "selectLabel": "プロファイルを選択",
+ "description": "プロファイルが有効化されたときに、各カメラをオン/オフのどちらにするかを設定します。「継承」に設定されたカメラはデフォルトの状態を維持します。",
+ "inherit": "継承",
+ "on": "オン",
+ "off": "オフ"
+ },
+ "cameraType": {
+ "title": "カメラタイプ",
+ "label": "カメラタイプ",
+ "description": "各カメラのタイプを設定します。専用 LPR カメラは、遠方の車両のナンバープレートを捉えるために強力な光学ズームを備えた専用カメラです。LPR 専用かつナンバープレートに焦点を絞った設置でない限り、通常は「Normal」を使用してください。",
+ "normal": "通常",
+ "dedicatedLpr": "専用 LPR",
+ "saveSuccess": "{{cameraName}} のカメラタイプを更新しました。変更を適用するには Frigate を再起動してください。"
+ },
+ "clone": {
+ "sectionTitle": "設定を複製",
+ "sectionDescription": "あるカメラの設定を、別のカメラまたは新しいカメラにコピーします。",
+ "button": "設定を複製",
+ "title": "カメラ設定を複製",
+ "description": "あるカメラの設定を、1台以上の他のカメラまたは新しいカメラにコピーします。識別情報(名前、フレンドリーネーム、Web UI URL、表示順序)はコピーされません。",
+ "source": {
+ "label": "ソースカメラ",
+ "placeholder": "ソースカメラを選択",
+ "required": "ソースカメラを選択"
+ },
+ "target": {
+ "legend": "ターゲット",
+ "newRadio": "新しいカメラ",
+ "newNameLabel": "カメラ名",
+ "newNamePlaceholder": "例:back_door または Back Door",
+ "newNameRequired": "カメラ名は必須です",
+ "newNameInvalid": "無効なカメラ名です",
+ "newNameCollision": "この名前のカメラは既に存在します",
+ "newStreamsForced": "新しいカメラには、常にストリームがコピーされます。",
+ "existingCamerasRadio": "既存のカメラ",
+ "allCameras": "すべてのカメラ",
+ "existingPlaceholder": "少なくとも1台のカメラを選択",
+ "existingDisabled": "コピー先のカメラがありません"
+ },
+ "categories": {
+ "legend": "クローンする設定",
+ "description": "ソースカメラからコピーする設定を選択します。",
+ "selectAll": "すべて選択",
+ "selectNone": "選択解除",
+ "resetDefaults": "デフォルトにリセット",
+ "general": "一般",
+ "spatial": "空間設定",
+ "streams": "ストリーム",
+ "spatialWarningTitle": "解像度の不一致",
+ "spatialWarning": "ソースカメラ {{srcCamera}} の検出解像度 ({{srcWidth}}×{{srcHeight}}) は {{cameras}} と異なります。これらのカメラではポリゴンが一致しない可能性があります。これらのデフォルト設定はオフになっています。そのままコピーするには有効にしてください。",
+ "restartHint": "再起動が必要",
+ "items": {
+ "record": "録画",
+ "snapshots": "スナップショット",
+ "review": "再生",
+ "motion": "動体検知",
+ "objects": "物体",
+ "audio": "音声検知",
+ "audio_transcription": "音声文字起こし",
+ "notifications": "通知",
+ "birdseye": "バードアイ",
+ "mqtt": "MQTT",
+ "timestamp_style": "タイムスタンプ形式",
+ "onvif": "ONVIF",
+ "lpr": "ナンバープレート認識",
+ "face_recognition": "顔認識",
+ "semantic_search": "セマンティック検索",
+ "genai": "生成AI",
+ "type": "カメラタイプ(通常 / 専用LPR)",
+ "profiles": "プロファイル",
+ "detect": "サイズ検出",
+ "zones": "ゾーン",
+ "motion_mask": "動きマスク",
+ "object_masks": "オブジェクトマスク",
+ "ffmpeg_live": "ストリームURLとロール"
+ }
+ },
+ "footer": {
+ "changeCount_other": "{{count}}件の変更が適用されます",
+ "restartNeeded": "一部の変更には再起動が必要です。",
+ "liveOnly": "すべての変更は再起動なしで即時適用されます。",
+ "submit": "クローン",
+ "submitting": "クローン作成中…"
+ },
+ "toast": {
+ "success": "設定が {{cameraName}} にコピーされました",
+ "successWithRestart": "設定が {{cameraName}} にコピーされました。すべての変更を適用するには、Frigate を再起動してください。",
+ "successMulti_other": "設定が {{count}} 台のカメラにコピーされました",
+ "successMultiWithRestart_other": "設定が {{count}} 台のカメラにコピーされました。すべての変更を適用するには、Frigate を再起動してください。",
+ "partialFailure": "{{successCount}} セクションが適用されました。「{{failedSection}}」で失敗しました: {{errorMessage}}",
+ "partialFailureMulti": "{{successCount}} 台のカメラにコピーされました。{{failed}} で失敗しました: {{errorMessage}}",
+ "newCameraPartialFailure": "カメラ {{cameraName}} が作成されましたが、一部の設定のコピーに失敗しました: {{errorMessage}}",
+ "sourceMissing": "ソースカメラが存在しません",
+ "submitError": "カメラのクローン作成に失敗しました: {{errorMessage}}"
+ }
}
},
"cameraReview": {
@@ -1203,29 +1453,640 @@
},
"review": {
"title": "レビュー",
- "desc": "Frigate を再起動するまで、このカメラのアラートと検出を一時的に有効/無効にします。無効にすると、新しいレビュー項目は生成されません。 ",
+ "desc": "Frigate を再起動するまで、このカメラのアラートと検知を一時的に有効/無効にします。無効にすると、新しいレビュー項目は生成されません。 ",
"alerts": "アラート ",
- "detections": "検出 "
+ "detections": "検知 "
},
"reviewClassification": {
"title": "レビュー分類",
- "desc": "Frigate はレビュー項目をアラートと検出に分類します。既定では、すべての person と car オブジェクトはアラートとして扱われます。必須ゾーンを設定することで、分類をより細かく調整できます。",
+ "desc": "Frigate はレビュー項目をアラートと検知に分類します。既定では、すべての person と car オブジェクトはアラートとして扱われます。必須ゾーンを設定することで、分類をより細かく調整できます。",
"noDefinedZones": "このカメラにはゾーンが定義されていません。",
"objectAlertsTips": "すべての {{alertsLabels}} オブジェクトは {{cameraName}} でアラートとして表示されます。",
- "zoneObjectAlertsTips": "{{cameraName}} の {{zone}} で検出されたすべての {{alertsLabels}} オブジェクトはアラートとして表示されます。",
- "objectDetectionsTips": "{{cameraName}} で分類されていないすべての {{detectionsLabels}} オブジェクトは、どのゾーンにあっても検出として表示されます。",
+ "zoneObjectAlertsTips": "{{cameraName}} の {{zone}} で検知されたすべての {{alertsLabels}} オブジェクトはアラートとして表示されます。",
+ "objectDetectionsTips": "{{cameraName}} で分類されていないすべての {{detectionsLabels}} オブジェクトは、どのゾーンにあっても検知として表示されます。",
"zoneObjectDetectionsTips": {
- "text": "{{cameraName}} の {{zone}} で分類されていないすべての {{detectionsLabels}} オブジェクトは検出として表示されます。",
- "notSelectDetections": "{{cameraName}} の {{zone}} で検出され、アラートに分類されなかったすべての {{detectionsLabels}} オブジェクトは、ゾーンに関係なく検出として表示されます。",
- "regardlessOfZoneObjectDetectionsTips": "{{cameraName}} で分類されていないすべての {{detectionsLabels}} オブジェクトは、どのゾーンにあっても検出として表示されます。"
+ "text": "{{cameraName}} の {{zone}} で分類されていないすべての {{detectionsLabels}} オブジェクトは検知として表示されます。",
+ "notSelectDetections": "{{cameraName}} の {{zone}} で検知され、アラートに分類されなかったすべての {{detectionsLabels}} オブジェクトは、ゾーンに関係なく検知として表示されます。",
+ "regardlessOfZoneObjectDetectionsTips": "{{cameraName}} で分類されていないすべての {{detectionsLabels}} オブジェクトは、どのゾーンにあっても検知として表示されます。"
},
"unsavedChanges": "未保存のレビュー分類設定({{camera}})",
"selectAlertsZones": "アラート用のゾーンを選択",
- "selectDetectionsZones": "検出用のゾーンを選択",
- "limitDetections": "特定のゾーンに検出を限定する",
+ "selectDetectionsZones": "検知用のゾーンを選択",
+ "limitDetections": "特定のゾーンに検知を限定する",
"toast": {
"success": "レビュー分類の設定を保存しました。変更を適用するには Frigate を再起動してください。"
}
}
+ },
+ "maintenance": {
+ "sync": {
+ "status": {
+ "queued": "キューに追加済み",
+ "running": "実行中",
+ "completed": "完了",
+ "failed": "失敗",
+ "notRunning": "実行されていません"
+ },
+ "title": "メディア同期",
+ "desc": "Frigate は保持設定に従って定期的にメディアを整理します。動作中に孤立ファイルが少数発生するのは正常です。この機能を使うと、データベース上で参照されなくなった孤立メディアファイルをディスクから削除できます。",
+ "started": "メディア同期を開始しました。",
+ "alreadyRunning": "同期ジョブはすでに実行中です",
+ "error": "同期の開始に失敗しました",
+ "currentStatus": "ステータス",
+ "jobId": "ジョブ ID",
+ "startTime": "開始時刻",
+ "endTime": "終了時刻",
+ "statusLabel": "ステータス",
+ "results": "結果",
+ "errorLabel": "エラー",
+ "mediaTypes": "メディアタイプ",
+ "allMedia": "すべてのメディア",
+ "dryRun": "ドライラン",
+ "dryRunEnabled": "ファイルは削除されません",
+ "dryRunDisabled": "ファイルは削除されます",
+ "force": "強制実行",
+ "forceDesc": "安全しきい値を無視して、削除対象がファイル全体の 50% を超えても同期を完了します。",
+ "verbose": "詳細出力",
+ "verboseDesc": "孤立ファイルの全リストを確認用にディスクに書き出します。",
+ "running": "同期実行中...",
+ "start": "同期を開始",
+ "inProgress": "同期実行中です。このページは現在操作できません。",
+ "resultsFields": {
+ "filesChecked": "確認済みファイル数",
+ "orphansFound": "検知された孤立ファイル数",
+ "orphansDeleted": "削除した孤立ファイル数",
+ "aborted": "中断しました。削除が安全しきい値を超過するためです。",
+ "error": "エラー",
+ "totals": "合計"
+ },
+ "event_snapshots": "追跡オブジェクトのスナップショット",
+ "event_thumbnails": "追跡オブジェクトのサムネイル",
+ "review_thumbnails": "レビューのサムネイル",
+ "previews": "プレビュー",
+ "exports": "エクスポート",
+ "recordings": "録画"
+ },
+ "title": "メンテナンス",
+ "regionGrid": {
+ "title": "リージョングリッド",
+ "desc": "リージョングリッドは、各カメラの画角内でさまざまなサイズのオブジェクトが通常出現する位置を学習する最適化機能です。Frigate はこのデータをもとに検知領域のサイズを効率化します。グリッドは追跡オブジェクトのデータから時間をかけて自動的に構築されます。",
+ "clear": "リージョングリッドをクリア",
+ "clearConfirmTitle": "リージョングリッドをクリア",
+ "clearConfirmDesc": "リージョングリッドのクリアは、検出器のモデルサイズを最近変更した場合や、カメラの物理的な位置を変更してオブジェクト追跡に問題がある場合を除き、推奨されません。追跡が進むにつれて自動的に再構築されます。変更を反映するには Frigate の再起動が必要です。",
+ "clearSuccess": "リージョングリッドをクリアしました",
+ "clearError": "リージョングリッドのクリアに失敗しました",
+ "restartRequired": "リージョングリッドの変更を反映するには再起動が必要です"
+ }
+ },
+ "button": {
+ "overriddenGlobal": "上書き済み(グローバル)",
+ "overriddenGlobalTooltip": "このカメラは、このセクションのグローバル設定を上書きします",
+ "overriddenBaseConfig": "上書き済み(基本設定)",
+ "overriddenBaseConfigTooltip": "{{profile}} プロファイルは、このセクションの設定を上書きします",
+ "overriddenInCameras": {
+ "label_other": "{{count}} 台のカメラで上書きされました",
+ "tooltip_other": "{{count}} 台のカメラがこのセクションの設定値を上書きしています。詳細を表示するにはクリックしてください。",
+ "heading_other": "このグローバルセクションには、{{count}} 台のカメラで上書きされているフィールドがあります。",
+ "othersField_other": "{{count}} その他",
+ "profilePrefix": "{{profile}} プロファイル: {{fields}}"
+ },
+ "overriddenGlobalHeading_other": "このカメラはグローバル設定のうち {{count}} 項目を上書きしています:",
+ "overriddenGlobalNoDeltas": "このカメラはグローバル設定を上書きしていますが、値が異なる項目はありません。",
+ "overriddenBaseConfigHeading_other": "{{profile}} プロファイルはベース設定のうち {{count}} 項目を上書きしています:",
+ "overriddenBaseConfigNoDeltas": "{{profile}} プロファイルはこのセクションを上書きしていますが、ベース設定と異なる値はありません。"
+ },
+ "menuDot": {
+ "overrideGlobal": "このセクションはグローバル設定を上書きします",
+ "overrideProfile": "このセクションは {{profile}} プロファイルによって上書きされます",
+ "unsaved": "このセクションには未保存の変更があります"
+ },
+ "saveAllPreview": {
+ "title": "保存対象の変更",
+ "triggerLabel": "保留中の変更を確認",
+ "empty": "保留中の変更はありません。",
+ "scope": {
+ "label": "対象範囲",
+ "global": "グローバル",
+ "camera": "カメラ: {{cameraName}}"
+ },
+ "profile": {
+ "label": "プロファイル"
+ },
+ "field": {
+ "label": "項目"
+ },
+ "value": {
+ "label": "新しい値",
+ "reset": "リセット"
+ }
+ },
+ "timestampPosition": {
+ "tl": "左上",
+ "tr": "右上",
+ "bl": "左下",
+ "br": "右下"
+ },
+ "detectorsAndModel": {
+ "title": "検出器とモデル",
+ "description": "物体検知を実行する検出器のバックエンドと、使用するモデルを設定します。検出器とモデルが同期するよう、変更は一括で保存されます。",
+ "cardTitles": {
+ "detector": "検出器ハードウェア",
+ "model": "検知モデル"
+ },
+ "tabs": {
+ "plus": "Frigate+",
+ "custom": "カスタムモデル"
+ },
+ "mismatch": {
+ "warning": "現在の Frigate+ モデル「{{model}}」は {{required}} 検出器が必要です。下から対応するモデルを選ぶか、保存前にカスタムモデルへ切り替えてください。"
+ },
+ "plusModel": {
+ "requiresDetector": "必要な検出器: {{detector}}",
+ "noModelSelected": "Frigate+ モデルを選択"
+ },
+ "toast": {
+ "saveSuccess": "検出器とモデルの設定を保存しました。変更を適用するには Frigate を再起動してください。",
+ "saveError": "検出器とモデルの設定の保存に失敗しました"
+ },
+ "unsavedChanges": "検出器・モデルに未保存の変更があります",
+ "restartRequired": "再起動が必要です(検出器またはモデルが変更されました)"
+ },
+ "configForm": {
+ "global": {
+ "title": "グローバル設定",
+ "description": "これらの設定は、カメラ固有の設定で上書きされない限り、全カメラに適用されます。"
+ },
+ "camera": {
+ "title": "カメラ設定",
+ "description": "これらの設定はこのカメラにのみ適用され、グローバル設定を上書きします。",
+ "noCameras": "利用可能なカメラがありません"
+ },
+ "advancedSettingsCount": "詳細設定 ({{count}})",
+ "advancedCount": "詳細 ({{count}})",
+ "showAdvanced": "詳細設定を表示",
+ "tabs": {
+ "sharedDefaults": "共通デフォルト",
+ "system": "システム",
+ "integrations": "連携"
+ },
+ "additionalProperties": {
+ "keyLabel": "キー",
+ "valueLabel": "値",
+ "keyPlaceholder": "新しいキー",
+ "remove": "削除"
+ },
+ "knownPlates": {
+ "namePlaceholder": "例: 妻の車",
+ "platePlaceholder": "ナンバーまたは正規表現"
+ },
+ "timezone": {
+ "defaultOption": "ブラウザのタイムゾーンを使用"
+ },
+ "roleMap": {
+ "empty": "ロールマッピングがありません",
+ "roleLabel": "ロール",
+ "groupsLabel": "グループ",
+ "addMapping": "ロールマッピングを追加",
+ "remove": "削除"
+ },
+ "ffmpegArgs": {
+ "preset": "プリセット",
+ "manual": "手動で引数指定",
+ "inherit": "カメラ設定から継承",
+ "none": "なし",
+ "useGlobalSetting": "グローバル設定から継承",
+ "selectPreset": "プリセットを選択",
+ "manualPlaceholder": "FFmpeg 引数を入力",
+ "presetLabels": {
+ "preset-rpi-64-h264": "Raspberry Pi (H.264)",
+ "preset-rpi-64-h265": "Raspberry Pi (H.265)",
+ "preset-vaapi": "VAAPI (Intel/AMD GPU)",
+ "preset-intel-qsv-h264": "Intel QuickSync (H.264)",
+ "preset-intel-qsv-h265": "Intel QuickSync (H.265)",
+ "preset-nvidia": "NVIDIA GPU",
+ "preset-jetson-h264": "NVIDIA Jetson (H.264)",
+ "preset-jetson-h265": "NVIDIA Jetson (H.265)",
+ "preset-rkmpp": "Rockchip RKMPP",
+ "preset-http-jpeg-generic": "HTTP JPEG (汎用)",
+ "preset-http-mjpeg-generic": "HTTP MJPEG (汎用)",
+ "preset-http-reolink": "HTTP - Reolink カメラ",
+ "preset-rtmp-generic": "RTMP (汎用)",
+ "preset-rtsp-generic": "RTSP (汎用)",
+ "preset-rtsp-restream": "RTSP - go2rtc からのリストリーム",
+ "preset-rtsp-restream-low-latency": "RTSP - go2rtc からのリストリーム (低遅延)",
+ "preset-rtsp-udp": "RTSP - UDP",
+ "preset-rtsp-blue-iris": "RTSP - Blue Iris",
+ "preset-record-generic": "録画 (汎用、音声なし)",
+ "preset-record-generic-audio-copy": "録画 (汎用 + 音声コピー)",
+ "preset-record-generic-audio-aac": "録画 (汎用 + 音声を AAC に変換)",
+ "preset-record-mjpeg": "録画 - MJPEG カメラ",
+ "preset-record-jpeg": "録画 - JPEG カメラ",
+ "preset-record-ubiquiti": "録画 - Ubiquiti カメラ"
+ }
+ },
+ "cameraInputs": {
+ "itemTitle": "ストリーム {{index}}"
+ },
+ "restartRequiredField": "再起動が必要",
+ "restartRequiredFooter": "設定が変更されました - 再起動が必要です",
+ "sections": {
+ "detect": "検知",
+ "record": "録画",
+ "snapshots": "スナップショット",
+ "motion": "モーション",
+ "objects": "オブジェクト",
+ "review": "レビュー",
+ "audio": "音声",
+ "notifications": "通知",
+ "live": "ライブビュー",
+ "timestamp_style": "タイムスタンプ",
+ "mqtt": "MQTT",
+ "database": "データベース",
+ "telemetry": "テレメトリ",
+ "auth": "認証",
+ "tls": "TLS",
+ "proxy": "プロキシ",
+ "go2rtc": "go2rtc",
+ "ffmpeg": "FFmpeg",
+ "detectors": "検出器",
+ "model": "モデル",
+ "semantic_search": "セマンティック検索",
+ "genai": "生成AI",
+ "face_recognition": "顔認識",
+ "lpr": "ナンバープレート認識",
+ "birdseye": "バードアイ",
+ "masksAndZones": "マスク / ゾーン"
+ },
+ "detect": {
+ "title": "検知設定"
+ },
+ "detectors": {
+ "title": "検出器設定",
+ "singleType": "{{type}} 検出器は 1 つしか追加できません。",
+ "keyRequired": "検出器名は必須です。",
+ "keyDuplicate": "この検出器名は既に存在します。",
+ "noSchema": "利用可能な検出器スキーマがありません。",
+ "none": "検出器インスタンスが設定されていません。",
+ "add": "検出器を追加",
+ "addCustomKey": "カスタムキーを追加"
+ },
+ "record": {
+ "title": "録画設定"
+ },
+ "snapshots": {
+ "title": "スナップショット設定"
+ },
+ "motion": {
+ "title": "モーション設定"
+ },
+ "objects": {
+ "title": "オブジェクト設定"
+ },
+ "audioLabels": {
+ "summary": "{{count}} 件の音声ラベルが選択されています",
+ "empty": "利用可能な音声ラベルがありません"
+ },
+ "objectLabels": {
+ "summary": "{{count}} 種類のオブジェクトタイプが選択されています",
+ "empty": "利用可能なオブジェクトラベルがありません"
+ },
+ "reviewLabels": {
+ "summary": "{{count}} 件のラベルが選択されています",
+ "empty": "利用可能なラベルがありません"
+ },
+ "filters": {
+ "objectFieldLabel": "{{label}} の {{field}}"
+ },
+ "zoneNames": {
+ "summary": "{{count}} 件選択",
+ "empty": "利用可能なゾーンがありません"
+ },
+ "inputRoles": {
+ "summary": "{{count}} 件のロールが選択されています",
+ "empty": "利用可能なロールがありません",
+ "options": {
+ "detect": "検知",
+ "record": "録画",
+ "audio": "音声"
+ }
+ },
+ "genaiRoles": {
+ "options": {
+ "embeddings": "埋め込み",
+ "descriptions": "説明",
+ "chat": "チャット"
+ }
+ },
+ "semanticSearchModel": {
+ "placeholder": "モデルを選択…",
+ "builtIn": "組み込みモデル",
+ "genaiProviders": "生成AIプロバイダ"
+ },
+ "semanticSearchModelSize": {
+ "notApplicable": "生成AIプロバイダには適用されません"
+ },
+ "review": {
+ "title": "レビュー設定"
+ },
+ "audio": {
+ "title": "音声設定"
+ },
+ "notifications": {
+ "title": "通知設定"
+ },
+ "live": {
+ "title": "ライブビュー設定"
+ },
+ "timestamp_style": {
+ "title": "タイムスタンプ設定"
+ },
+ "searchPlaceholder": "検索...",
+ "addCustomLabel": "カスタムラベルを追加...",
+ "genaiModel": {
+ "placeholder": "モデルを選択または入力…",
+ "search": "モデルを検索または入力…",
+ "noModels": "利用可能なモデルがありません",
+ "available": "利用可能なモデル",
+ "useCustom": "「{{value}}」を使用",
+ "refresh": "モデルを更新",
+ "probeFailed": "モデルの取得に失敗しました",
+ "fetchedModels": "モデル一覧を取得しました"
+ },
+ "liveStreams": {
+ "streamNameLabel": "ストリーム名",
+ "streamNamePlaceholder": "例: Main HD Stream",
+ "go2rtcStreamLabel": "go2rtc ストリーム",
+ "go2rtcStreamPlaceholder": "go2rtc ストリームを選択してください",
+ "go2rtcStreamSearch": "ストリーム名を検索または入力してください…",
+ "noGo2rtcStreams": "設定済みの go2rtc ストリームはありません",
+ "availableStreams": "利用可能なストリーム",
+ "useCustom": "\"{{value}}\"を使用",
+ "addStream": "ストリームを追加"
+ },
+ "ptzPresets": {
+ "placeholder": "プリセットを選択または入力してください...",
+ "search": "検索またはプリセットを入力してください...",
+ "noPresets": "プリセットはありません",
+ "available": "カメラプリセット",
+ "useCustom": "\"{{value}}\" を使用してください"
+ },
+ "defaultRole": {
+ "admin": "管理者",
+ "viewer": "閲覧者"
+ }
+ },
+ "globalConfig": {
+ "title": "グローバル設定",
+ "description": "上書きされない限り全カメラに適用されるグローバル設定を行います。",
+ "toast": {
+ "success": "グローバル設定を保存しました",
+ "error": "グローバル設定の保存に失敗しました",
+ "validationError": "入力検証に失敗しました"
+ }
+ },
+ "cameraConfig": {
+ "title": "カメラ設定",
+ "description": "個別のカメラの設定を行います。グローバルのデフォルトを上書きします。",
+ "overriddenBadge": "上書き済み",
+ "resetToGlobal": "グローバル設定にリセット",
+ "toast": {
+ "success": "カメラ設定を保存しました",
+ "error": "カメラ設定の保存に失敗しました"
+ }
+ },
+ "toast": {
+ "success": "設定を保存しました",
+ "applied": "設定を適用しました",
+ "successRestartRequired": "設定を保存しました。変更を適用するには Frigate を再起動してください。",
+ "error": "設定の保存に失敗しました",
+ "validationError": "入力検証に失敗しました: {{message}}",
+ "resetSuccess": "グローバルのデフォルトにリセットしました",
+ "resetError": "設定のリセットに失敗しました",
+ "saveAllSuccess_other": "{{count}} 件のセクションをすべて保存しました。",
+ "saveAllSuccessRestartRequired_other": "{{count}} 件のセクションをすべて保存しました。変更を適用するには Frigate を再起動してください。",
+ "saveAllPartial_other": "{{totalCount}} 件中 {{successCount}} 件のセクションを保存しました。{{failCount}} 件失敗。",
+ "saveAllFailure": "すべてのセクションの保存に失敗しました。"
+ },
+ "profiles": {
+ "title": "プロファイル",
+ "activeProfile": "アクティブなプロファイル",
+ "noActiveProfile": "アクティブなプロファイルなし",
+ "active": "アクティブ",
+ "activated": "プロファイル「{{profile}}」を有効化しました",
+ "activateFailed": "プロファイルの設定に失敗しました",
+ "deactivated": "プロファイルを無効化しました",
+ "noProfiles": "プロファイルが定義されていません。",
+ "noOverrides": "上書きなし",
+ "cameraCount_other": "{{count}} 台のカメラ",
+ "columnCamera": "カメラ",
+ "columnOverrides": "プロファイルの上書き",
+ "baseConfig": "ベース設定",
+ "addProfile": "プロファイルを追加",
+ "newProfile": "新しいプロファイル",
+ "profileNamePlaceholder": "例: 在宅、外出、夜間モード",
+ "friendlyNameLabel": "プロファイル名",
+ "profileIdLabel": "プロファイル ID",
+ "profileIdDescription": "設定や自動化で使用される内部 ID です",
+ "nameInvalid": "使用できるのは小文字、数字、アンダースコアのみです",
+ "nameDuplicate": "この名前のプロファイルは既に存在します",
+ "error": {
+ "mustBeAtLeastTwoCharacters": "2 文字以上で入力してください",
+ "mustNotContainPeriod": "ピリオドは使用できません",
+ "alreadyExists": "この ID のプロファイルは既に存在します"
+ },
+ "renameProfile": "プロファイル名を変更",
+ "renameSuccess": "プロファイル名を「{{profile}}」に変更しました",
+ "deleteProfile": "プロファイルを削除",
+ "deleteProfileConfirm": "プロファイル「{{profile}}」を全カメラから削除しますか?この操作は元に戻せません。",
+ "deleteSuccess": "プロファイル「{{profile}}」を削除しました",
+ "createSuccess": "プロファイル「{{profile}}」を作成しました",
+ "removeOverride": "プロファイル上書きを解除",
+ "deleteSection": "セクション上書きを削除",
+ "deleteSectionConfirm": "{{camera}} の {{profile}} プロファイルから {{section}} の上書きを削除しますか?",
+ "deleteSectionSuccess": "{{profile}} の {{section}} 上書きを削除しました",
+ "enableSwitch": "プロファイルを有効にする",
+ "enabledDescription": "プロファイルが有効化されています。下から新しいプロファイルを作成し、カメラ設定セクションで変更を加えて保存すると反映されます。",
+ "disabledDescription": "プロファイルを使うと、名前付きのカメラ設定上書きセット(例: 在宅、外出、夜間)を定義し、必要に応じて有効化できます。"
+ },
+ "unsavedChanges": "未保存の変更があります",
+ "confirmReset": "リセットの確認",
+ "resetToDefaultDescription": "このセクションのすべての設定をデフォルト値にリセットします。この操作は元に戻せません。",
+ "resetToGlobalDescription": "このセクションの設定をグローバルのデフォルトにリセットします。この操作は元に戻せません。",
+ "go2rtcStreams": {
+ "title": "go2rtc ストリーム",
+ "description": "カメラのリストリーム用に go2rtc のストリーム設定を管理します。各ストリームは名前と 1 つ以上のソース URL を持ちます。",
+ "addStream": "ストリームを追加",
+ "addStreamDesc": "新しいストリームの名前を入力してください。この名前はカメラ設定でストリームを参照する際に使用されます。",
+ "addUrl": "URL を追加",
+ "streamNumber": "ストリーム {{index}}",
+ "streamName": "ストリーム名",
+ "streamNamePlaceholder": "例: front_door",
+ "streamUrlPlaceholder": "例: rtsp://user:pass@192.168.1.100/stream",
+ "deleteStream": "ストリームを削除",
+ "deleteStreamConfirm": "ストリーム「{{streamName}}」を削除してもよろしいですか?このストリームを参照しているカメラは動作しなくなる可能性があります。",
+ "noStreams": "go2rtc ストリームが設定されていません。ストリームを追加して始めてください。",
+ "validation": {
+ "nameRequired": "ストリーム名は必須です",
+ "nameDuplicate": "この名前のストリームは既に存在します",
+ "nameInvalid": "ストリーム名には英字、数字、アンダースコア、ハイフンのみ使用できます",
+ "urlRequired": "URL を少なくとも 1 つ指定してください"
+ },
+ "renameStream": "ストリーム名を変更",
+ "renameStreamDesc": "このストリームの新しい名前を入力してください。名前を変更すると、この名前で参照しているカメラや他のストリームが動作しなくなる可能性があります。",
+ "newStreamName": "新しいストリーム名",
+ "ffmpeg": {
+ "useFfmpegModule": "互換モード (ffmpeg) を使用",
+ "video": "映像",
+ "audio": "音声",
+ "hardware": "ハードウェアアクセラレーション",
+ "videoCopy": "コピー",
+ "videoH264": "H.264 にトランスコード",
+ "videoH265": "H.265 にトランスコード",
+ "videoExclude": "除外",
+ "audioCopy": "コピー",
+ "audioAac": "AAC にトランスコード",
+ "audioOpus": "Opus にトランスコード",
+ "audioPcmu": "PCM μ-law にトランスコード",
+ "audioPcma": "PCM A-law にトランスコード",
+ "audioPcm": "PCM にトランスコード",
+ "audioMp3": "MP3 にトランスコード",
+ "audioExclude": "除外",
+ "hardwareNone": "ハードウェアアクセラレーションなし",
+ "hardwareAuto": "自動 (推奨)",
+ "hardwareVaapi": "VAAPI",
+ "hardwareCuda": "CUDA",
+ "hardwareV4l2m2m": "V4L2 M2M",
+ "hardwareDxva2": "DXVA2",
+ "hardwareVideotoolbox": "VideoToolbox",
+ "addVideoCodec": "ビデオコーデックを追加",
+ "addAudioCodec": "音声コーデックを追加",
+ "removeCodec": "コーデックを削除"
+ },
+ "sourceNumber": "ソース {{index}}"
+ },
+ "birdseye": {
+ "trackingMode": {
+ "objects": "オブジェクト",
+ "motion": "モーション",
+ "continuous": "常時"
+ },
+ "cameraOrder": {
+ "label": "カメラ順序",
+ "description": "カメラをドラッグしてバードアイレイアウト内の順序を設定します。",
+ "reorderHandle": "ドラッグで並び替え",
+ "saving": "保存中…",
+ "saved": "保存しました"
+ }
+ },
+ "retainMode": {
+ "all": "すべて",
+ "motion": "モーション",
+ "active_objects": "アクティブなオブジェクト"
+ },
+ "previewQuality": {
+ "very_high": "非常に高",
+ "high": "高",
+ "medium": "中",
+ "low": "低",
+ "very_low": "非常に低"
+ },
+ "ui": {
+ "timeFormat": {
+ "browser": "ブラウザ設定",
+ "12hour": "12 時間表示",
+ "24hour": "24 時間表示"
+ },
+ "TimeOrDateStyle": {
+ "full": "完全表示",
+ "long": "長い形式",
+ "medium": "中程度",
+ "short": "短い形式"
+ },
+ "unitSystem": {
+ "metric": "メートル法",
+ "imperial": "ヤード・ポンド法"
+ }
+ },
+ "review": {
+ "imageSource": {
+ "recordings": "録画",
+ "previews": "プレビュー"
+ }
+ },
+ "logger": {
+ "logLevel": {
+ "debug": "デバッグ",
+ "info": "情報",
+ "warning": "警告",
+ "error": "エラー",
+ "critical": "重大"
+ }
+ },
+ "onvif": {
+ "profileAuto": "自動",
+ "profileLoading": "プロファイルを読み込み中...",
+ "autotracking": {
+ "zooming": {
+ "disabled": "無効",
+ "absolute": "絶対値",
+ "relative": "相対値"
+ }
+ }
+ },
+ "modelSize": {
+ "small": "小",
+ "large": "大"
+ },
+ "configMessages": {
+ "review": {
+ "recordDisabled": "録画が無効化されているため、レビュー項目は生成されません。",
+ "detectDisabled": "物体検知が無効化されています。レビュー項目はアラートと検知を分類するために検知オブジェクトが必要です。",
+ "allNonAlertDetections": "アラート以外のすべてのアクティビティが検知として含まれます。",
+ "genaiImageSourceRecordingsRecordDisabled": "画像ソースが「録画」に設定されていますが、録画は無効化されています。Frigate はプレビュー画像を代わりに使用します。"
+ },
+ "audio": {
+ "noAudioRole": "audio ロールが定義されたストリームがありません。音声検知を機能させるには audio ロールを有効にする必要があります。"
+ },
+ "audioTranscription": {
+ "audioDetectionDisabled": "このカメラでは音声検知が有効化されていません。音声文字起こしには音声検知が必要です。"
+ },
+ "detect": {
+ "fpsGreaterThanFive": "検知 FPS を 5 より高く設定することは推奨されません。値を大きくしてもパフォーマンス上の問題を引き起こすだけで、メリットはありません。",
+ "disabled": "物体検知が無効化されています。スナップショット、レビュー項目、顔認識、ナンバープレート認識、生成AI などのエンリッチメントは機能しません。",
+ "resolutionShouldBeMultipleOfFour": "最良の結果を得るため、検知の幅と高さは 4 の倍数にしてください。他の偶数値でも動作しますが、検知ストリームに視覚的なノイズや軽微な歪みが生じる可能性があります。",
+ "aspectRatioMismatch": "入力した幅と高さは現在の検知解像度のアスペクト比と一致していません。映像が引き伸ばされたり歪んだりする可能性があります。"
+ },
+ "objects": {
+ "genaiNoDescriptionsProvider": "説明を生成するには「descriptions」ロールを持つ生成AIプロバイダを設定する必要があります。"
+ },
+ "faceRecognition": {
+ "globalDisabled": "このカメラで顔認識機能を使うには、顔認識のエンリッチメントを有効にする必要があります。",
+ "personNotTracked": "顔認識には「person」オブジェクトの追跡が必要です。このカメラのオブジェクト設定で「person」を有効にしてください。",
+ "modelSizeLarge": "「large」モデルは合理的な性能を発揮するために GPU または NPU が必要です。CPU のみのシステムでは「small」を使用してください。"
+ },
+ "lpr": {
+ "globalDisabled": "このカメラで LPR 機能を使うには、ナンバープレート認識のエンリッチメントを有効にする必要があります。",
+ "vehicleNotTracked": "ナンバープレート認識には「car」または「motorcycle」の追跡が必要です。このカメラのオブジェクト設定でいずれかを有効にしてください。",
+ "modelSizeLarge": "「large」モデルは複数行のナンバープレート向けに最適化されています。お住まいの地域で複数行プレートが使われていない限り、「small」モデルの方が性能が良いためそちらを使用してください。"
+ },
+ "record": {
+ "noRecordRole": "record ロールが定義されたストリームがありません。録画は機能しません。"
+ },
+ "birdseye": {
+ "objectsModeDetectDisabled": "バードアイが「objects」モードに設定されていますが、このカメラの物体検知は無効化されています。このカメラはバードアイに表示されません。"
+ },
+ "snapshots": {
+ "detectDisabled": "物体検知が無効化されています。スナップショットは追跡オブジェクトから生成されるため、作成されません。"
+ },
+ "detectors": {
+ "mixedTypes": "すべての検出器は同じタイプである必要があります。別のタイプを使うには既存の検出器を削除してください。",
+ "mixedTypesSuggestion": "すべての検出器は同じタイプである必要があります。既存の検出器を削除するか、{{type}} を選択してください。"
+ },
+ "semanticSearch": {
+ "jinav2SmallModelSize": "Jina V2 モデルの「small」サイズは RAM と推論コストが高くなります。専用 GPU と「large」モデルの組み合わせを推奨します。"
+ },
+ "onvif": {
+ "autotrackingNoZones": "オートトラッキング機能を使用するには、少なくとも1つのゾーンが必要です。「マスク / ゾーン」でこのカメラ用のゾーンを定義し、以下でそれを必須ゾーンとして設定してください。"
+ }
}
}
diff --git a/web/public/locales/ja/views/system.json b/web/public/locales/ja/views/system.json
index d3f8f88a71..2971039638 100644
--- a/web/public/locales/ja/views/system.json
+++ b/web/public/locales/ja/views/system.json
@@ -7,7 +7,8 @@
"logs": {
"frigate": "Frigate ログ - Frigate",
"go2rtc": "Go2RTC ログ - Frigate",
- "nginx": "Nginx ログ - Frigate"
+ "nginx": "Nginx ログ - Frigate",
+ "websocket": "メッセージログ - Frigate"
}
},
"title": "システム",
@@ -22,7 +23,7 @@
"error": "ログをクリップボードにコピーできませんでした"
},
"type": {
- "label": "種類",
+ "label": "タイプ",
"timestamp": "タイムスタンプ",
"tag": "タグ",
"message": "メッセージ"
@@ -42,7 +43,23 @@
"filter": {
"events": "イベント",
"classification": "分類",
- "face_recognition": "顔認識"
+ "face_recognition": "顔認識",
+ "all": "全てのトピックス",
+ "topics": "トピックス",
+ "reviews": "レビュー",
+ "lpr": "LPR",
+ "camera_activity": "カメラアクティビティ",
+ "system": "システム",
+ "camera": "カメラ",
+ "all_cameras": "全てのカメラ",
+ "cameras_count_one": "{{count}} カメラ",
+ "cameras_count_other": "{{count}} カメラ"
+ },
+ "empty": "まだメッセージは記録されていません",
+ "count_one": "{{count}} メッセージ",
+ "count_other": "{{count}} メッセージ",
+ "expanded": {
+ "payload": "ペイロード"
}
}
},
@@ -53,7 +70,7 @@
"inferenceSpeed": "ディテクタ推論速度",
"temperature": "ディテクタ温度",
"cpuUsage": "ディテクタの CPU 使用率",
- "cpuUsageInformation": "検出モデルへの入力/出力データの準備に使用される CPU。GPU やアクセラレータを使用していても、この値は推論の使用量を測定しません。",
+ "cpuUsageInformation": "検知モデルへの入力/出力データの準備に使用される CPU。GPU やアクセラレータを使用していても、この値は推論の使用量を測定しません。",
"memoryUsage": "ディテクタのメモリ使用量"
},
"hardwareInfo": {
@@ -91,8 +108,11 @@
"intelGpuWarning": {
"title": "Intel GPU 統計情報の警告",
"message": "GPU の統計情報を取得できません",
- "description": "これは Intel の GPU 統計取得ツール(intel_gpu_top)における既知の不具合です。ハードウェアアクセラレーションやオブジェクト検出が (i)GPU 上で正しく動作している場合でも、GPU 使用率が 0% と繰り返し表示されることがあります。これは Frigate の不具合ではありません。ホストを再起動することで一時的に解消し、GPU が正常に動作していることを確認できます。本問題はパフォーマンスには影響しません。"
- }
+ "description": "これは Intel の GPU 統計取得ツール(intel_gpu_top)における既知の不具合です。ハードウェアアクセラレーションやオブジェクト検知が (i)GPU 上で正しく動作している場合でも、GPU 使用率が 0% と繰り返し表示されることがあります。これは Frigate の不具合ではありません。ホストを再起動することで一時的に解消し、GPU が正常に動作していることを確認できます。本問題はパフォーマンスには影響しません。"
+ },
+ "gpuCompute": "GPU 演算 / エンコード",
+ "gpuTemperature": "GPU 温度",
+ "npuTemperature": "NPU 温度"
},
"otherProcesses": {
"title": "その他のプロセス",
@@ -117,7 +137,11 @@
},
"shm": {
"title": "SHM(共有メモリ)の割り当て",
- "warning": "現在の SHM サイズ {{total}}MB は小さすぎます。少なくとも {{min_shm}}MB に増やしてください。"
+ "warning": "現在の SHM サイズ {{total}}MB は小さすぎます。少なくとも {{min_shm}}MB に増やしてください。",
+ "frameLifetime": {
+ "title": "フレーム保持時間",
+ "description": "各カメラは共有メモリ内に {{frames}} 個のフレームスロットを持ちます。最も高速なカメラのフレームレートでは、各フレームは上書きされるまで約 {{lifetime}} 秒間利用可能です。"
+ }
},
"cameraStorage": {
"title": "カメラストレージ",
@@ -152,22 +176,23 @@
"title": "カメラプローブ情報"
}
},
- "framesAndDetections": "フレーム / 検出",
+ "framesAndDetections": "フレーム / 検知",
"label": {
"camera": "カメラ",
- "detect": "検出",
+ "detect": "検知",
"skipped": "スキップ",
"ffmpeg": "FFmpeg",
"capture": "キャプチャ",
"overallFramesPerSecond": "全体フレーム/秒",
- "overallDetectionsPerSecond": "全体検出/秒",
- "overallSkippedDetectionsPerSecond": "全体スキップ検出/秒",
+ "overallDetectionsPerSecond": "全体検知/秒",
+ "overallSkippedDetectionsPerSecond": "全体スキップ検知/秒",
"cameraFfmpeg": "{{camName}} FFmpeg",
"cameraCapture": "{{camName}} キャプチャ",
- "cameraDetect": "{{camName}} 検出",
+ "cameraDetect": "{{camName}} 検知",
"cameraFramesPerSecond": "{{camName}} フレーム/秒",
- "cameraDetectionsPerSecond": "{{camName}} 検出/秒",
- "cameraSkippedDetectionsPerSecond": "{{camName}} スキップ検出/秒"
+ "cameraDetectionsPerSecond": "{{camName}} 検知/秒",
+ "cameraSkippedDetectionsPerSecond": "{{camName}} スキップ検知/秒",
+ "cameraGpu": "{{camName}} GPU"
},
"toast": {
"success": {
@@ -176,18 +201,33 @@
"error": {
"unableToProbeCamera": "カメラをプローブできません: {{errorMessage}}"
}
+ },
+ "noCameras": {
+ "title": "カメラが見つかりません"
+ },
+ "connectionQuality": {
+ "title": "接続品質",
+ "excellent": "非常に良好",
+ "fair": "普通",
+ "poor": "不良",
+ "unusable": "使用不可",
+ "fps": "FPS",
+ "expectedFps": "想定 FPS",
+ "reconnectsLastHour": "再接続回数 (直近1時間)",
+ "stallsLastHour": "ストール回数 (直近1時間)"
}
},
"lastRefreshed": "最終更新: ",
"stats": {
"ffmpegHighCpuUsage": "{{camera}} の FFmpeg の CPU 使用率が高い({{ffmpegAvg}}%)",
- "detectHighCpuUsage": "{{camera}} の検出の CPU 使用率が高い({{detectAvg}}%)",
+ "detectHighCpuUsage": "{{camera}} の検知の CPU 使用率が高い({{detectAvg}}%)",
"healthy": "システムは正常です",
"reindexingEmbeddings": "埋め込みを再インデックス中({{processed}}% 完了)",
"cameraIsOffline": "{{camera}} はオフラインです",
"detectIsSlow": "{{detect}} が遅い({{speed}} ms)",
"detectIsVerySlow": "{{detect}} が非常に遅い({{speed}} ms)",
- "shmTooLow": "/dev/shm の割り当て({{total}} MB)は少なくとも {{min}} MB に増やす必要があります。"
+ "shmTooLow": "/dev/shm の割り当て({{total}} MB)は少なくとも {{min}} MB に増やす必要があります。",
+ "debugReplayActive": "デバッグリプレイセッションが実行中です"
},
"enrichments": {
"title": "高度解析",
@@ -202,8 +242,8 @@
"face_recognition_speed": "顔認識速度",
"plate_recognition_speed": "ナンバープレート認識速度",
"text_embedding_speed": "テキスト埋め込み速度",
- "yolov9_plate_detection_speed": "YOLOv9 ナンバープレート検出速度",
- "yolov9_plate_detection": "YOLOv9 ナンバープレート検出",
+ "yolov9_plate_detection_speed": "YOLOv9 ナンバープレート検知速度",
+ "yolov9_plate_detection": "YOLOv9 ナンバープレート検知",
"review_description": "レビュー説明",
"review_description_speed": "レビュー説明の処理速度",
"review_description_events_per_second": "レビュー説明",
diff --git a/web/public/locales/km/audio.json b/web/public/locales/km/audio.json
new file mode 100644
index 0000000000..3070081d6e
--- /dev/null
+++ b/web/public/locales/km/audio.json
@@ -0,0 +1,7 @@
+{
+ "speech": "ការនិយាយ",
+ "babbling": "សំឡេងរំខាន",
+ "yell": "ស្រែក",
+ "bellow": "ប៊ែលឡូវ",
+ "whoop": "អូប"
+}
diff --git a/web/public/locales/km/common.json b/web/public/locales/km/common.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/common.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/components/auth.json b/web/public/locales/km/components/auth.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/components/auth.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/components/camera.json b/web/public/locales/km/components/camera.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/components/camera.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/components/dialog.json b/web/public/locales/km/components/dialog.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/components/dialog.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/components/filter.json b/web/public/locales/km/components/filter.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/components/filter.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/components/icons.json b/web/public/locales/km/components/icons.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/components/icons.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/components/input.json b/web/public/locales/km/components/input.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/components/input.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/components/player.json b/web/public/locales/km/components/player.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/components/player.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/config/cameras.json b/web/public/locales/km/config/cameras.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/config/cameras.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/config/global.json b/web/public/locales/km/config/global.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/config/global.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/config/groups.json b/web/public/locales/km/config/groups.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/config/groups.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/config/validation.json b/web/public/locales/km/config/validation.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/config/validation.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/objects.json b/web/public/locales/km/objects.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/objects.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/views/chat.json b/web/public/locales/km/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/views/classificationModel.json b/web/public/locales/km/views/classificationModel.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/views/classificationModel.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/views/configEditor.json b/web/public/locales/km/views/configEditor.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/views/configEditor.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/views/events.json b/web/public/locales/km/views/events.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/views/events.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/views/explore.json b/web/public/locales/km/views/explore.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/views/explore.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/views/exports.json b/web/public/locales/km/views/exports.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/views/exports.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/views/faceLibrary.json b/web/public/locales/km/views/faceLibrary.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/views/faceLibrary.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/views/live.json b/web/public/locales/km/views/live.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/views/live.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/views/motionSearch.json b/web/public/locales/km/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/views/recording.json b/web/public/locales/km/views/recording.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/views/recording.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/views/replay.json b/web/public/locales/km/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/views/search.json b/web/public/locales/km/views/search.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/views/search.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/views/settings.json b/web/public/locales/km/views/settings.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/views/settings.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/km/views/system.json b/web/public/locales/km/views/system.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/km/views/system.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/kn/audio.json b/web/public/locales/kn/audio.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/kn/audio.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/kn/common.json b/web/public/locales/kn/common.json
new file mode 100644
index 0000000000..9b5e63fafd
--- /dev/null
+++ b/web/public/locales/kn/common.json
@@ -0,0 +1,5 @@
+{
+ "time": {
+ "untilForTime": "{{time}}ವರೆಗೆ"
+ }
+}
diff --git a/web/public/locales/kn/components/auth.json b/web/public/locales/kn/components/auth.json
new file mode 100644
index 0000000000..6d4a2c27fe
--- /dev/null
+++ b/web/public/locales/kn/components/auth.json
@@ -0,0 +1,5 @@
+{
+ "form": {
+ "user": "ಬಳಕೆದಾರರಹೆಸರು"
+ }
+}
diff --git a/web/public/locales/kn/components/camera.json b/web/public/locales/kn/components/camera.json
new file mode 100644
index 0000000000..71a4f6946b
--- /dev/null
+++ b/web/public/locales/kn/components/camera.json
@@ -0,0 +1,5 @@
+{
+ "group": {
+ "label": "ಕ್ಯಾಮೆರಾ ಗುಂಪು"
+ }
+}
diff --git a/web/public/locales/kn/components/dialog.json b/web/public/locales/kn/components/dialog.json
new file mode 100644
index 0000000000..9fff19e814
--- /dev/null
+++ b/web/public/locales/kn/components/dialog.json
@@ -0,0 +1,5 @@
+{
+ "restart": {
+ "title": "ನೀವು ಫ್ರಿಗೇಟನ್ನು ಖಂಡಿತವಗೆ ರೀಸ್ಟಾರ್ಟ್ ಮಾಡಬಯಸುತ್ತೀರಾ?"
+ }
+}
diff --git a/web/public/locales/kn/components/filter.json b/web/public/locales/kn/components/filter.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/kn/components/filter.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/kn/components/icons.json b/web/public/locales/kn/components/icons.json
new file mode 100644
index 0000000000..c2c11d4eed
--- /dev/null
+++ b/web/public/locales/kn/components/icons.json
@@ -0,0 +1,5 @@
+{
+ "iconPicker": {
+ "selectIcon": "ಬಿಂಬವನ್ನು ಆಯ್ಕೆಮಾಡಿ"
+ }
+}
diff --git a/web/public/locales/kn/components/input.json b/web/public/locales/kn/components/input.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/kn/components/input.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/kn/components/player.json b/web/public/locales/kn/components/player.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/kn/components/player.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/kn/config/cameras.json b/web/public/locales/kn/config/cameras.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/kn/config/cameras.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/kn/config/global.json b/web/public/locales/kn/config/global.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/kn/config/global.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/kn/config/groups.json b/web/public/locales/kn/config/groups.json
new file mode 100644
index 0000000000..70b11825c7
--- /dev/null
+++ b/web/public/locales/kn/config/groups.json
@@ -0,0 +1,7 @@
+{
+ "audio": {
+ "global": {
+ "detection": "ಜಾಗತಿಕ ಪತ್ತೆದಾರಿ"
+ }
+ }
+}
diff --git a/web/public/locales/kn/config/validation.json b/web/public/locales/kn/config/validation.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/kn/config/validation.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/kn/objects.json b/web/public/locales/kn/objects.json
new file mode 100644
index 0000000000..4eb1d32167
--- /dev/null
+++ b/web/public/locales/kn/objects.json
@@ -0,0 +1,3 @@
+{
+ "person": "ವ್ಯಕ್ತಿ"
+}
diff --git a/web/public/locales/kn/views/chat.json b/web/public/locales/kn/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/kn/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/kn/views/classificationModel.json b/web/public/locales/kn/views/classificationModel.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/kn/views/classificationModel.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/kn/views/configEditor.json b/web/public/locales/kn/views/configEditor.json
new file mode 100644
index 0000000000..04d0948db8
--- /dev/null
+++ b/web/public/locales/kn/views/configEditor.json
@@ -0,0 +1,3 @@
+{
+ "documentTitle": "ಕಾನ್ಫಿಗ್ ಸಂಪಾದಕ - ಫ್ರಿಗೇಟ್"
+}
diff --git a/web/public/locales/kn/views/events.json b/web/public/locales/kn/views/events.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/kn/views/events.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/kn/views/explore.json b/web/public/locales/kn/views/explore.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/kn/views/explore.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/kn/views/exports.json b/web/public/locales/kn/views/exports.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/kn/views/exports.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/kn/views/faceLibrary.json b/web/public/locales/kn/views/faceLibrary.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/kn/views/faceLibrary.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/kn/views/live.json b/web/public/locales/kn/views/live.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/kn/views/live.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/kn/views/motionSearch.json b/web/public/locales/kn/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/kn/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/kn/views/recording.json b/web/public/locales/kn/views/recording.json
new file mode 100644
index 0000000000..c0a9826aef
--- /dev/null
+++ b/web/public/locales/kn/views/recording.json
@@ -0,0 +1,3 @@
+{
+ "export": "ರಪ್ತು ಮಾಡು"
+}
diff --git a/web/public/locales/kn/views/replay.json b/web/public/locales/kn/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/kn/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/kn/views/search.json b/web/public/locales/kn/views/search.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/kn/views/search.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/kn/views/settings.json b/web/public/locales/kn/views/settings.json
new file mode 100644
index 0000000000..99f0d31851
--- /dev/null
+++ b/web/public/locales/kn/views/settings.json
@@ -0,0 +1,5 @@
+{
+ "documentTitle": {
+ "default": "ಸೆಟ್ಟಿಂಗ್ಗಳು - ಫ್ರಿಗೇಟ್"
+ }
+}
diff --git a/web/public/locales/kn/views/system.json b/web/public/locales/kn/views/system.json
new file mode 100644
index 0000000000..c7bd532fee
--- /dev/null
+++ b/web/public/locales/kn/views/system.json
@@ -0,0 +1,5 @@
+{
+ "documentTitle": {
+ "cameras": "ಕ್ಯಾಮೆರಾ ಅಂಕಿಅಂಶಗಳು - ಫ್ರಿಗೇಟ್"
+ }
+}
diff --git a/web/public/locales/ko/common.json b/web/public/locales/ko/common.json
index 80293f4f03..94f0b194ec 100644
--- a/web/public/locales/ko/common.json
+++ b/web/public/locales/ko/common.json
@@ -185,7 +185,8 @@
"classification": "분류",
"chat": "채팅",
"actions": "작업",
- "profiles": "프로필"
+ "profiles": "프로필",
+ "features": "기능"
},
"unit": {
"speed": {
diff --git a/web/public/locales/ko/components/camera.json b/web/public/locales/ko/components/camera.json
index 67b1a2ee67..610fae4c85 100644
--- a/web/public/locales/ko/components/camera.json
+++ b/web/public/locales/ko/components/camera.json
@@ -81,6 +81,7 @@
"zones": "구역 (Zones)",
"mask": "마스크",
"motion": "움직임",
- "regions": "영역 (Regions)"
+ "regions": "영역 (Regions)",
+ "paths": "경로"
}
}
diff --git a/web/public/locales/ko/components/player.json b/web/public/locales/ko/components/player.json
index 38ef7daacd..e6b1f0df06 100644
--- a/web/public/locales/ko/components/player.json
+++ b/web/public/locales/ko/components/player.json
@@ -1,7 +1,8 @@
{
"submitFrigatePlus": {
"submit": "제출",
- "title": "이 프레임을 Frigate+에 제출하시겠습니까?"
+ "title": "이 프레임을 Frigate+에 제출하시겠습니까?",
+ "previewError": "스냅샷 미리보기를 불러올 수 없습니다. 현재 녹화된 영상을 사용할 수 없을 수 있습니다."
},
"stats": {
"bandwidth": {
diff --git a/web/public/locales/ko/config/cameras.json b/web/public/locales/ko/config/cameras.json
index 3f64349db6..2e5d533706 100644
--- a/web/public/locales/ko/config/cameras.json
+++ b/web/public/locales/ko/config/cameras.json
@@ -3,5 +3,61 @@
"name": {
"label": "카메라 이름",
"description": "카메라 이름은 필수 항목입니다"
+ },
+ "friendly_name": {
+ "label": "별칭",
+ "description": "Frigate UI에서 사용되는 카메라 별칭"
+ },
+ "enabled": {
+ "label": "활성화됨",
+ "description": "활성화됨"
+ },
+ "audio": {
+ "label": "오디오 이벤트",
+ "description": "이 카메라의 오디오 기반 이벤트 감지 설정입니다.",
+ "enabled": {
+ "label": "오디오 감지 활성화",
+ "description": "이 카메라의 오디오 이벤트 감지를 활성화하거나 비활성화합니다."
+ },
+ "max_not_heard": {
+ "label": "종료 타임아웃",
+ "description": "오디오 이벤트가 종료되기 전, 설정된 오디오 유형이 감지되지 않는 시간(초)입니다."
+ },
+ "min_volume": {
+ "label": "최소 볼륨",
+ "description": "오디오 감지를 실행하는 데 필요한 최소 RMS 볼륨 임계값으로, 낮을수록 민감도가 높아집니다(예: 200 높음, 500 보통, 1000 낮음)."
+ },
+ "listen": {
+ "label": "청취 유형",
+ "description": "감지할 오디오 이벤트 유형 목록입니다(예: bark, fire_alarm, scream, speech, yell)."
+ },
+ "filters": {
+ "label": "오디오 필터",
+ "description": "오탐지를 줄이기 위해 사용되는 신뢰도 임계값과 같은 오디오 유형별 필터 설정입니다."
+ },
+ "enabled_in_config": {
+ "label": "원래 오디오 상태"
+ }
+ },
+ "mqtt": {
+ "label": "MQTT"
+ },
+ "notifications": {
+ "label": "알림",
+ "enabled": {
+ "label": "알림 활성화"
+ },
+ "email": {
+ "label": "알림 이메일",
+ "description": "푸시 알림에 사용되거나 특정 알림 제공업체에서 요구하는 이메일 주소입니다."
+ },
+ "cooldown": {
+ "label": "알림 재발송 대기 시간",
+ "description": "수신자에게 스팸 메일을 보내는 것을 방지하기 위해 알림 재발송 대기시간(초)을 설정합니다."
+ },
+ "enabled_in_config": {
+ "label": "초기 알림 활성 상태",
+ "description": "초기 구성에서 알림이 활성화되었는지 여부를 나타냅니다."
+ }
}
}
diff --git a/web/public/locales/ko/config/global.json b/web/public/locales/ko/config/global.json
index f2cdb1059b..95fa0b1a6d 100644
--- a/web/public/locales/ko/config/global.json
+++ b/web/public/locales/ko/config/global.json
@@ -4,6 +4,196 @@
"description": "마이그레이션 및 데이터 형식 변경 확인을 위한 현재 설정의 버전 정보(숫자 또는 문자열)입니다."
},
"safe_mode": {
- "label": "안전 모드"
+ "label": "안전 모드",
+ "description": "활성화하면 문제 해결을 위해 기능이 제한된 안전 모드로 Frigate를 시작합니다."
+ },
+ "environment_vars": {
+ "label": "환경 변수",
+ "description": "Home Assistant OS에서 Frigate 프로세스에 설정할 환경 변수의 키/값 쌍입니다. HAOS가 아닌 사용자는 대신 Docker 환경 변수 설정을 사용해야 합니다."
+ },
+ "logger": {
+ "label": "로깅",
+ "description": "기본 로그 상세 수준 및 구성 요소별 로그 수준 재정의를 제어합니다.",
+ "default": {
+ "label": "로그 수준",
+ "description": "기본 전역 로그 상세 수준(debug, info, warning, error)입니다."
+ },
+ "logs": {
+ "label": "프로세스별 로그 수준",
+ "description": "특정 모듈의 상세 수준을 높이거나 낮추기 위한 구성 요소별 로그 수준 재정의입니다."
+ }
+ },
+ "audio": {
+ "label": "오디오 이벤트",
+ "enabled": {
+ "label": "오디오 감지 활성화"
+ },
+ "max_not_heard": {
+ "label": "종료 타임아웃",
+ "description": "오디오 이벤트가 종료되기 전, 설정된 오디오 유형이 감지되지 않는 시간(초)입니다."
+ },
+ "min_volume": {
+ "label": "최소 볼륨",
+ "description": "오디오 감지를 실행하는 데 필요한 최소 RMS 볼륨 임계값으로, 낮을수록 민감도가 높아집니다(예: 200 높음, 500 보통, 1000 낮음)."
+ },
+ "listen": {
+ "label": "청취 유형",
+ "description": "감지할 오디오 이벤트 유형 목록입니다(예: bark, fire_alarm, scream, speech, yell)."
+ },
+ "filters": {
+ "label": "오디오 필터",
+ "description": "오탐지를 줄이기 위해 사용되는 신뢰도 임계값과 같은 오디오 유형별 필터 설정입니다."
+ },
+ "enabled_in_config": {
+ "label": "원래 오디오 상태"
+ }
+ },
+ "auth": {
+ "label": "인증",
+ "description": "쿠키 및 속도 제한 옵션을 포함한 인증 및 세션 관련 설정입니다.",
+ "enabled": {
+ "label": "인증 활성화",
+ "description": "Frigate UI에 대한 기본 인증을 활성화합니다."
+ },
+ "reset_admin_password": {
+ "label": "관리자 비밀번호 재설정",
+ "description": "true로 설정하면 시작 시 관리자 비밀번호를 재설정하고 새 비밀번호를 로그에 출력합니다."
+ },
+ "cookie_name": {
+ "label": "JWT 쿠키 이름",
+ "description": "자체 인증용 JWT 토큰을 저장할 쿠키 이름입니다."
+ },
+ "cookie_secure": {
+ "label": "보안 쿠키 설정",
+ "description": "인증 쿠키에 보안 플래그를 설정합니다. TLS를 사용하는 경우 'True'로 설정해야 합니다."
+ },
+ "session_length": {
+ "label": "세션 길이",
+ "description": "JWT 기반 세션의 유지 시간(초)입니다."
+ },
+ "refresh_time": {
+ "label": "세션 갱신 주기",
+ "description": "세션 만료까지 남은 시간이 몇 초 남지 않을 경우, 세션 시간을 다시 최대로 연장합니다."
+ },
+ "failed_login_rate_limit": {
+ "label": "로그인 실패 제한",
+ "description": "무차별 대입 공격을 줄이기 위해 로그인 시도 실패 횟수를 제한하는 규칙을 적용합니다."
+ },
+ "trusted_proxies": {
+ "label": "신뢰할 수 있는 프록시",
+ "description": "속도 제한을 위해 클라이언트 IP를 결정할 때 사용되는 신뢰할 수 있는 프록시 IP 목록입니다."
+ },
+ "hash_iterations": {
+ "label": "해시 반복 횟수",
+ "description": "사용자 암호를 해싱할 때 사용할 PBKDF2-SHA256 반복 횟수입니다."
+ },
+ "roles": {
+ "label": "역할 할당",
+ "description": "역할별로 접근 가능한 카메라 목록을 매핑합니다. 목록이 비어 있으면 해당 역할에 모든 카메라 접근 권한을 부여합니다."
+ },
+ "admin_first_time_login": {
+ "label": "관리자 초기 로그인 설정",
+ "description": "활성화 시, 관리자 비밀번호 초기화 후 로그인 방법 안내 링크가 로그인 페이지에 표시됩니다. "
+ }
+ },
+ "database": {
+ "label": "데이터베이스",
+ "description": "추적된 객체 및 녹화 메타데이터를 저장하는 SQLite 데이터베이스 설정입니다.",
+ "path": {
+ "label": "데이터베이스 경로",
+ "description": "Frigate SQLite 데이터베이스 파일이 저장될 파일 시스템 경로입니다."
+ }
+ },
+ "go2rtc": {
+ "label": "go2rtc",
+ "description": "라이브 스트림 중계 및 번역에 사용되는 통합 go2rtc 리스트리밍 서비스 설정입니다."
+ },
+ "mqtt": {
+ "label": "MQTT",
+ "description": "MQTT 브로커에 원격 측정 데이터, 스냅샷 및 이벤트 세부 정보를 연결하고 게시하기 위한 설정입니다.",
+ "enabled": {
+ "label": "MQTT 활성화",
+ "description": "상태, 이벤트 및 스냅샷에 대한 MQTT 통합을 활성화 또는 비활성화합니다."
+ },
+ "host": {
+ "label": "MQTT 호스트",
+ "description": "MQTT 브로커의 호스트 이름 또는 IP 주소입니다."
+ },
+ "port": {
+ "label": "MQTT 포트",
+ "description": "MQTT 브로커의 포트 번호입니다 (일반적인 포트는 1883입니다)."
+ },
+ "topic_prefix": {
+ "label": "토픽 접두사",
+ "description": "Frigate의 모든 MQTT 메시지에 사용할 접두사입니다. 여러 대의 Frigate를 실행하는 경우 각각 고유한 이름을 사용해야 합니다."
+ },
+ "client_id": {
+ "label": "클라이언트 ID",
+ "description": "MQTT 브로커 연결 시 사용하는 클라이언트 식별자입니다. 인스턴스마다 고유한 이름을 사용해야 합니다."
+ },
+ "stats_interval": {
+ "label": "통계 간격",
+ "description": "시스템 및 카메라 통계 정보를 MQTT로 전송하는 간격(초)입니다."
+ },
+ "user": {
+ "label": "MQTT 사용자 이름",
+ "description": "MQTT 사용자 이름(선택 사항)입니다. 환경 변수나 비밀 값(Secrets)을 통해 입력할 수 있습니다."
+ },
+ "password": {
+ "label": "MQTT 비밀번호",
+ "description": "MQTT 비밀번호(선택 사항)입니다. 환경 변수나 비밀 값(Secrets)을 통해 입력할 수 있습니다."
+ },
+ "tls_ca_certs": {
+ "label": "TLS CA 인증서",
+ "description": "브로커와의 TLS 연결에 사용할 CA 인증서 경로(자체 서명 인증서의 경우)."
+ },
+ "tls_client_cert": {
+ "label": "클라이언트 인증서",
+ "description": "TLS 상호 인증을 위한 클라이언트 인증서 경로입니다. 클라이언트 인증서를 사용할 때는 사용자 이름/암호를 설정하지 마십시오."
+ },
+ "tls_client_key": {
+ "label": "클라이언트 키",
+ "description": "클라이언트 인증서의 개인 키 경로입니다."
+ },
+ "tls_insecure": {
+ "label": "TLS 비보안 모드",
+ "description": "호스트 이름 확인을 건너뛰어 안전하지 않은 TLS 연결을 허용합니다(권장하지 않음)."
+ },
+ "qos": {
+ "label": "MQTT QoS",
+ "description": "MQTT 메시지 전송 및 구독에 대한 서비스 품질(QoS) 등급입니다 (0, 1, 2 중 선택)."
+ }
+ },
+ "notifications": {
+ "label": "알림",
+ "description": "모든 카메라에 대한 알림을 활성화하고 제어하는 설정입니다. 카메라별로 설정을 재정의할 수 있습니다.",
+ "enabled": {
+ "label": "알림 활성화",
+ "description": "모든 카메라에 대한 알림을 활성화 또는 비활성화할 수 있으며, 카메라별로 설정을 재정의할 수 있습니다."
+ },
+ "email": {
+ "label": "알림 이메일",
+ "description": "푸시 알림에 사용되거나 특정 알림 제공업체에서 요구하는 이메일 주소입니다."
+ },
+ "cooldown": {
+ "label": "알림 재발송 대기 시간",
+ "description": "수신자에게 스팸 메일을 보내는 것을 방지하기 위해 알림 재발송 대기시간(초)을 설정합니다."
+ },
+ "enabled_in_config": {
+ "label": "초기 알림 활성 상태",
+ "description": "초기 구성에서 알림이 활성화되었는지 여부를 나타냅니다."
+ }
+ },
+ "networking": {
+ "label": "네트워킹",
+ "description": "Frigate 엔드포인트에 대한 IPv6 활성화와 같은 네트워크 관련 설정입니다.",
+ "ipv6": {
+ "label": "IPv6 구성",
+ "description": "Frigate 네트워크 서비스에 대한 IPv6 관련 설정입니다.",
+ "enabled": {
+ "label": "IPv6 활성화",
+ "description": "Frigate 서비스(API 및 UI)에 IPv6 지원이 필요한 경우 활성화하십시오."
+ }
+ }
}
}
diff --git a/web/public/locales/ko/config/groups.json b/web/public/locales/ko/config/groups.json
index 78b422e831..4578c83cf7 100644
--- a/web/public/locales/ko/config/groups.json
+++ b/web/public/locales/ko/config/groups.json
@@ -5,7 +5,53 @@
"sensitivity": "전체 민감도"
},
"cameras": {
- "detection": "감지"
+ "detection": "감지",
+ "sensitivity": "민감도"
+ }
+ },
+ "timestamp_style": {
+ "global": {
+ "appearance": "전역 외관"
+ },
+ "cameras": {
+ "appearance": "외관"
+ }
+ },
+ "motion": {
+ "global": {
+ "sensitivity": "전역 민감도",
+ "algorithm": "전역 알고리즘"
+ },
+ "cameras": {
+ "sensitivity": "민감도",
+ "algorithm": "알고리즘"
+ }
+ },
+ "snapshots": {
+ "global": {
+ "display": "전역 표시"
+ },
+ "cameras": {
+ "display": "표시"
+ }
+ },
+ "detect": {
+ "global": {
+ "resolution": "전역 해상도",
+ "tracking": "전역 추적"
+ },
+ "cameras": {
+ "resolution": "해상도",
+ "tracking": "추적"
+ }
+ },
+ "objects": {
+ "global": {
+ "tracking": "전역 추적",
+ "filtering": "전역 필터링"
+ },
+ "cameras": {
+ "tracking": "추적"
}
}
}
diff --git a/web/public/locales/ko/views/chat.json b/web/public/locales/ko/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/ko/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/ko/views/classificationModel.json b/web/public/locales/ko/views/classificationModel.json
index 227621f10c..832c3723f5 100644
--- a/web/public/locales/ko/views/classificationModel.json
+++ b/web/public/locales/ko/views/classificationModel.json
@@ -8,6 +8,28 @@
"button": {
"deleteClassificationAttempts": "분류 이미지 삭제",
"renameCategory": "클래스 이름 변경",
- "deleteCategory": "클래스 삭제"
+ "deleteCategory": "클래스 삭제",
+ "deleteImages": "이미지 삭제",
+ "trainModel": "모델 훈련",
+ "addClassification": "분류 추가",
+ "deleteModels": "모델 삭제",
+ "editModel": "모델 편집"
+ },
+ "tooltip": {
+ "trainingInProgress": "모델이 현재 학습 중입니다",
+ "noNewImages": "훈련할 새 이미지가 없습니다. 먼저 데이터셋에서 더 많은 이미지를 분류하세요.",
+ "noChanges": "마지막 훈련 이후 데이터셋에 변경 사항이 없습니다.",
+ "modelNotReady": "모델이 훈련 준비가 되지 않았습니다"
+ },
+ "toast": {
+ "success": {
+ "deletedModel_other": "{{count}}개 모델을 성공적으로 삭제했습니다",
+ "categorizedImage": "이미지 분류 성공",
+ "reclassifiedImage": "이미지 재분류 성공",
+ "trainedModel": "모델 훈련 완료."
+ }
+ },
+ "train": {
+ "titleShort": "최근"
}
}
diff --git a/web/public/locales/ko/views/events.json b/web/public/locales/ko/views/events.json
index 971494a814..3e357be850 100644
--- a/web/public/locales/ko/views/events.json
+++ b/web/public/locales/ko/views/events.json
@@ -9,9 +9,15 @@
"empty": {
"alert": "다시 볼 '경보' 영상이 없습니다",
"detection": "다시 볼 '대상 감지' 영상이 없습니다",
- "motion": "움직임 감지 데이터가 없습니다"
+ "motion": "움직임 감지 데이터가 없습니다",
+ "recordingsDisabled": {
+ "title": "녹화가 활성화되어야 합니다",
+ "description": "다시 보기 항목은 해당 카메라에서 녹화가 활성화된 경우에만 카메라에 대해 생성할 수 있습니다."
+ }
+ },
+ "timeline": {
+ "label": "타임라인"
},
- "timeline": "타임라인",
"timeline.aria": "타임라인 선택",
"events": {
"label": "이벤트",
@@ -23,7 +29,8 @@
"aria": "상세 보기",
"trackedObject_one": "추적 대상",
"trackedObject_other": "추적 대상",
- "noObjectDetailData": "상세 보기 데이터가 없습니다."
+ "noObjectDetailData": "상세 보기 데이터가 없습니다.",
+ "label": "세부 정보"
},
"objectTrack": {
"trackedPoint": "추적 포인트",
@@ -47,5 +54,7 @@
"camera": "카메라",
"detected": "감지됨",
"suspiciousActivity": "수상한 행동",
- "threateningActivity": "위협적인 행동"
+ "threateningActivity": "위협적인 행동",
+ "zoomIn": "확대",
+ "zoomOut": "축소"
}
diff --git a/web/public/locales/ko/views/explore.json b/web/public/locales/ko/views/explore.json
index 513d90d84e..5b9c9d5872 100644
--- a/web/public/locales/ko/views/explore.json
+++ b/web/public/locales/ko/views/explore.json
@@ -22,10 +22,18 @@
"visionModelFeatureExtractor": "비전 모델 특징 추출기",
"textModel": "Text model",
"textTokenizer": "텍스트 토크나이저"
- }
+ },
+ "tips": {
+ "context": "모델이 다운로드된 후 추적 객체의 임베딩을 색인 재구성하는 것이 좋습니다."
+ },
+ "error": "오류가 발생했습니다. Frigate 로그를 확인하세요."
}
},
"details": {
"timestamp": "시간 기록"
+ },
+ "trackedObjectDetails": "추적 객체 세부 정보",
+ "type": {
+ "details": "세부 정보"
}
}
diff --git a/web/public/locales/ko/views/exports.json b/web/public/locales/ko/views/exports.json
index 94b1a5ab78..588cdd800a 100644
--- a/web/public/locales/ko/views/exports.json
+++ b/web/public/locales/ko/views/exports.json
@@ -2,7 +2,9 @@
"documentTitle": "내보내기 - Frigate",
"search": "검색",
"noExports": "내보내기가 없습니다",
- "deleteExport": "내보내기 삭제",
+ "deleteExport": {
+ "label": "내보내기 삭제"
+ },
"deleteExport.desc": "{{exportName}}을 지우시겠습니까?",
"editExport": {
"title": "내보내기 이름 변경",
@@ -11,10 +13,24 @@
},
"toast": {
"error": {
- "renameExportFailed": "내보내기 이름 변경에 실패했습니다: {{errorMessage}}"
+ "renameExportFailed": "내보내기 이름 변경에 실패했습니다: {{errorMessage}}",
+ "assignCaseFailed": "케이스 할당 업데이트 실패: {{errorMessage}}"
}
},
"headings": {
- "uncategorizedExports": "분류되지 않은 내보내기"
+ "uncategorizedExports": "분류되지 않은 내보내기",
+ "cases": "케이스"
+ },
+ "tooltip": {
+ "shareExport": "내보내기 공유",
+ "downloadVideo": "동영상 다운로드",
+ "editName": "이름 편집",
+ "deleteExport": "내보내기 삭제",
+ "assignToCase": "케이스에 추가"
+ },
+ "caseDialog": {
+ "title": "케이스에 추가",
+ "description": "기존 케이스를 선택하거나 새 케이스를 만드세요.",
+ "selectLabel": "케이스"
}
}
diff --git a/web/public/locales/ko/views/faceLibrary.json b/web/public/locales/ko/views/faceLibrary.json
index a04ac45cc5..a99cb3875d 100644
--- a/web/public/locales/ko/views/faceLibrary.json
+++ b/web/public/locales/ko/views/faceLibrary.json
@@ -16,22 +16,28 @@
"selectItem": "{{item}} 선택",
"documentTitle": "얼굴 라이브러리 - Frigate",
"uploadFaceImage": {
- "title": "얼굴 사진 올리기"
+ "title": "얼굴 사진 올리기",
+ "desc": "얼굴을 스캔하고 {{pageToggle}}에 포함하기 위해 이미지를 업로드하세요"
},
"collections": "모음집",
"createFaceLibrary": {
"title": "모음집 만들기",
"desc": "새로운 모음집 만들기",
- "new": "새 얼굴 만들기"
+ "new": "새 얼굴 만들기",
+ "nextSteps": "강력한 기반을 구축하려면:최근 인식 탭을 사용하여 감지된 각 사람의 이미지를 선택하고 학습하세요. 최상의 결과를 위해 정면 이미지에 집중하고, 각도가 있는 얼굴이 촬영된 이미지는 학습에 사용하지 마세요. "
},
"steps": {
"faceName": "얼굴 이름 입력",
"uploadFace": "얼굴 사진 올리기",
- "nextSteps": "다음 단계"
+ "nextSteps": "다음 단계",
+ "description": {
+ "uploadFace": "정면을 향한 {{name}}의 얼굴이 보이는 이미지를 업로드하세요. 이미지를 얼굴만 자를 필요는 없습니다."
+ }
},
"train": {
- "title": "학습",
- "aria": "학습 선택"
+ "title": "최근 인식",
+ "aria": "최근 인식 선택",
+ "titleShort": "최근"
},
"selectFace": "얼굴 선택",
"deleteFaceLibrary": {
diff --git a/web/public/locales/ko/views/live.json b/web/public/locales/ko/views/live.json
index 5a825a08f5..c2b05d04ea 100644
--- a/web/public/locales/ko/views/live.json
+++ b/web/public/locales/ko/views/live.json
@@ -15,7 +15,8 @@
"clickMove": {
"label": "클릭해서 카메라 중앙 배치",
"enable": "클릭해서 움직이기 기능 활성화",
- "disable": "클릭해서 움직이기 기능 비활성화"
+ "disable": "클릭해서 움직이기 기능 비활성화",
+ "enableWithZoom": "클릭하여 이동 / 드래그하여 확대 활성화"
},
"left": {
"label": "PTZ 카메라 왼쪽으로 이동"
diff --git a/web/public/locales/ko/views/motionSearch.json b/web/public/locales/ko/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/ko/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/ko/views/replay.json b/web/public/locales/ko/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/ko/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/ko/views/search.json b/web/public/locales/ko/views/search.json
index b898fb8265..efb5c93870 100644
--- a/web/public/locales/ko/views/search.json
+++ b/web/public/locales/ko/views/search.json
@@ -5,7 +5,24 @@
"clear": "검색 초기화",
"save": "검색 저장",
"filterInformation": "필터 정보",
- "delete": "저장된 검색 삭제"
+ "delete": "저장된 검색 삭제",
+ "filterActive": "필터 활성화됨"
},
- "searchFor": "{{inputValue}} 검색"
+ "searchFor": "{{inputValue}} 검색",
+ "trackedObjectId": "추적 객체 ID",
+ "filter": {
+ "label": {
+ "cameras": "카메라",
+ "labels": "레이블",
+ "zones": "구역",
+ "sub_labels": "하위 레이블",
+ "attributes": "속성",
+ "search_type": "검색 유형",
+ "time_range": "시간 범위",
+ "before": "이전",
+ "after": "이후",
+ "min_score": "최소 점수",
+ "max_score": "최대 점수"
+ }
+ }
}
diff --git a/web/public/locales/ko/views/settings.json b/web/public/locales/ko/views/settings.json
index c17eaa7fd5..220cceb8f2 100644
--- a/web/public/locales/ko/views/settings.json
+++ b/web/public/locales/ko/views/settings.json
@@ -29,14 +29,15 @@
"masksAndZones": "마스크와 구역 편집기 - Frigate",
"motionTuner": "움직임 감지 조정 - Frigate",
"object": "디버그 - Frigate",
- "general": "프로필 설정 - Frigate",
+ "general": "UI 설정 - Frigate",
"frigatePlus": "Frigate+ 설정 - Frigate",
"notifications": "알림 설정 - Frigate",
"cameraManagement": "카메라 관리 - Frigate",
"cameraReview": "카메라 다시보기 설정 - Frigate",
"globalConfig": "전체 설정 - Frigate",
"cameraConfig": "카메라 설정 - Frigate",
- "maintenance": "유지 관리 - Frigate"
+ "maintenance": "유지 관리 - Frigate",
+ "profiles": "프로필 - Frigate"
},
"users": {
"table": {
@@ -219,5 +220,11 @@
"label": "새 값",
"reset": "초기화"
}
+ },
+ "button": {
+ "overriddenGlobal": "전역 재정의됨",
+ "overriddenGlobalTooltip": "이 카메라는 이 섹션의 전역 설정을 재정의합니다",
+ "overriddenBaseConfig": "기본 설정 재정의됨",
+ "overriddenBaseConfigTooltip": "{{profile}} 프로필은 이 섹션의 구성 설정을 재정의합니다"
}
}
diff --git a/web/public/locales/lt/views/chat.json b/web/public/locales/lt/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/lt/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/lt/views/motionSearch.json b/web/public/locales/lt/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/lt/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/lt/views/replay.json b/web/public/locales/lt/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/lt/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/lv/audio.json b/web/public/locales/lv/audio.json
index aab12a1b25..56bab07d93 100644
--- a/web/public/locales/lv/audio.json
+++ b/web/public/locales/lv/audio.json
@@ -31,5 +31,96 @@
"laughter": "Smiekli",
"boat": "Laiva",
"snicker": "Ķiķināšana",
- "camera": "Kamera"
+ "camera": "Kamera",
+ "crying": "Raudāšana",
+ "child_singing": "Bērna dziedāšana",
+ "humming": "Dungošana",
+ "sneeze": "Šķaudīšana",
+ "footsteps": "Soļi",
+ "run": "Skrien",
+ "chewing": "Košļāšana",
+ "fart": "Pirdiens",
+ "pets": "Mājdzīvnieki",
+ "dog": "Suns",
+ "bark": "Riešana",
+ "cat": "Kaķis",
+ "purr": "Murrā",
+ "meow": "Ņaud",
+ "horse": "Zirgs",
+ "pig": "Cūka",
+ "turkey": "Tītars",
+ "duck": "Pīle",
+ "quack": "Pēkšk",
+ "goose": "Zos",
+ "owl": "Pūce",
+ "dogs": "Suņi",
+ "rats": "Žurkas",
+ "mouse": "Pele",
+ "buzz": "Dūc",
+ "pulse": "Pulsē",
+ "inside": "Iekšā",
+ "outside": "Ārā",
+ "echo": "Atbals",
+ "noise": "Troksnis",
+ "vibration": "Vibrācija",
+ "sigh": "Nopūta",
+ "singing": "Nopūsties",
+ "choir": "Koris",
+ "yodeling": "Jodelēšana",
+ "mantra": "Mantra",
+ "rapping": "Repot",
+ "whistling": "Svilpot",
+ "sniff": "Ošņāt",
+ "hands": "Rokas",
+ "animal": "Dzīvnieks",
+ "drum_kit": "Bungu komplekts",
+ "drum": "Bungas",
+ "gong": "Gongs",
+ "violin": "Vijole",
+ "flute": "Flauta",
+ "cello": "Čells",
+ "bell": "Zvans",
+ "ship": "Kuģis",
+ "motorboat": "Motorlaiva",
+ "sailboat": "Buru laiva",
+ "motor_vehicle": "Automašīna",
+ "car_alarm": "Auto signalizācija",
+ "truck": "Smagā automašīna",
+ "air_horn": "Gaisa taure",
+ "police_car": "Policijas auto",
+ "fire_engine": "Ugunsdzēsēju auto",
+ "train_whistle": "Vilciena taure",
+ "aircraft": "Lidmašīna",
+ "aircraft_engine": "Lidmašīnas dzinējs",
+ "jet_engine": "Reaktīvais dzinējs",
+ "propeller": "Propellers",
+ "helicopter": "Helikopters",
+ "engine": "Dzinējs",
+ "lawn_mower": "Zāles pļāvējs",
+ "door": "Durvis",
+ "doorbell": "Durvju zvans",
+ "accelerating": "Paātrinās",
+ "engine_starting": "Dzinēja iedarbināšana",
+ "idling": "rūc",
+ "slam": "Aizcirst",
+ "sink": "Izlietne",
+ "bathtub": "Vanna",
+ "hair_dryer": "Fēns",
+ "toothbrush": "Zobu birste",
+ "coin": "Monēta",
+ "alarm": "Signalizācija",
+ "telephone": "Telefons",
+ "siren": "Sirēna",
+ "smoke_detector": "Dūmu detektors",
+ "clock": "Pulkstens",
+ "printer": "Printeris",
+ "whistle": "Svilpe",
+ "tools": "Instrumenti",
+ "drill": "Urbis",
+ "gunshot": "Šaviens",
+ "explosion": "Sprādziens",
+ "fireworks": "Uguņošana",
+ "glass": "Stikls",
+ "white_noise": "Baltais troksnis",
+ "radio": "Radio"
}
diff --git a/web/public/locales/lv/common.json b/web/public/locales/lv/common.json
index 623ad21190..61b3d076fb 100644
--- a/web/public/locales/lv/common.json
+++ b/web/public/locales/lv/common.json
@@ -156,7 +156,15 @@
"export": "Eksportēt",
"deleteNow": "Izdzēst tagad",
"next": "Nākamais",
- "continue": "Turpināt"
+ "continue": "Turpināt",
+ "add": "Pievienot",
+ "applying": "Apstiprina…",
+ "copiedToClipboard": "Nokopēts atmiņā",
+ "undo": "Atsaukt",
+ "undoAll": "Atsaukt visu",
+ "saveAll": "Saglabāt visu",
+ "retry": "Mēģināt vēlreiz",
+ "savingAll": "Saglabā visu…"
},
"menu": {
"system": "Sistēma",
@@ -208,7 +216,8 @@
"ur": "اردو (urdu)",
"withSystem": {
"label": "Izmantojiet sistēmas iestatījumus valodai"
- }
+ },
+ "zhHant": "繁體中文 (Tradicionālā ķīniešu)"
},
"appearance": "Izskats",
"darkMode": {
@@ -258,7 +267,8 @@
"anonymous": "anonīms",
"logout": "Iziet",
"setPassword": "Izveidot paroli"
- }
+ },
+ "profiles": "Profili"
},
"toast": {
"copyUrlToClipboard": "Adrese nokopēta.",
diff --git a/web/public/locales/lv/components/camera.json b/web/public/locales/lv/components/camera.json
index d061f554d6..4c1e5f1259 100644
--- a/web/public/locales/lv/components/camera.json
+++ b/web/public/locales/lv/components/camera.json
@@ -33,7 +33,7 @@
"title": "Straumēšanas iestatījumi{{cameraName}}",
"desc": "Mainiet šīs kameru grupas paneļa tiešraides straumes iestatījumus. Šie iestatījumi ir atkarīgi no ierīces/pārlūkprogrammas. ",
"audioIsAvailable": "Šai straumei ir pieejams audio",
- "audioIsUnavailable": "Šai straumei nav pieejams audio.",
+ "audioIsUnavailable": "Šai straumei nav pieejams audio",
"audio": {
"tips": {
"title": "Šai straumei audio ir jāizvada no kameras un tas ir jākonfigurē go2rtc."
@@ -67,7 +67,10 @@
"desc": "Iespējojiet šo opciju tikai tad, ja kameras tiešraides straumē tiek rādīti krāsu artefakti un attēla labajā pusē ir diagonāla līnija."
}
}
- }
+ },
+ "showAll": "Parādīt visas kameru grupas",
+ "showLess": "Rādīt mazāk",
+ "editGroups": "Rediģēt kameru grupas"
},
"debug": {
"options": {
@@ -81,6 +84,7 @@
"zones": "Zonas",
"mask": "Maska",
"motion": "Kustība",
- "regions": "Reģioni"
+ "regions": "Reģioni",
+ "paths": "Ceļi"
}
}
diff --git a/web/public/locales/lv/components/player.json b/web/public/locales/lv/components/player.json
index 43f8359f92..c5c04ecfc4 100644
--- a/web/public/locales/lv/components/player.json
+++ b/web/public/locales/lv/components/player.json
@@ -4,12 +4,13 @@
"noPreviewFoundFor": "{{cameraName}} nav atrasts priekšskatījums",
"submitFrigatePlus": {
"title": "Nosūtīt šo kadru uz Frigate+?",
- "submit": "Iesniegt"
+ "submit": "Iesniegt",
+ "previewError": "Nevar ielādēt atēla priekškatījumu. Ieraksts var nebūt pieejams, šōbrīd."
},
"livePlayerRequiredIOSVersion": "Šāda veida straumēšanai nepieciešama iOS 17.1 vai jaunāka versija.",
"streamOffline": {
"title": "Bezsaistes straume",
- "desc": "No kameras {{cameraName}} detect straumes netika saņemti kadri, pārbaudiet kļūdu žurnālus."
+ "desc": "No kameras {{cameraName}} detect straumes netika saņemti kadri, pārbaudiet kļūdu žurnālus"
},
"cameraDisabled": "Kamera ir izslēgta",
"stats": {
@@ -47,5 +48,6 @@
"error": {
"submitFrigatePlusFailed": "Neizdevās iesniegt kadru Frigate+"
}
- }
+ },
+ "cameraOff": "Kamera ir izslēgta"
}
diff --git a/web/public/locales/lv/objects.json b/web/public/locales/lv/objects.json
index 981d5cb44e..cf1edce711 100644
--- a/web/public/locales/lv/objects.json
+++ b/web/public/locales/lv/objects.json
@@ -15,5 +15,16 @@
"dhl": "DHL",
"postnl": "PostNL",
"dpd": "DPD",
- "boat": "Laiva"
+ "boat": "Laiva",
+ "dog": "Suns",
+ "bark": "Riešana",
+ "cat": "Kaķis",
+ "horse": "Zirgs",
+ "mouse": "Pele",
+ "animal": "Dzīvnieks",
+ "door": "Durvis",
+ "sink": "Izlietne",
+ "hair_dryer": "Fēns",
+ "toothbrush": "Zobu birste",
+ "clock": "Pulkstens"
}
diff --git a/web/public/locales/lv/views/chat.json b/web/public/locales/lv/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/lv/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/lv/views/motionSearch.json b/web/public/locales/lv/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/lv/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/lv/views/replay.json b/web/public/locales/lv/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/lv/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/lv/views/settings.json b/web/public/locales/lv/views/settings.json
index 7fb5924883..867d6933c6 100644
--- a/web/public/locales/lv/views/settings.json
+++ b/web/public/locales/lv/views/settings.json
@@ -68,7 +68,8 @@
"roles": "Lomas",
"frigateplus": "Frigate+",
"notifications": "Paziņojumi",
- "triggers": "Trigeri"
+ "triggers": "Trigeri",
+ "profiles": "Profili"
},
"cameraSetting": {
"camera": "Kamera"
diff --git a/web/public/locales/ml/views/chat.json b/web/public/locales/ml/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/ml/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/ml/views/motionSearch.json b/web/public/locales/ml/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/ml/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/ml/views/replay.json b/web/public/locales/ml/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/ml/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/nb-NO/common.json b/web/public/locales/nb-NO/common.json
index 921ddc77b3..efe695371a 100644
--- a/web/public/locales/nb-NO/common.json
+++ b/web/public/locales/nb-NO/common.json
@@ -171,7 +171,7 @@
"configuration": "Konfigurasjon",
"systemLogs": "Systemlogger",
"settings": "Innstillinger",
- "configurationEditor": "Rediger konfigurasjonen",
+ "configurationEditor": "Rediger konfigurasjonsfil",
"languages": "Språk",
"language": {
"en": "English (Engelsk)",
@@ -216,7 +216,9 @@
"gl": "Galego (Galisisk)",
"id": "Bahasa Indonesia (Indonesisk)",
"ur": "اردو (Urdu)",
- "hr": "Hrvatski (Kroatisk)"
+ "hr": "Hrvatski (Kroatisk)",
+ "bs": "Bosanski (Bosnisk)",
+ "zhHant": "繁體中文 (Tradisjonell kinesisk)"
},
"appearance": "Utseende",
"darkMode": {
@@ -241,7 +243,8 @@
"classification": "Klassifisering",
"profiles": "Profiler",
"chat": "Chat",
- "actions": "Handlinger"
+ "actions": "Handlinger",
+ "features": "Funksjoner"
},
"pagination": {
"next": {
@@ -324,5 +327,8 @@
"separatorWithSpace": ", "
},
"validation_errors": "Valideringsfeil",
- "no_items": "Ingen elementer"
+ "no_items": "Ingen elementer",
+ "credentialField": {
+ "savedPlaceholder": "Lagret – la stå tomt for å beholde gjeldende"
+ }
}
diff --git a/web/public/locales/nb-NO/components/camera.json b/web/public/locales/nb-NO/components/camera.json
index 601da4bc1a..60d318a4ad 100644
--- a/web/public/locales/nb-NO/components/camera.json
+++ b/web/public/locales/nb-NO/components/camera.json
@@ -68,7 +68,10 @@
"desc": "Velg kameraer for denne gruppen."
},
"icon": "Ikon",
- "success": "Kameragruppen ({{name}}) er lagret."
+ "success": "Kameragruppen ({{name}}) er lagret.",
+ "showAll": "Vis alle kameragrupper",
+ "showLess": "Vis mindre",
+ "editGroups": "Rediger kameragrupper"
},
"debug": {
"options": {
diff --git a/web/public/locales/nb-NO/components/dialog.json b/web/public/locales/nb-NO/components/dialog.json
index 6f38ca4242..cdb09308a3 100644
--- a/web/public/locales/nb-NO/components/dialog.json
+++ b/web/public/locales/nb-NO/components/dialog.json
@@ -34,6 +34,9 @@
"ask_an": "Er dette objekt en {{label}}?",
"ask_a": "Er dette objektet en {{label}}?",
"ask_full": "Er dette objekt en {{untranslatedLabel}} ({{translatedLabel}})?"
+ },
+ "toast": {
+ "error": "Kunne ikke sende til Frigate+. Vennligst sjekk nettverksforbindelsen din og prøv igjen."
}
}
},
@@ -59,15 +62,26 @@
"toast": {
"success": "Eksport startet. Se filen på eksportsiden.",
"error": {
- "failed": "Klarte ikke å starte eksport: {{error}}",
+ "failed": "Kunne ikke legge eksport i kø: {{error}}",
"noVaildTimeSelected": "Ingen gyldig tidsperiode valgt",
"endTimeMustAfterStartTime": "Sluttid må være etter starttid"
},
- "view": "Vis"
+ "view": "Vis",
+ "queued": "Eksport lagt i kø. Se fremdrift på eksportsiden.",
+ "batchPartial": "Startet {{successful}} av {{total}} eksporter. Kameraer som feilet: {{failedCameras}}",
+ "batchFailed": "Kunne ikke starte {{total}} eksporter. Kameraer som feilet: {{failedCameras}}",
+ "batchQueuedPartial": "La {{successful}} av {{total}} eksporter i kø. Kameraer som feilet: {{failedCameras}}",
+ "batchQueueFailed": "Kunne ikke legge {{total}} eksporter i kø. Kameraer som feilet: {{failedCameras}}",
+ "batchSuccess_one": "Startet 1 eksport. Åpner saken nå.",
+ "batchSuccess_other": "Startet {{count}} eksporter. Åpner saken nå.",
+ "batchQueuedSuccess_one": "La 1 eksport i kø. Åpner saken nå.",
+ "batchQueuedSuccess_other": "La {{count}} eksporter i kø. Åpner saken nå."
},
"fromTimeline": {
"previewExport": "Forhåndsvis eksport",
- "saveExport": "Lagre eksport"
+ "saveExport": "Lagre eksport",
+ "queueingExport": "Legger eksport i kø...",
+ "useThisRange": "Bruk dette tidsrommet"
},
"name": {
"placeholder": "Gi eksporten et navn"
@@ -77,7 +91,56 @@
"selectOrExport": "Velg eller eksporter",
"case": {
"label": "Sak",
- "placeholder": "Velg en sak"
+ "placeholder": "Velg en sak",
+ "newCaseOption": "Opprett ny sak",
+ "newCaseNamePlaceholder": "Navn på ny sak",
+ "newCaseDescriptionPlaceholder": "Saksbeskrivelse",
+ "nonAdminHelp": "En ny sak vil bli opprettet for disse eksportene."
+ },
+ "queueing": "Legger eksport i kø...",
+ "tabs": {
+ "export": "Enkeltkamera",
+ "multiCamera": "Multikamera"
+ },
+ "multiCamera": {
+ "timeRange": "Tidsrom",
+ "selectFromTimeline": "Velg fra tidslinje",
+ "cameraSelection": "Kameraer",
+ "cameraSelectionHelp": "Kameraer med sporede objekter i dette tidsrommet er forhåndsvalgt",
+ "checkingActivity": "Sjekker kameraaktivitet...",
+ "noCameras": "Ingen kameraer tilgjengelig",
+ "nameLabel": "Eksportnavn",
+ "namePlaceholder": "Valgfritt navneprefiks for disse eksportene",
+ "queueingButton": "Legger eksporter i kø...",
+ "detectionCount_one": "1 sporet objekt",
+ "detectionCount_other": "{{count}} sporede objekter",
+ "exportButton_one": "Eksporter 1 kamera",
+ "exportButton_other": "Eksporter {{count}} kameraer",
+ "searchOrSelectGroup": "Søk, eller velg en kameragruppe...",
+ "selectAll": "Velg alle kameraer",
+ "clearSelection": "Nullstill valg",
+ "selectWithActivity": "Kameraer med sporede objekter",
+ "selectGroup": "Velg gruppe",
+ "noMatchingCameras": "Ingen kameraer samsvarer med søket ditt",
+ "selectedCount": "{{selected}} / {{total}} valgt"
+ },
+ "multi": {
+ "description": "Eksporter hver valgte inspeksjon. Alle eksporter vil bli gruppert under én sak.",
+ "descriptionNoCase": "Eksporter hver valgte inspeksjon.",
+ "caseNamePlaceholder": "Eksport av inspeksjon - {{date}}",
+ "exportingButton": "Eksporterer...",
+ "toast": {
+ "partial": "Startet {{successful}} av {{total}} eksporter. Feilet: {{failedItems}}",
+ "failed": "Kunne ikke starte {{total}} eksporter. Feilet: {{failedItems}}",
+ "started_one": "Startet 1 eksport. Åpner saken nå.",
+ "started_other": "Startet {{count}} eksporter. Åpner saken nå.",
+ "startedNoCase_one": "Startet 1 eksport.",
+ "startedNoCase_other": "Startet {{count}} eksporter."
+ },
+ "title_one": "Eksporter 1 inspeksjon",
+ "title_other": "Eksporter {{count}} inspeksjon",
+ "exportButton_one": "Eksporter 1 inspeksjon",
+ "exportButton_other": "Eksporter {{count}} inspeksjon"
}
},
"streaming": {
@@ -125,6 +188,14 @@
"markAsReviewed": "Merk som inspisert",
"deleteNow": "Slett nå",
"markAsUnreviewed": "Merk som ikke inspisert"
+ },
+ "shareTimestamp": {
+ "description": "Del en tidsstemplet URL fra avspillerens nåværende posisjon, eller velg et egendefinert tidsstempel. Merk at dette ikke er en offentlig delingslenke, og at den kun er tilgjengelig for brukere med tilgang til Frigate og dette kameraet.",
+ "custom": "Egendefinert tidsstempel",
+ "title": "Del tidsstempel",
+ "label": "Del tidsstempel",
+ "button": "Del URL med tidsstempel",
+ "shareTitle": "Tidsstempel for Frigate-inspeksjon: {{camera}}"
}
},
"imagePicker": {
diff --git a/web/public/locales/nb-NO/components/player.json b/web/public/locales/nb-NO/components/player.json
index b08459cfcf..31e8e28b4a 100644
--- a/web/public/locales/nb-NO/components/player.json
+++ b/web/public/locales/nb-NO/components/player.json
@@ -32,7 +32,8 @@
"noPreviewFoundFor": "Ingen forhåndsvisning funnet for {{cameraName}}",
"submitFrigatePlus": {
"title": "Send dette bildet til Frigate+?",
- "submit": "Send"
+ "submit": "Send",
+ "previewError": "Kunne ikke laste forhåndsvisning av stillbilde. Opptaket er kanskje ikke tilgjengelig for øyeblikket."
},
"livePlayerRequiredIOSVersion": "iOS 17.1 eller høyere kreves for denne typen direkte-strømming.",
"streamOffline": {
@@ -47,5 +48,6 @@
"error": {
"submitFrigatePlusFailed": "Kunne ikke sende bildet til Frigate+"
}
- }
+ },
+ "cameraOff": "Kameraet er av"
}
diff --git a/web/public/locales/nb-NO/config/cameras.json b/web/public/locales/nb-NO/config/cameras.json
index ef94b6f352..5f8c818459 100644
--- a/web/public/locales/nb-NO/config/cameras.json
+++ b/web/public/locales/nb-NO/config/cameras.json
@@ -52,7 +52,7 @@
"description": "Innstillinger for å aktivere og kontrollere varslinger for dette kameraet."
},
"audio": {
- "label": "Lydhendelser",
+ "label": "Lyddeteksjon",
"enabled": {
"label": "Aktiver lyddeteksjon",
"description": "Aktiver eller deaktiver deteksjon av lydhendelser for dette kameraet."
@@ -67,11 +67,15 @@
},
"listen": {
"label": "Lyttetyper",
- "description": "Liste over typer lydhendelser som skal detekteres (f.eks. bjeff, brannalarm, skrik, tale, rop)."
+ "description": "Liste over typer lydhendelser som skal detekteres (f.eks. bjeff, brannalarm, tale, rop)."
},
"filters": {
"label": "Lydfiltre",
- "description": "Filterinnstillinger per lydtype, som konfidensterskler for å redusere falske positive."
+ "description": "Filterinnstillinger per lydtype, som konfidensterskler for å redusere falske positive.",
+ "threshold": {
+ "description": "Minimum konfidens-terskel for at lydhendelsen skal bli registrert.",
+ "label": "Minimum konfidens for lyd"
+ }
},
"enabled_in_config": {
"label": "Opprinnelig lydstatus",
@@ -160,11 +164,11 @@
}
},
"ffmpeg": {
- "label": "FFmpeg",
+ "label": "Strømmer (FFmpeg)",
"description": "FFmpeg-innstillinger, inkludert sti til binærfil, argumenter, alternativer for maskinvareakselerasjon og utdata-argumenter per rolle.",
"path": {
"label": "FFmpeg-sti",
- "description": "Sti til FFmpeg-binærfilen som skal brukes, eller et versjonsalias (\"5.0\" eller \"7.0\")."
+ "description": "Sti til FFmpeg-binærfilen som skal brukes, eller et versjonsalias (\"7.0\" eller \"8.0\")."
},
"global_args": {
"label": "Globale FFmpeg-argumenter",
@@ -442,7 +446,7 @@
},
"mode": {
"label": "Bevaringsmodus",
- "description": "Modus for bevaring: all (alle), motion (bevegelse) eller active_objects (aktive objekter)."
+ "description": "Modus for bevaring: \"Alle\" (lagre alle segmenter), \"Bevegelse\" (lagre segmenter med bevegelse) eller \"Aktive objekter\" (lagre segmenter med aktive objekter)."
}
}
},
@@ -466,7 +470,7 @@
},
"mode": {
"label": "Bevaringsmodus",
- "description": "Modus for bevaring: all (lagre alle segmenter), motion (lagre segmenter med bevegelse) eller active_objects (lagre segmenter med aktive objekter)."
+ "description": "Modus for bevaring: \"Alle\" (lagre alle segmenter), \"Bevegelse\" (lagre segmenter med bevegelse) eller \"Aktive objekter\" (lagre segmenter med aktive objekter)."
}
}
},
@@ -476,6 +480,13 @@
"hwaccel_args": {
"label": "Argumenter for maskinvareakselerasjon ved eksport",
"description": "Argumenter for maskinvareakselerasjon som skal brukes ved eksport og transkoding."
+ },
+ "max_concurrent": {
+ "description": "Maksimalt antall eksportjobber som kan behandles samtidig.",
+ "label": "Maksimalt antall samtidige eksporter"
+ },
+ "chapters": {
+ "label": "Kapittelmetadata som skal bygges inn i eksporterte opptak"
}
},
"preview": {
@@ -619,7 +630,7 @@
},
"mode": {
"label": "Bevaringsmodus",
- "description": "Modus for bevaring: all (lagre alle segmenter), motion (lagre segmenter med bevegelse) eller active_objects (lagre segmenter med aktive objekter)."
+ "description": "Modus for bevaring: \"Alle\" (lagre alle segmenter), \"Bevegelse\" (lagre segmenter med bevegelse) eller \"Aktive objekter\" (lagre segmenter med aktive objekter)."
},
"objects": {
"label": "Objektbevaring",
@@ -666,7 +677,7 @@
"label": "Effekt for tidsstempel",
"description": "Visuell effekt for tidsstempelteksten (none, solid, shadow)."
},
- "description": "Stilalternativer for tidsstempler i strømmen, brukt på opptak og stillbilder."
+ "description": "Stilalternativer for tidsstempler brukt på stillbilder og i Feilsøkingsvisning."
},
"audio_transcription": {
"label": "Lydtranskripsjon",
@@ -840,7 +851,7 @@
"label": "Opprinnelig kamerastatus"
},
"friendly_name": {
- "description": "Kamerats visningsnavn i Frigate-grensesnittet",
+ "description": "Kameraets visningsnavn i Frigate-grensesnittet",
"label": "Visningsnavn"
},
"label": "Kamerakonfigurasjon",
@@ -862,6 +873,10 @@
"order": {
"description": "Numerisk rekkefølge for sortering av kameraet i grensesnittet; høyere tall vises senere.",
"label": "Rekkefølge i UI"
+ },
+ "review": {
+ "description": "Velg om dette kameraet skal være synlig i inspeksjonen (inspeksjonssiden og dens kamerafilter, bevegelsesinspeksjon og historikkvisning).",
+ "label": "Vis i inspeksjon"
}
},
"webui_url": {
diff --git a/web/public/locales/nb-NO/config/global.json b/web/public/locales/nb-NO/config/global.json
index d123063200..5f50981a88 100644
--- a/web/public/locales/nb-NO/config/global.json
+++ b/web/public/locales/nb-NO/config/global.json
@@ -5,7 +5,7 @@
},
"safe_mode": {
"label": "Trygg modus",
- "description": "Når aktivert, start Frigate i trygg modus med reduserte funksjoner for feilsøking."
+ "description": "Når aktivert, start Frigate i trygg modus med redusert funksjonalitet for feilsøking."
},
"environment_vars": {
"label": "Miljøvariabler",
@@ -15,7 +15,7 @@
"label": "Logging",
"description": "Kontrollerer standard loggdetaljnivå og overstyringer av loggnivå per komponent.",
"default": {
- "label": "Loggnivå",
+ "label": "Loggenivå",
"description": "Standard globale loggedetaljer (debug, info, warning, error)."
},
"logs": {
@@ -212,7 +212,7 @@
},
"default_role": {
"label": "Standardrolle",
- "description": "Standardrolle tildelt proxy-autentiserte brukere når ingen rolletilordning gjelder (admin eller viewer)."
+ "description": "Standardrolle tildelt proxy-autentiserte brukere når ingen rolletilordning gjelder."
},
"separator": {
"label": "Skilletegn",
@@ -242,7 +242,7 @@
"description": "Aktiver overvåking av nettverksbåndbredde per prosess for kamera-ffmpeg-prosesser og detektorer."
},
"intel_gpu_device": {
- "label": "SR-IOV-enhet",
+ "label": "Intel GPU-enhet",
"description": "Enhetsidentifikator som brukes når Intel-GPU-er behandles som SR-IOV for å korrigere GPU-statistikk."
}
},
@@ -527,7 +527,7 @@
},
"roles": {
"label": "Roller",
- "description": "GenAI-roller (verktøy, bildeforståelse/syn, vektorrepresentasjoner); én leverandør per rolle."
+ "description": "GenAI-roller (chat, beskrivelser, vektorrepresentasjoner); én leverandør per rolle."
},
"provider_options": {
"label": "Leverandøralternativer",
@@ -539,7 +539,7 @@
}
},
"audio": {
- "label": "Lydhendelser",
+ "label": "Lyddeteksjon",
"description": "Innstillinger for lydbasert hendelsesdeteksjon for alle kameraer; kan overstyres per kamera.",
"enabled": {
"label": "Aktiver lyddeteksjon",
@@ -555,11 +555,15 @@
},
"listen": {
"label": "Lyttetyper",
- "description": "Liste over typer lydhendelser som skal detekteres (f.eks. bjeff, brannalarm, skrik, tale, rop)."
+ "description": "Liste over typer lydhendelser som skal detekteres (f.eks. bjeff, brannalarm, tale, rop)."
},
"filters": {
"label": "Lydfiltre",
- "description": "Filterinnstillinger per lydtype, som konfidensterskler for å redusere falske positive."
+ "description": "Filterinnstillinger per lydtype, som konfidensterskler for å redusere falske positive.",
+ "threshold": {
+ "description": "Minimum konfidens-terskel for at lydhendelsen skal bli registrert.",
+ "label": "Minimum konfidens for lyd"
+ }
},
"enabled_in_config": {
"label": "Opprinnelig lydstatus",
@@ -687,7 +691,7 @@
"description": "FFmpeg-innstillinger, inkludert sti til binærfil, argumenter, alternativer for maskinvareakselerasjon og utdata-argumenter per rolle.",
"path": {
"label": "FFmpeg-sti",
- "description": "Sti til FFmpeg-binærfilen som skal brukes, eller et versjonsalias (\"5.0\" eller \"7.0\")."
+ "description": "Sti til FFmpeg-binærfilen som skal brukes, eller et versjonsalias (\"7.0\" eller \"8.0\")."
},
"global_args": {
"label": "Globale FFmpeg-argumenter",
@@ -917,6 +921,41 @@
"label": "Opprinnelig GenAI-status",
"description": "Indikerer om GenAI var aktivert i den opprinnelige statiske konfigurasjonen."
}
+ },
+ "filters_attribute": {
+ "label": "Attributtfiltre",
+ "description": "Filtre som brukes på detekterte attributter for å redusere falske positiver (område, sideforhold, konfidens).",
+ "min_area": {
+ "label": "Minimum attributtområde",
+ "description": "Minimum areal for markeringsrammen(piksler eller prosent) som kreves for dette attributtet. Kan oppgis i piksler (heltall) eller prosent (desimaltall mellom 0.000001 og 0.99)."
+ },
+ "max_area": {
+ "label": "Maksimum attributtområde",
+ "description": "Maksimum areal for markeringsrammen (piksler eller prosent) tillatt for dette attributtet. Kan oppgis i piksler (heltall) eller prosent (desimaltall mellom 0.000001 og 0.99)."
+ },
+ "min_ratio": {
+ "label": "Minimum sideforhold",
+ "description": "Minimum bredde-/høydeforhold som kreves for at markeringsrammen skal kvalifisere."
+ },
+ "max_ratio": {
+ "label": "Maksimum sideforhold",
+ "description": "Maksimum bredde-/høydeforhold tillatt for at markeringsrammen skal kvalifisere."
+ },
+ "threshold": {
+ "label": "Konfidensterskel",
+ "description": "Gjennomsnittlig terskel for deteksjonskonfidens som kreves for at attributtet skal anses som en ekte positiv."
+ },
+ "min_score": {
+ "label": "Minimum konfidens",
+ "description": "Minimum deteksjonskonfidens i et enkeltbilde som kreves for å knytte dette attributtet til sitt hovedobjekt."
+ },
+ "mask": {
+ "label": "Filtermaske",
+ "description": "Polygonkoordinater som definerer hvor denne masken gjelder innenfor bildet."
+ },
+ "raw_mask": {
+ "label": "Råmaske"
+ }
}
},
"record": {
@@ -966,7 +1005,7 @@
},
"mode": {
"label": "Bevaringsmodus",
- "description": "Modus for bevaring: all (alle), motion (bevegelse) eller active_objects (aktive objekter)."
+ "description": "Modus for bevaring: \"Alle\" (lagre alle segmenter), \"Bevegelse\" (lagre segmenter med bevegelse) eller \"Aktive objekter\" (lagre segmenter med aktive objekter)."
}
}
},
@@ -990,7 +1029,7 @@
},
"mode": {
"label": "Bevaringsmodus",
- "description": "Modus for bevaring: all (lagre alle segmenter), motion (lagre segmenter med bevegelse) eller active_objects (lagre segmenter med aktive objekter)."
+ "description": "Modus for bevaring: \"Alle\" (lagre alle segmenter), \"Bevegelse\" (lagre segmenter med bevegelse) eller \"Aktive objekter\" (lagre segmenter med aktive objekter)."
}
}
},
@@ -1000,6 +1039,13 @@
"hwaccel_args": {
"label": "Argumenter for maskinvareakselerasjon ved eksport",
"description": "Argumenter for maskinvareakselerasjon som skal brukes ved eksport og transkoding."
+ },
+ "max_concurrent": {
+ "description": "Maksimalt antall eksportjobber som kan behandles samtidig.",
+ "label": "Maksimalt antall samtidige eksporter"
+ },
+ "chapters": {
+ "label": "Kapittelmetadata som skal bygges inn i eksporterte opptak"
}
},
"preview": {
@@ -1143,7 +1189,7 @@
},
"mode": {
"label": "Bevaringsmodus",
- "description": "Modus for bevaring: all (lagre alle segmenter), motion (lagre segmenter med bevegelse) eller active_objects (lagre segmenter med aktive objekter)."
+ "description": "Modus for bevaring: \"Alle\" (lagre alle segmenter), \"Bevegelse\" (lagre segmenter med bevegelse) eller \"Aktive objekter\" (lagre segmenter med aktive objekter)."
},
"objects": {
"label": "Objektbevaring",
@@ -1511,6 +1557,10 @@
"dashboard": {
"label": "Vis i grensesnitt",
"description": "Velg om dette kameraet skal være synlig i Frigate-grensesnittet. Deaktivering krever manuell redigering av konfigurasjonen for å vise kameraet igjen."
+ },
+ "review": {
+ "label": "Vis i inspeksjon",
+ "description": "Velg om dette kameraet skal være synlig i inspeksjon (inspeksjonssiden og dens kamerafilter, bevegelsesinspeksjon og historikkvisning)."
}
},
"onvif": {
diff --git a/web/public/locales/nb-NO/config/validation.json b/web/public/locales/nb-NO/config/validation.json
index e9e34a202c..afc1e4b332 100644
--- a/web/public/locales/nb-NO/config/validation.json
+++ b/web/public/locales/nb-NO/config/validation.json
@@ -28,5 +28,8 @@
}
},
"minLength": "Må være minst {{limit}} tegn",
- "maxLength": "Må være maks {{limit}} tegn"
+ "maxLength": "Må være maks {{limit}} tegn",
+ "detect": {
+ "dimensionMustBeEven": "Må være et partall."
+ }
}
diff --git a/web/public/locales/nb-NO/objects.json b/web/public/locales/nb-NO/objects.json
index eb4b3ee36d..ab352714fc 100644
--- a/web/public/locales/nb-NO/objects.json
+++ b/web/public/locales/nb-NO/objects.json
@@ -121,5 +121,10 @@
"skunk": "Skunk",
"school_bus": "Skolebuss",
"royal_mail": "Royal Mail",
- "canada_post": "Canada Post"
+ "canada_post": "Canada Post",
+ "baby_stroller": "Barnevogn",
+ "Rodent": "Gnager",
+ "baby": "Baby",
+ "rickshaw": "Rickshaw",
+ "rodent": "Gnager"
}
diff --git a/web/public/locales/nb-NO/views/chat.json b/web/public/locales/nb-NO/views/chat.json
new file mode 100644
index 0000000000..fcef10748a
--- /dev/null
+++ b/web/public/locales/nb-NO/views/chat.json
@@ -0,0 +1,72 @@
+{
+ "documentTitle": "Chat - Frigate",
+ "title": "Frigate Chat",
+ "subtitle": "Din AI-assistent for kamerahåndtering og innsikt",
+ "placeholder": "Spør om hva som helst...",
+ "error": "Noe gikk galt. Vennligst prøv igjen.",
+ "processing": "Behandler...",
+ "toolsUsed": "Brukt: {{tools}}",
+ "showTools": "Vis verktøy ({{count}})",
+ "hideTools": "Skjul verktøy",
+ "call": "Kall",
+ "result": "Resultat",
+ "arguments": "Argumenter:",
+ "response": "Svar:",
+ "attachment_chip_label": "{{label}} på {{camera}}",
+ "attachment_chip_remove": "Fjern vedlegg",
+ "open_in_explore": "Åpne i Utforsk",
+ "attach_event_aria": "Legg ved hendelse {{eventId}}",
+ "attachment_picker_paste_label": "Eller lim inn hendelses-ID",
+ "attachment_picker_attach": "Legg ved",
+ "attachment_picker_placeholder": "Legg ved en hendelse",
+ "quick_reply_find_similar": "Finn lignende observasjoner",
+ "quick_reply_tell_me_more": "Fortell meg mer om dette",
+ "quick_reply_when_else": "Når ellers ble det sett?",
+ "quick_reply_find_similar_text": "Finn lignende observasjoner som denne.",
+ "quick_reply_tell_me_more_text": "Fortell meg mer om denne.",
+ "quick_reply_when_else_text": "Når ellers ble denne sett?",
+ "anchor": "Referanse",
+ "similarity_score": "Likhet",
+ "no_similar_objects_found": "Ingen lignende objekter funnet.",
+ "semantic_search_required": "Semantisk søk må være aktivert for å finne lignende objekter.",
+ "send": "Send",
+ "suggested_requests": "Prøv å spørre:",
+ "starting_requests": {
+ "show_recent_events": "Vis nylige hendelser",
+ "show_camera_status": "Vis kamerastatus",
+ "recap": "Hva skjedde mens jeg var borte?",
+ "watch_camera": "Overvåk et kamera for aktivitet"
+ },
+ "starting_requests_prompts": {
+ "show_recent_events": "Vis meg nylige hendelser fra den siste timen",
+ "show_camera_status": "Hva er status for kameraene mine akkurat nå?",
+ "recap": "Hva skjedde mens jeg var borte?",
+ "watch_camera": "Overvåk inngangsdøren og gi meg beskjed hvis noen dukker opp"
+ },
+ "new_chat": "Ny chat",
+ "settings": {
+ "title": "Chat-innstillinger",
+ "show_stats": {
+ "title": "Vis statistikk",
+ "desc": "Vis genereringshastighet og kontekststørrelse for chat-svar.",
+ "while_generating": "Under generering",
+ "always": "Alltid"
+ },
+ "auto_scroll": {
+ "title": "Auto-rulling",
+ "desc": "Følg nye meldinger etter hvert som de ankommer."
+ }
+ },
+ "stats": {
+ "context": "{{tokens}} tokens",
+ "tokens_per_second": "{{rate}} t/s"
+ },
+ "reasoning": {
+ "active": "Resonnerer…",
+ "show": "Vis resonnering",
+ "hide": "Skjul resonnering"
+ },
+ "thinking": {
+ "toggle": "Aktiver/deaktiver tenking"
+ }
+}
diff --git a/web/public/locales/nb-NO/views/configEditor.json b/web/public/locales/nb-NO/views/configEditor.json
index df0cd00a9a..2aa6fade07 100644
--- a/web/public/locales/nb-NO/views/configEditor.json
+++ b/web/public/locales/nb-NO/views/configEditor.json
@@ -1,5 +1,5 @@
{
- "documentTitle": "Konfigurasjonseditor - Frigate",
+ "documentTitle": "Konfigurasjonsfil - Frigate",
"toast": {
"error": {
"savingError": "Feil ved lagring av konfigurasjon"
@@ -8,11 +8,11 @@
"copyToClipboard": "Konfigurasjonen ble kopiert til utklippstavlen."
}
},
- "configEditor": "Konfig-editor",
+ "configEditor": "Konfigurasjonsfil",
"copyConfig": "Kopier konfigurasjonen",
"saveAndRestart": "Lagre og omstart",
"saveOnly": "Kun lagre",
"confirm": "Avslutt uten å lagre?",
- "safeConfigEditor": "Konfig-editor (Sikker modus)",
+ "safeConfigEditor": "Konfigurasjonsfil (Sikker modus)",
"safeModeDescription": "Frigate er i sikker modus grunnet en feil i validering av konfigurasjonen."
}
diff --git a/web/public/locales/nb-NO/views/events.json b/web/public/locales/nb-NO/views/events.json
index d1c3b02de5..4a46f3f973 100644
--- a/web/public/locales/nb-NO/views/events.json
+++ b/web/public/locales/nb-NO/views/events.json
@@ -31,7 +31,9 @@
"timeline.aria": "Velg tidslinje",
"documentTitle": "Inspeksjon - Frigate",
"recordings": {
- "documentTitle": "Opptak - Frigate"
+ "documentTitle": "Opptak - Frigate",
+ "invalidSharedLink": "Kunne ikke åpne tidsstemplet opptakslenke på grunn av tolkningsfeil.",
+ "invalidSharedCamera": "Kunne ikke åpne tidsstemplet opptakslenke på grunn av et ukjent eller uautorisert kamera."
},
"calendarFilter": {
"last24Hours": "Siste 24 timer"
@@ -39,7 +41,7 @@
"markAsReviewed": "Merk som inspisert",
"markTheseItemsAsReviewed": "Merk disse elementene som inspiserte",
"selected_one": "{{count}} valgt",
- "selected_other": "{{count}} valgt",
+ "selected_other": "{{count}} valgte",
"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 6aac95d76e..1e3b485bfe 100644
--- a/web/public/locales/nb-NO/views/explore.json
+++ b/web/public/locales/nb-NO/views/explore.json
@@ -287,7 +287,10 @@
"zones": "Soner",
"ratio": "Sideforhold",
"area": "Område",
- "score": "Score"
+ "score": "Score",
+ "computedScore": "Beregnet score",
+ "topScore": "Toppscore",
+ "toggleAdvancedScores": "Vis/skjul avanserte scoringer"
}
},
"annotationSettings": {
diff --git a/web/public/locales/nb-NO/views/exports.json b/web/public/locales/nb-NO/views/exports.json
index 481750f5c3..d455fd8c5c 100644
--- a/web/public/locales/nb-NO/views/exports.json
+++ b/web/public/locales/nb-NO/views/exports.json
@@ -14,7 +14,9 @@
"toast": {
"error": {
"renameExportFailed": "Kunne ikke gi nytt navn til eksport: {{errorMessage}}",
- "assignCaseFailed": "Kunne ikke oppdatere sakstilknytning: {{errorMessage}}"
+ "assignCaseFailed": "Kunne ikke oppdatere sakstilknytning: {{errorMessage}}",
+ "caseSaveFailed": "Kunne ikke lagre sak: {{errorMessage}}",
+ "caseDeleteFailed": "Kunne ikke slette sak: {{errorMessage}}"
}
},
"tooltip": {
@@ -22,7 +24,8 @@
"downloadVideo": "Last ned video",
"editName": "Rediger navn",
"deleteExport": "Slett eksport",
- "assignToCase": "Legg til i sak"
+ "assignToCase": "Legg til i sak",
+ "removeFromCase": "Fjern fra sak"
},
"caseDialog": {
"nameLabel": "Saksnavn",
@@ -35,5 +38,91 @@
"headings": {
"cases": "Saker",
"uncategorizedExports": "Eksporter uten sak"
+ },
+ "toolbar": {
+ "newCase": "Ny sak",
+ "addExport": "Legg til eksport",
+ "editCase": "Rediger sak",
+ "deleteCase": "Slett sak"
+ },
+ "deleteCase": {
+ "label": "Slett sak",
+ "desc": "Er du sikker på at du vil slette {{caseName}}?",
+ "descKeepExports": "Eksporter vil fortsatt være tilgjengelige som eksporter uten sak.",
+ "descDeleteExports": "Alle eksporter i denne saken vil bli slettet permanent.",
+ "deleteExports": "Slett også eksporter"
+ },
+ "caseCard": {
+ "emptyCase": "Ingen eksporter ennå"
+ },
+ "jobCard": {
+ "defaultName": "{{camera}}-eksport",
+ "queued": "Lagt i kø",
+ "running": "Kjører",
+ "preparing": "Forbereder",
+ "copying": "Kopierer",
+ "encoding": "Enkoder",
+ "encodingRetry": "Enkoder (nytt forsøk)",
+ "finalizing": "Fullfører"
+ },
+ "caseView": {
+ "noDescription": "Ingen beskrivelse",
+ "createdAt": "Opprettet {{value}}",
+ "exportCount_one": "1 eksport",
+ "exportCount_other": "{{count}} eksporter",
+ "cameraCount_one": "1 kamera",
+ "cameraCount_other": "{{count}} kameraer",
+ "showMore": "Vis mer",
+ "showLess": "Vis mindre",
+ "emptyTitle": "Denne saken er tom",
+ "emptyDescription": "Legg til eksisterende eksporter uten sak for å holde orden i saken.",
+ "emptyDescriptionNoExports": "Det er ingen ledige eksporter tilgjengelig for å legges til ennå."
+ },
+ "caseEditor": {
+ "createTitle": "Opprett sak",
+ "editTitle": "Rediger sak",
+ "namePlaceholder": "Saksnavn",
+ "descriptionPlaceholder": "Legg til notater eller kontekst for denne saken"
+ },
+ "addExportDialog": {
+ "title": "Legg til eksport i {{caseName}}",
+ "searchPlaceholder": "Søk i eksporter uten sak",
+ "empty": "Ingen eksporter uten sak samsvarer med søket.",
+ "addButton_one": "Legg til 1 eksport",
+ "addButton_other": "Legg til {{count}} eksporter",
+ "adding": "Legger til..."
+ },
+ "selected_one": "{{count}} valgt",
+ "selected_other": "{{count}} valgte",
+ "bulkActions": {
+ "addToCase": "Legg til i sak",
+ "moveToCase": "Flytt til sak",
+ "removeFromCase": "Fjern fra sak",
+ "delete": "Slett",
+ "deleteNow": "Slett nå"
+ },
+ "bulkDelete": {
+ "title": "Slett eksporter",
+ "desc_one": "Er du sikker på at du vil slette {{count}} eksport?",
+ "desc_other": "Er du sikker på at du vil slette {{count}} eksporter?"
+ },
+ "bulkRemoveFromCase": {
+ "title": "Fjern fra sak",
+ "desc_one": "Fjerne {{count}} eksport fra denne saken?",
+ "desc_other": "Fjerne {{count}} eksporter fra denne saken?",
+ "descKeepExports": "Eksporter vil bli flyttet til \"uten sak\".",
+ "descDeleteExports": "Eksporter vil bli slettet permanent.",
+ "deleteExports": "Slett eksporter i stedet"
+ },
+ "bulkToast": {
+ "success": {
+ "delete": "Eksporter ble slettet",
+ "reassign": "Sakstilknytning ble oppdatert",
+ "remove": "Eksporter ble fjernet fra saken"
+ },
+ "error": {
+ "deleteFailed": "Kunne ikke slette eksporter: {{errorMessage}}",
+ "reassignFailed": "Kunne ikke oppdatere sakstilknytning: {{errorMessage}}"
+ }
}
}
diff --git a/web/public/locales/nb-NO/views/faceLibrary.json b/web/public/locales/nb-NO/views/faceLibrary.json
index cf8d81e394..4ea7e819ff 100644
--- a/web/public/locales/nb-NO/views/faceLibrary.json
+++ b/web/public/locales/nb-NO/views/faceLibrary.json
@@ -27,7 +27,11 @@
"aria": "Velg nylige gjenkjennelser",
"title": "Nylige gjenkjennelser",
"empty": "Det er ingen nylige forsøk på ansiktsgjenkjenning",
- "titleShort": "Nylige"
+ "titleShort": "Nylige",
+ "emptyNoLibrary": {
+ "description": "Du må legge til minst ett ansikt i biblioteket for at ansiktsgjenkjenning skal fungere.",
+ "title": "Last opp et ansikt"
+ }
},
"selectFace": "Velg ansikt",
"deleteFaceLibrary": {
diff --git a/web/public/locales/nb-NO/views/live.json b/web/public/locales/nb-NO/views/live.json
index be891769e2..4ba80b0dfa 100644
--- a/web/public/locales/nb-NO/views/live.json
+++ b/web/public/locales/nb-NO/views/live.json
@@ -50,7 +50,9 @@
},
"camera": {
"enable": "Aktiver kamera",
- "disable": "Deaktiver kamera"
+ "disable": "Deaktiver kamera",
+ "turnOn": "Slå kamera på",
+ "turnOff": "Skå kamera av"
},
"snapshots": {
"enable": "Aktiver stillbilder",
@@ -152,7 +154,8 @@
},
"recording": {
"enable": "Aktiver opptak",
- "disable": "Deaktiver opptak"
+ "disable": "Deaktiver opptak",
+ "disabledInConfig": "Opptak må først aktiveres i Innstillinger for dette kameraet."
},
"streamStats": {
"enable": "Vis Strømmestatistikk",
@@ -168,7 +171,8 @@
"snapshots": "Stillbilder",
"audioDetection": "Lydregistrering",
"autotracking": "Automatisk sporing",
- "transcription": "Lydtranskripsjon"
+ "transcription": "Lydtranskripsjon",
+ "camera": "Kamera"
},
"transcription": {
"enable": "Aktiver direkte lydtranskripsjon",
diff --git a/web/public/locales/nb-NO/views/motionSearch.json b/web/public/locales/nb-NO/views/motionSearch.json
new file mode 100644
index 0000000000..79e2a6d312
--- /dev/null
+++ b/web/public/locales/nb-NO/views/motionSearch.json
@@ -0,0 +1,80 @@
+{
+ "documentTitle": "Bevegelsessøk - Frigate",
+ "title": "Bevegelsessøk",
+ "description": "Tegn et polygon for å definere et interesseområde, og angi et tidsrom for å søke etter bevegelsesendringer i dette området.",
+ "selectCamera": "Bevegelsessøk laster",
+ "startSearch": "Start søk",
+ "searchStarted": "Søk startet",
+ "searchCancelled": "Søk avbrutt",
+ "cancelSearch": "Avbryt",
+ "searching": "Søk pågår...",
+ "searchComplete": "Søk fullført",
+ "noResultsYet": "Kjør et søk for å finne bevegelsesendringer i det valgte området",
+ "noChangesFound": "Ingen pikselendringer ble funnet i det valgte området",
+ "changesFound_one": "Fant {{count}} bevegelsesendring",
+ "changesFound_other": "Fant {{count}} bevegelsesendringer",
+ "framesProcessed": "{{count}} bilder behandlet",
+ "jumpToTime": "Gå til dette tidspunktet",
+ "results": "Resultater",
+ "showSegmentHeatmap": "Varmekart",
+ "newSearch": "Nytt søk",
+ "clearResults": "Tøm resultater",
+ "clearROI": "Slett polygon",
+ "polygonControls": {
+ "points_one": "{{count}} punkt",
+ "points_other": "{{count}} punkter",
+ "undo": "Angre siste punkt",
+ "reset": "Tilbakestill polygon",
+ "moveMode": "Flytt",
+ "drawMode": "Tegn"
+ },
+ "motionHeatmapLabel": "Varmekart for bevegelse",
+ "dialog": {
+ "title": "Bevegelsessøk",
+ "cameraLabel": "Kamera",
+ "previewAlt": "Forhåndsvisning av kamera for {{camera}}"
+ },
+ "timeRange": {
+ "title": "Søkeperiode",
+ "start": "Starttid",
+ "end": "Sluttid"
+ },
+ "settings": {
+ "title": "Søkeinnstillinger",
+ "parallelMode": "Parallellmodus",
+ "parallelModeDesc": "Skann flere opptaksintervaller samtidig (raskere, men krever mer dekodingsressurser)",
+ "threshold": "Følsomhetsterskel",
+ "thresholdDesc": "Lavere verdier detekterer mindre endringer (1–255)",
+ "minArea": "Minimum endringsområde",
+ "minAreaDesc": "Minimumsstørrelse for et enkelt bevegelsesområde, som en prosentandel av interesseområdet",
+ "frameSkip": "Bilde-sprang",
+ "frameSkipDesc": "Behandle hvert N-te bilde. Sett denne til kameraets bildefrekvens for å behandle ett bilde i sekundet (f.eks. 5 for et 5 FPS-kamera, 30 for et 30 FPS-kamera). Høyere verdier vil være raskere, men kan gå glipp av korte bevegelseshendelser.",
+ "maxResults": "Maksimalt antall resultater",
+ "maxResultsDesc": "Stopp etter dette antallet samsvarende tidsstempler"
+ },
+ "errors": {
+ "noCamera": "Vennligst velg et kamera",
+ "noROI": "Vennligst tegn et interesseområde",
+ "noTimeRange": "Vennligst velg en tidsperiode",
+ "invalidTimeRange": "Sluttid må være etter starttid",
+ "searchFailed": "Søk mislyktes: {{message}}",
+ "polygonTooSmall": "Polygonet må ha minst 3 punkter",
+ "unknown": "Ukjent feil"
+ },
+ "changePercentage": "{{percentage}} % endret",
+ "metrics": {
+ "title": "Statistikk for søk",
+ "segmentsScanned": "Segmenter skannet",
+ "segmentsProcessed": "Behandlet",
+ "segmentsSkippedInactive": "Hoppet over (ingen aktivitet)",
+ "segmentsSkippedHeatmap": "Hoppet over (manglende ROI-overlapp)",
+ "fallbackFullRange": "Fullskanning som reserve",
+ "framesDecoded": "Bilder dekodet",
+ "wallTime": "Søketid",
+ "segmentErrors": "Segmentfeil",
+ "seconds": "{{seconds}}s",
+ "scanSummary": "{{segments}} segmenter · {{time}}",
+ "minutesSeconds": "{{minutes}}m {{seconds}}s"
+ },
+ "scanning": "Skanner {{time}}"
+}
diff --git a/web/public/locales/nb-NO/views/replay.json b/web/public/locales/nb-NO/views/replay.json
new file mode 100644
index 0000000000..3cf72e2011
--- /dev/null
+++ b/web/public/locales/nb-NO/views/replay.json
@@ -0,0 +1,59 @@
+{
+ "title": "Feilsøkingsavspilling",
+ "description": "Spill av kameraopptak for feilsøking. Objektlisten viser et tidsforsinket sammendrag av detekterte objekter, og fanen Meldinger viser en strøm av Frigates interne meldinger fra avspillingen.",
+ "websocket_messages": "Meldinger",
+ "dialog": {
+ "title": "Start feilsøkingsavspilling",
+ "description": "Opprett et midlertidig avspillingskamera som repeterer historisk materiale for å feilsøke problemer med objektdeteksjon og sporing. Avspillingskameraet vil ha samme deteksjonskonfigurasjon som kildekameraet. Velg et tidsrom for å begynne.",
+ "camera": "Kildekamera",
+ "timeRange": "Tidsrom",
+ "preset": {
+ "1m": "Siste minutt",
+ "5m": "Siste 5 minutter",
+ "timeline": "Fra tidslinje",
+ "custom": "Egendefinert"
+ },
+ "startButton": "Start avspilling",
+ "selectFromTimeline": "Velg",
+ "starting": "Starter avspilling...",
+ "startLabel": "Start",
+ "endLabel": "Slutt",
+ "toast": {
+ "error": "Kunne ikke starte feilsøkingsavspilling: {{error}}",
+ "alreadyActive": "En avspillingsøkt er allerede aktiv",
+ "stopError": "Kunne ikke stoppe feilsøkingsavspilling: {{error}}",
+ "goToReplay": "Gå til avspilling"
+ }
+ },
+ "page": {
+ "noSession": "Ingen aktiv feilsøkingsøkt",
+ "noSessionDesc": "Start en feilsøkingsavspilling fra Historikk-visningen ved å klikke på Handlinger-knappen i verktøylinjen og velge Feilsøkingsavspilling.",
+ "goToRecordings": "Gå til historikk",
+ "preparingClip": "Forbereder klipp…",
+ "preparingClipDesc": "Frigate syr sammen opptak for det valgte tidsrommet. Dette kan ta et minutt for lengre perioder.",
+ "startingCamera": "Starter feilsøkingsavspilling…",
+ "startError": {
+ "title": "Kunne ikke starte feilsøkingsavspilling",
+ "back": "Tilbake til historikk"
+ },
+ "sourceCamera": "Kildekamera",
+ "replayCamera": "Avspillingskamera",
+ "initializingReplay": "Initialiserer feilsøkingsavspilling...",
+ "stoppingReplay": "Stopper feilsøkingsavspilling...",
+ "stopReplay": "Stopp avspilling",
+ "confirmStop": {
+ "title": "Stoppe feilsøkingsavspilling?",
+ "description": "Dette vil stoppe økten og slette alle midlertidige data. Er du sikker?",
+ "confirm": "Stopp avspilling",
+ "cancel": "Avbryt"
+ },
+ "activity": "Aktivitet",
+ "objects": "Objektliste",
+ "audioDetections": "Lyd-deteksjoner",
+ "noActivity": "Ingen aktivitet detektert",
+ "activeTracking": "Aktiv sporing",
+ "noActiveTracking": "Ingen aktiv sporing",
+ "configuration": "Konfigurasjon",
+ "configurationDesc": "Finjuster innstillinger for bevegelsesdeteksjon og objektsporing for feilsøkingskameraet. Ingen endringer lagres i din Frigate-konfigurasjonsfil."
+ }
+}
diff --git a/web/public/locales/nb-NO/views/settings.json b/web/public/locales/nb-NO/views/settings.json
index 3b0f3b4f09..73de4de4fd 100644
--- a/web/public/locales/nb-NO/views/settings.json
+++ b/web/public/locales/nb-NO/views/settings.json
@@ -16,7 +16,8 @@
"globalConfig": "Global konfigurasjon - Frigate",
"cameraConfig": "Kamerakonfigurasjon - Frigate",
"profiles": "Profiler - Frigate",
- "maintenance": "Vedlikehold - Frigate"
+ "maintenance": "Vedlikehold - Frigate",
+ "detectorsAndModel": "Detektorer og modell - Frigate"
},
"menu": {
"classification": "Klassifisering",
@@ -30,7 +31,7 @@
"notifications": "Meldingsvarsler",
"enrichments": "Utvidelser",
"triggers": "Utløsere",
- "cameraManagement": "Administrasjon",
+ "cameraManagement": "Kamerabehandling",
"cameraReview": "Inspeksjon",
"roles": "Roller",
"profiles": "Profiler",
@@ -45,7 +46,7 @@
"systemDetectionModel": "Deteksjonsmodell",
"cameraLivePlayback": "Direkteavspilling",
"globalLivePlayback": "Direkteavspilling",
- "cameraFfmpeg": "FFmpeg",
+ "cameraFfmpeg": "Strømmer (FFmpeg)",
"globalFfmpeg": "FFmpeg",
"systemFfmpeg": "FFmpeg",
"cameraBirdseye": "Fugleperspektiv",
@@ -61,8 +62,8 @@
"cameraLpr": "Kjennemerke-gjenkjenning",
"integrationLpr": "Kjennemerke-gjenkjenning",
"systemLogging": "Logging",
- "cameraAudioEvents": "Lydhendelser",
- "globalAudioEvents": "Lydhendelser",
+ "cameraAudioEvents": "Lyd-deteksjon",
+ "globalAudioEvents": "Lyd-deteksjon",
"cameraAudioTranscription": "Lydtranskripsjon",
"integrationAudioTranscription": "Lydtranskripsjon",
"systemDetectorHardware": "Maskinvare for detektor",
@@ -91,7 +92,8 @@
"system": "System",
"systemTelemetry": "Telemetri",
"systemTls": "TLS",
- "maintenance": "Vedlikehold"
+ "maintenance": "Vedlikehold",
+ "systemDetectorsAndModel": "Detektorer og modell"
},
"dialog": {
"unsavedChanges": {
@@ -721,7 +723,8 @@
"notificationUnavailable": {
"documentation": "Se dokumentasjonen",
"title": "Meldingsvarsler utilgjengelig",
- "desc": "Nettleser push-varsler krever et sikkert miljø (https://…). Dette er en nettleserbegrensning. Få tilgang til Frigate på en sikker måte for å bruke meldingsvarsler."
+ "desc": "Nettleser push-varsler krever et sikkert miljø (https://…). Dette er en nettleserbegrensning. Få tilgang til Frigate på en sikker måte for å bruke meldingsvarsler.",
+ "descPwa": "På iOS er web-pushvarsler kun tilgjengelig når Frigate er installert på hjemskjermen din. Åpne Del -menyen, velg Legg til på hjemskjermen , og åpne deretter Frigate fra det nye ikonet for å registrere denne enheten for varsler."
},
"email": {
"title": "E-post",
@@ -786,11 +789,19 @@
"supportedDetectors": "Støttede detektorer",
"dimensions": "Dimensjoner",
"cameras": "Kameraer",
- "availableModels": "Tilgjengelige modeller",
+ "availableModels": "Tilgjengelige Frigate+ modeller",
"modelSelect": "Dine tilgjengelige modeller på Frigate+ kan velges her. Merk at bare modeller som er kompatible med din nåværende detektorkonfigurasjon kan velges.",
"plusModelType": {
"userModel": "Finjustert",
"baseModel": "Basismodell"
+ },
+ "noModelLoaded": "Ingen Frigate+-modell er lastet inn for øyeblikket.",
+ "selectModel": "Velg en modell",
+ "noModelsAvailable": "Ingen modeller tilgjengelig",
+ "filter": {
+ "ariaLabel": "Filtrer modeller etter type",
+ "baseModels": "Basismodeller",
+ "fineTunedModels": "Finjusterte modeller"
}
},
"title": "Frigate+ Innstillinger",
@@ -817,7 +828,8 @@
"currentModel": "Gjeldende modell",
"configuration": "Konfigurasjon"
},
- "description": "Frigate+ er en abonnementstjeneste som gir tilgang til tilleggsfunksjoner og kapasiteter for Frigate-instansen din, inkludert muligheten til å bruke egendefinerte objektdeteksjonsmodeller trent på dine egne data. Du kan administrere innstillingene for Frigate+-modellen din her."
+ "description": "Frigate+ er en abonnementstjeneste som gir tilgang til tilleggsfunksjoner og kapasiteter for Frigate-instansen din, inkludert muligheten til å bruke egendefinerte objektdeteksjonsmodeller trent på dine egne data. Du kan administrere innstillingene for Frigate+-modellen din her.",
+ "changeInDetectorsAndModel": "Endre modell"
},
"enrichments": {
"title": "Innstillinger for utvidelser",
@@ -1125,7 +1137,7 @@
"brands": {
"reolink-rtsp": "Reolink RTSP anbefales ikke. Aktiver HTTP i kameraets fastvare-innstillinger og start kameraveiviseren på nytt."
},
- "customUrlRtspRequired": "Egendefinerte URL-er må begynne med \"rtsp://\". Manuell konfigurering kreves for kamerastrømmer som ikke bruker RTSP."
+ "customUrlRtspRequired": "Egendefinerte URL-er må begynne med «rtsp://» eller «rtsps://». Manuell konfigurasjon kreves for kamerastrømmer som ikke bruker RTSP."
},
"docs": {
"reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras"
@@ -1341,7 +1353,8 @@
},
"hikvision": {
"substreamWarning": "Substrøm 1 er låst til lav oppløsning. Mange Hikvision-kameraer støtter flere substrømmer som må aktiveres i kameraets innstillinger. Det anbefales å sjekke og benytte disse strømmene hvis de er tilgjengelige."
- }
+ },
+ "resolutionUnknown": "Oppløsningen for denne strømmen kunne ikke fastslås. Du bør angi deteksjonsoppløsningen manuelt i innstillingene eller i konfigurasjonen din."
}
}
},
@@ -1352,13 +1365,45 @@
"selectCamera": "Velg et kamera",
"backToSettings": "Tilbake til kamerainnstillinger",
"streams": {
- "title": "Aktiver / Deaktiver kameraer",
+ "title": "Kamerastatus og detaljer",
"desc": "Midlertidig deaktiver et kamera til Frigate startes på nytt. Deaktivering av et kamera stopper Frigates behandling av dette kameraets strømmer fullstendig. Deteksjon, opptak og feilsøking vil være utilgjengelig. Merk: Dette deaktiverer ikke go2rtc-restrømming. ",
"disableDesc": "Aktiver et kamera som for øyeblikket ikke er synlig i grensesnittet og deaktivert i konfigurasjonen. En omstart av Frigate kreves etter aktivering.",
- "enableSuccess": "Aktiverte {{cameraName}} i konfigurasjonen. Start Frigate på nytt for å ta i bruk endringene.",
+ "enableSuccess": "Aktiverte {{cameraName}}. Start Frigate på nytt for å ta i bruk.",
"enableLabel": "Aktiverte kameraer",
- "enableDesc": "Deaktiver et aktivert kamera midlertidig frem til Frigate starter på nytt. Deaktivering av et kamera stopper all prosessering av kameraets strømmer. Deteksjon, opptak og feilsøking vil være utilgjengelig. Merk: Dette deaktiverer ikke videreformidling (restream) i go2rtc. ",
- "disableLabel": "Deaktiverte kameraer"
+ "enableDesc": "Deaktiver et aktivert kamera midlertidig frem til Frigate starter på nytt. Deaktivering av et kamera stopper all prosessering av kameraets strømmer. Deteksjon, opptak og feilsøking vil være utilgjengelig. Merk: Dette deaktiverer ikke videreformidling (restream) i go2rtc. Dra i feltet for å endre rekkefølgen på kameraene slik de vises i grensesnittet. Rekkefølgen på de aktiverte kameraene vil gjenspeiles i hele grensesnittet, inkludert Live-dashbordet og rullegardinmenyene for kameravalg.",
+ "disableLabel": "Deaktiverte kameraer",
+ "friendlyName": {
+ "edit": "Rediger visningsnavn for kamera",
+ "title": "Rediger visningsnavn",
+ "description": "Angi visningsnavnet som skal brukes for dette kameraet i Frigate-grensesnittet. La feltet stå tomt for å bruke kamera-ID.",
+ "rename": "Omdøp"
+ },
+ "reorderHandle": "Dra for å endre rekkefølge",
+ "saving": "Lagrer…",
+ "saved": "Lagret",
+ "label": "Kamerastatus",
+ "description": "Angi driftsstatus for hvert kamera.På : Strømmer behandles normalt.Av : Setter behandlingen midlertidig på pause. Vedvarer ikke etter omstart av Frigate.Deaktivert : Stopper behandlingen og lagrer endringen i konfigurasjonen din. En omstart kreves for å aktivere et deaktivert kamera igjen.Merk: Deaktivering påvirker ikke videreformidling (restream) i go2rtc. Dra i ikonet for å endre rekkefølgen på aktive kameraer slik de vises i grensesnittet, inkludert Live-dashbordet og rullegardinmenyene for kameravalg.",
+ "disabledSubheading": "Deaktivert i konfigurasjonen",
+ "status": {
+ "on": "På",
+ "off": "Av",
+ "disabled": "Deaktivert"
+ },
+ "disableSuccess": "Deaktiverte {{cameraName}} og lagret i konfigurasjonen.",
+ "details": {
+ "edit": "Rediger kameradetaljer",
+ "title": "Rediger kameradetaljer",
+ "description": "Oppdater visningsnavn, ekstern URL og synlighet brukt for dette kameraet i Frigate-grensesnittet.",
+ "friendlyNameLabel": "Visningsnavn",
+ "friendlyNameHelp": "Vennlig navn som vises for dette kameraet i Frigate-grensesnittet. La stå tomt for å bruke kamera-ID.",
+ "webuiUrlLabel": "URL til kameraets webgrensesnitt",
+ "webuiUrlHelp": "URL for å besøke kameraets webgrensesnitt direkte fra feilsøkingsvisningen (Debug). La stå tomt for å deaktivere lenken.",
+ "webuiUrlInvalid": "Må være en gyldig URL (f.eks. https://example.com).",
+ "dashboardLabel": "Vis på Live-dashbordet",
+ "dashboardHelp": "Vis dette kameraet på Live-dashbordet.",
+ "reviewLabel": "Vis i Inspeksjons-modus",
+ "reviewHelp": "Vis dette kameraet i Inspeksjon (Review), inkludert kamerafilteret, bevegelsesgjennomgang og historikkvisningen."
+ }
},
"cameraConfig": {
"add": "Legg til kamera",
@@ -1393,9 +1438,11 @@
"enabled": "Aktivert",
"inherit": "Arv",
"disabled": "Deaktivert",
- "description": "Konfigurer hvilke kameraer som er aktivert eller deaktivert når en profil aktiveres. Kameraer satt til \"Arv\" beholder sin opprinnelige status.",
+ "description": "Konfigurer hvilke kameraer som slås på eller av når en profil aktiveres. Kameraer satt til «Arv» beholder sin standardstatus.",
"title": "Profiloverstyringer for kamera",
- "selectLabel": "Velg profil"
+ "selectLabel": "Velg profil",
+ "on": "På",
+ "off": "Av"
},
"deleteCameraDialog": {
"confirmTitle": "Er du sikker?",
@@ -1408,7 +1455,101 @@
"description": "Sletting av et kamera vil fjerne alle opptak, sporede objekter og konfigurasjon for dette kameraet permanent. Eventuelle go2rtc-strømmer tilknyttet kameraet må eventuelt fjernes manuelt.",
"selectPlaceholder": "Velg kamera..."
},
- "deleteCamera": "Slett kamera"
+ "deleteCamera": "Slett kamera",
+ "cameraType": {
+ "title": "Kameratype",
+ "label": "Kameratype",
+ "description": "Angi type for hvert kamera. Dedikerte LPR-kameraer er spesialkameraer med kraftig optisk zoom for å fange opp kjennemerker på kjøretøy langt unna. De fleste kameraer bør bruke typen \"Normal\", med mindre kameraet er spesifikt for gjenkjenning av kjennemerker og har et snevert fokus på kjennemerker.",
+ "normal": "Normal",
+ "dedicatedLpr": "Dedikert LPR (lesing av kjennemerker)",
+ "saveSuccess": "Kameratype oppdatert for {{cameraName}}. Start Frigate på nytt for å bruke endringene."
+ },
+ "description": "Legg til, rediger og slett kameraer, kontroller statusen til hvert kamera, og konfigurer overstyringer per profil og kameratype. For å konfigurere strømmer, deteksjon, bevegelse og andre kameraspesifikke innstillinger, velg den aktuelle seksjonen under Kamerakonfigurasjon.",
+ "clone": {
+ "sectionTitle": "Klon innstillinger",
+ "sectionDescription": "Kopier konfigurasjon fra ett kamera til et annet eller til et nytt kamera.",
+ "button": "Klon innstillinger",
+ "title": "Klon kamerainnstillinger",
+ "description": "Kopier et kameras konfigurasjon til ett eller flere andre kameraer eller til et nytt kamera. Identitet (navn, visningsnavn, URL til webgrensesnitt, visningsrekkefølge) kopieres ikke.",
+ "source": {
+ "label": "Kildekamera",
+ "placeholder": "Velg et kildekamera",
+ "required": "Kildekamera må velges"
+ },
+ "target": {
+ "legend": "Mål",
+ "newRadio": "Nytt kamera",
+ "newNameLabel": "Kameranavn",
+ "newNamePlaceholder": "f.eks. bakdoer eller Bakdør",
+ "newNameRequired": "Kameranavn kreves",
+ "newNameInvalid": "Ugyldig kameranavn",
+ "newNameCollision": "Et kamera med dette navnet eksisterer allerede",
+ "newStreamsForced": "Strømmer kopieres alltid for et nytt kamera.",
+ "existingCamerasRadio": "Eksisterende kameraer",
+ "allCameras": "Alle kameraer",
+ "existingPlaceholder": "Velg minst ett kamera",
+ "existingDisabled": "Ingen andre kameraer å kopiere til"
+ },
+ "categories": {
+ "legend": "Innstillinger som skal klones",
+ "description": "Velg hvilke innstillinger som skal kopieres fra kildekameraet.",
+ "selectAll": "Velg alle",
+ "selectNone": "Velg ingen",
+ "resetDefaults": "Tilbakestill til standardverdier",
+ "general": "Generelt",
+ "spatial": "Romlige innstillinger",
+ "streams": "Strømmer",
+ "spatialWarningTitle": "Uoverensstemmelse i oppløsning",
+ "spatialWarning": "Kildekameraet {{srcCamera}} sin deteksjonsoppløsning ({{srcWidth}}×{{srcHeight}}) er forskjellig fra: {{cameras}}. Polygoner vil kanskje ikke samsvare på disse kameraene. Disse er deaktivert som standard; aktiver for å kopiere som de er.",
+ "restartHint": "Omstart kreves",
+ "items": {
+ "record": "Opptak pågår",
+ "snapshots": "Øyeblikksbilder",
+ "review": "Inspeksjon",
+ "motion": "Bevegelsesdeteksjon",
+ "objects": "Objekter",
+ "audio": "Lyddeteksjon",
+ "audio_transcription": "Lydtranskripsjon",
+ "notifications": "Varsler",
+ "birdseye": "Fugleperspektiv",
+ "mqtt": "MQTT",
+ "timestamp_style": "Tidsstempelstil",
+ "onvif": "ONVIF",
+ "lpr": "Kjennemerke gjenkjenning",
+ "face_recognition": "Ansiktsgjenkjenning",
+ "semantic_search": "Semantisk søk",
+ "genai": "Generativ AI",
+ "type": "Kameratype (normal / dedikert LPR)",
+ "profiles": "Profiler",
+ "detect": "Deteksjonsdimensjoner",
+ "zones": "Soner",
+ "motion_mask": "Bevegelsesmasker",
+ "object_masks": "Objektmasker",
+ "ffmpeg_live": "Strømme-URL-er og roller"
+ }
+ },
+ "footer": {
+ "restartNeeded": "Omstart vil kreves for enkelte endringer.",
+ "liveOnly": "Alle endringer vil trå i kraft direkte uten omstart.",
+ "submit": "Klon",
+ "submitting": "Kloner…",
+ "changeCount_one": "{{count}} endring vil bli brukt",
+ "changeCount_other": "{{count}} endringer vil bli brukt"
+ },
+ "toast": {
+ "success": "Innstillinger kopiert til {{cameraName}}",
+ "successWithRestart": "Innstillinger kopiert til {{cameraName}}. Start Frigate på nytt for å ta i bruk alle endringer.",
+ "partialFailure": "{{successCount}} seksjoner ble tatt i bruk; «{{failedSection}}» feilet: {{errorMessage}}",
+ "partialFailureMulti": "Kopiert til {{successCount}} kamera(er); feilet for {{failed}}: {{errorMessage}}",
+ "newCameraPartialFailure": "Kameraet {{cameraName}} ble opprettet, men noen innstillinger kunne ikke kopieres: {{errorMessage}}",
+ "sourceMissing": "Kildekameraet eksisterer ikke lenger",
+ "submitError": "Kunne ikke klone kamera: {{errorMessage}}",
+ "successMulti_one": "Innstillinger kopiert til {{count}} kamera",
+ "successMulti_other": "Innstillinger kopiert til {{count}} kameraer",
+ "successMultiWithRestart_one": "Innstillinger kopiert til {{count}} kamera. Start Frigate på nytt for å aktivere alle endringene.",
+ "successMultiWithRestart_other": "Innstillinger kopiert til {{count}} kameraer. Start Frigate på nytt for å bruke alle endringene."
+ }
+ }
},
"cameraReview": {
"title": "Innstillinger for kamerainspeksjon",
@@ -1572,7 +1713,9 @@
"options": {
"embeddings": "Vektorrepresentasjoner",
"tools": "Verktøy",
- "vision": "Bildegjenkjenning"
+ "vision": "Bildegjenkjenning",
+ "descriptions": "Beskrivelser",
+ "chat": "Chat"
}
},
"additionalProperties": {
@@ -1627,16 +1770,82 @@
"addCustomLabel": "Legg til egendefinert etikett...",
"restartRequiredField": "Omstart påkrevd",
"cameraInputs": {
- "itemTitle": "Strøm {{index}}"
+ "itemTitle": "Strøm {{index}}",
+ "sourceMode": {
+ "restream": "Videreformidling / Restream (go2rtc)",
+ "manual": "Manuell kildesti",
+ "go2rtcStreamLabel": "go2rtc-strøm",
+ "go2rtcStreamPlaceholder": "Velg en go2rtc-strøm",
+ "noGo2rtcStreams": "Ingen go2rtc-strømmer er konfigurert",
+ "go2rtcStreamSearch": "Søk i strømmer...",
+ "availableStreams": "Tilgjengelige strømmer",
+ "noMatchingStreams": "Ingen matchende strømmer"
+ }
},
"searchPlaceholder": "Søk...",
- "showAdvanced": "Vis avanserte innstillinger"
+ "showAdvanced": "Vis avanserte innstillinger",
+ "genaiModel": {
+ "placeholder": "Velg eller skriv inn en modell…",
+ "search": "Søk eller skriv inn en modell…",
+ "noModels": "Ingen modeller tilgjengelig",
+ "available": "Tilgjengelige modeller",
+ "useCustom": "Bruk «{{value}}»",
+ "refresh": "Oppdater modeller",
+ "probeFailed": "Kunne ikke hente modeller",
+ "fetchedModels": "Modelliste ble hentet"
+ },
+ "knownPlates": {
+ "platePlaceholder": "Kjennemerke nummer eller regex",
+ "namePlaceholder": "f.eks konas bil"
+ },
+ "liveStreams": {
+ "streamNameLabel": "Strømnavn",
+ "streamNamePlaceholder": "f.eks. Hovedstrøm HD",
+ "go2rtcStreamLabel": "go2rtc-strøm",
+ "go2rtcStreamPlaceholder": "Velg en go2rtc-strøm",
+ "go2rtcStreamSearch": "Søk eller skriv inn et strømnavn…",
+ "noGo2rtcStreams": "Ingen go2rtc-strømmer er konfigurert",
+ "availableStreams": "Tilgjengelige strømmer",
+ "useCustom": "Bruk «{{value}}»",
+ "addStream": "Legg til strøm"
+ },
+ "semanticSearchModelSize": {
+ "notApplicable": "Ikke relevant for leverandører av generativ AI"
+ },
+ "ptzPresets": {
+ "placeholder": "Velg eller skriv inn en forhåndsinnstilling...",
+ "search": "Søk eller skriv inn en forhåndsinnstilling...",
+ "noPresets": "Ingen forhåndsinnstillinger tilgjengelig",
+ "available": "Forhåndsinnstillinger for kamera",
+ "useCustom": "Bruk «{{value}}»"
+ },
+ "defaultRole": {
+ "admin": "Administrator",
+ "viewer": "Seer (Viewer)"
+ }
},
"button": {
"overriddenBaseConfigTooltip": "{{profile}}-profilen overstyrer konfigurasjonsinnstillinger i denne seksjonen",
"overriddenGlobalTooltip": "Dette kameraet overstyrer globale konfigurasjonsinnstillinger i denne seksjonen",
"overriddenBaseConfig": "Overstyrt (Basiskonfigurasjon)",
- "overriddenGlobal": "Overstyrt (Global)"
+ "overriddenGlobal": "Overstyrt (Global)",
+ "overriddenInCameras": {
+ "label_one": "Overstyrt i {{count}} kamera",
+ "label_other": "Overstyrt i {{count}} kameraer",
+ "tooltip_one": "{{count}} kamera overstyrer verdier i denne seksjonen. Klikk for å se detaljer.",
+ "tooltip_other": "{{count}} kameraer overstyrer verdier i denne seksjonen. Klikk for å se detaljer.",
+ "heading_one": "Denne globale seksjonen har felt som er overstyrt i {{count}} kamera.",
+ "heading_other": "Denne globale seksjonen har felt som er overstyrt i {{count}} kameraer.",
+ "othersField_one": "{{count}} annen",
+ "othersField_other": "{{count}} andre",
+ "profilePrefix": "{{profile}}-profil: {{fields}}"
+ },
+ "overriddenGlobalHeading_one": "Dette kameraet overstyrer {{count}} felt fra den globale konfigurasjonen:",
+ "overriddenGlobalHeading_other": "Dette kameraet overstyrer {{count}} felt fra den globale konfigurasjonen:",
+ "overriddenGlobalNoDeltas": "Dette kameraet overstyrer den globale konfigurasjonen, men ingen feltverdier er forskjellige.",
+ "overriddenBaseConfigHeading_one": "{{profile}}-profilen overstyrer {{count}} felt fra basiskonfigurasjonen:",
+ "overriddenBaseConfigHeading_other": "{{profile}}-profilen overstyrer {{count}} felt fra basiskonfigurasjonen:",
+ "overriddenBaseConfigNoDeltas": "{{profile}}-profilen overstyrer denne seksjonen, men ingen feltverdier er forskjellige fra basiskonfigurasjonen."
},
"detectionModel": {
"plusActive": {
@@ -1650,7 +1859,7 @@
"go2rtcStreams": {
"description": "Administrer go2rtc-strømkonfigurasjoner for videreformidling av kamera-strømmer. Hver strøm har ett navn og én eller flere kilde-URL-er.",
"ffmpeg": {
- "hardwareAuto": "Automatisk maskinvareakselerasjon",
+ "hardwareAuto": "Automatisk (anbefalt)",
"useFfmpegModule": "Bruk kompatibilitetsmodus (ffmpeg)",
"audioExclude": "Ekskluder",
"videoExclude": "Ekskluder",
@@ -1667,7 +1876,15 @@
"audioPcma": "Transkod til PCM A-law",
"audioPcmu": "Transkod til PCM μ-law",
"audioAac": "Transkod til AAC",
- "video": "Video"
+ "video": "Video",
+ "hardwareVaapi": "VAAPI",
+ "hardwareCuda": "CUDA",
+ "hardwareV4l2m2m": "V4L2 M2M",
+ "hardwareDxva2": "DXVA2",
+ "hardwareVideotoolbox": "VideoToolbox",
+ "addVideoCodec": "Legg til videokodek",
+ "addAudioCodec": "Legg til lydkodek",
+ "removeCodec": "Fjern kodek"
},
"validation": {
"nameDuplicate": "En strøm med dette navnet eksisterer allerede",
@@ -1687,7 +1904,9 @@
"addStreamDesc": "Skriv inn et navn for den nye strømmen. Dette navnet vil bli brukt til å referere til strømmen i kamerakonfigurasjonen din.",
"renameStreamDesc": "Skriv inn et nytt navn for denne strømmen. Endring av navn kan ødelegge for kameraer eller andre strømmer som refererer til den ved navn.",
"deleteStream": "Slett strøm",
- "streamName": "Strømnavn"
+ "streamName": "Strømnavn",
+ "streamNumber": "Strøm {{index}}",
+ "sourceNumber": "Kilde {{index}}"
},
"profiles": {
"active": "Aktiv",
@@ -1735,18 +1954,27 @@
"review": {
"allNonAlertDetections": "All aktivitet som ikke er et varsel, vil bli inkludert som deteksjoner.",
"detectDisabled": "Objektdeteksjon er deaktivert. Inspeksjonselementer krever detekterte objekter for å kategorisere varsler og deteksjoner.",
- "recordDisabled": "Opptak er deaktivert, inspeksjonselementer vil ikke bli generert."
+ "recordDisabled": "Opptak er deaktivert, inspeksjonselementer vil ikke bli generert.",
+ "genaiImageSourceRecordingsRecordDisabled": "Bildekilde er satt til \"opptak\", men opptak er deaktivert. Frigate vil falle tilbake til forhåndsvisningsbilder."
},
"detectors": {
"mixedTypesSuggestion": "Alle detektorer må bruke samme type. Fjern eksisterende detektorer eller velg {{type}}.",
"mixedTypes": "Alle detektorer må bruke samme type. Fjern eksisterende detektorer for å bruke en annen type."
},
"faceRecognition": {
- "globalDisabled": "Ansiktsgjenkjenning er ikke aktivert på globalt nivå. Aktiver det i globale innstillinger for at ansiktsgjenkjenning på kameranivå skal fungere.",
- "personNotTracked": "Ansiktsgjenkjenning krever at objektet 'person' spores. Sørg for at 'person' er i listen over objektsporing."
+ "globalDisabled": "Utvidelse for ansiktsgjenkjenning må være aktivert for at funksjoner for ansiktsgjenkjenning skal fungere på dette kameraet.",
+ "personNotTracked": "Ansiktsgjenkjenning krever at objektet 'person' spores. Aktiver 'person' under Objekter for dette kameraet.",
+ "modelSizeLarge": "Den store (large) modellen krever GPU eller NPU for akseptabel ytelse. Bruk liten (small) på systemer med kun CPU."
},
"detect": {
- "fpsGreaterThanFive": "Det anbefales ikke å sette FPS for deteksjon høyere enn 5."
+ "fpsGreaterThanFive": "Det anbefales ikke å sette FPS for deteksjon høyere enn 5. Høyere verdier kan føre til ytelsesproblemer uten å gi noen fordeler.",
+ "disabled": "Objektdeteksjon er deaktivert. Stillbilder, inspeksjonselementer og utvidelser som ansiktsgjenkjenning, lesing av kjennemerker og generativ AI vil ikke fungere.",
+ "resolutionShouldBeMultipleOfFour": "For best resultat bør deteksjonsbredde og -høyde være delelig med 4. Andre partall kan forårsake visuelle feil eller svak forvrengning i deteksjonsstrømmen.",
+ "aspectRatioMismatch": "Bredden og høyden du har oppgitt samsvarer ikke med sideforholdet til din nåværende deteksjonsoppløsning. Dette kan føre til et strukket eller forvrengt bilde.",
+ "maxFramesSet": "Å angi maksimalt antall bilder (max frames) overstyrer standard oppførsel og deaktiverer sporing av stasjonære objekter. Det er svært få situasjoner hvor dette er nødvendig, så bruk det med forsiktighet.",
+ "squareResolution": "En kvadratisk deteksjonsoppløsning er uvanlig. Deteksjonsbredden og -høyden bør samsvare med kameraets sideforhold (for eksempel 16:9), ikke dimensjonene til objektdeteksjonsmodellen. Et feilaktig sideforhold kan strekke bildet og redusere treffsikkerheten for deteksjon.",
+ "resolutionHigh": "Denne deteksjonsoppløsningen er høyere enn anbefalt og kan føre til økt ressursbruk uten å forbedre deteksjonsnøyaktigheten. En deteksjonsoppløsning på eller under 1080p anbefales for de fleste kameraer.",
+ "globalResolutionMultipleCameras": "En global deteksjonsoppløsning er satt samtidig som flere kameraer er konfigurert. Med mindre alle kameraer deler nøyaktig samme oppløsning og sideforhold, bør deteksjonsbredden og -høyden defineres per kamera for å matche hvert kameras opprinnelige sideforhold."
},
"birdseye": {
"objectsModeDetectDisabled": "Fugleperspektiv er satt til 'objekter'-modus, men objektdeteksjon er deaktivert for dette kameraet. Kameraet vil ikke vises i Fugleperspektiv."
@@ -1764,8 +1992,25 @@
"detectDisabled": "Objektdeteksjon er deaktivert. Stillbilder genereres fra sporede objekter og vil ikke bli opprettet."
},
"lpr": {
- "globalDisabled": "Identifisering av kjennemerker er ikke aktivert på globalt nivå. Aktiver det i globale innstillinger for at identifisering på kameranivå skal fungere.",
- "vehicleNotTracked": "Identifisering av kjnnemerker krever at 'bil' eller 'motorsykkel' spores."
+ "globalDisabled": "Utvidelse for identifisering av kjennemerker må være aktivert for at LPR-funksjoner skal fungere på dette kameraet.",
+ "vehicleNotTracked": "Identifisering av kjennemerker krever at 'bil' eller 'motorsykkel' spores. Aktiver 'bil' eller 'motorsykkel' under Objekter for dette kameraet.",
+ "modelSizeLarge": "Den store (large) modellen er optimalisert for kjennemerker over flere linjer. Den lille (small) modellen gir bedre ytelse og bør brukes med mindre din region bruker skiltformater med flere linjer."
+ },
+ "objects": {
+ "genaiNoDescriptionsProvider": "Du må konfigurere en GenAI-leverandør med rollen \"beskrivelser\" for at beskrivelser skal kunne genereres."
+ },
+ "semanticSearch": {
+ "jinav2SmallModelSize": "Størrelsen \"liten\" med Jina V2-modellen har høyt minnebruk og beregningskostnad. Den \"store\" modellen med en dedikert GPU anbefales."
+ },
+ "model": {
+ "optimizedFor320": "Frigate er optimalisert for en 320x320-modell, som er det beste valget for de fleste oppsett. En 640x640-modell er tregere og hjelper kun i spesifikke scenarier.",
+ "inputDimensionsNotDetectResolution": "Modellens inndata for bredde og høyde er dimensjonene til selve objektdeteksjonsmodellen, ikke kameraets deteksjonsoppløsning. De skal samsvare med dimensjonene til modellen du bruker – vanligvis en kvadratisk størrelse som 320x320 eller 640x640."
+ },
+ "ffmpeg": {
+ "hwaccelManualNotRecommended": "Manuelle argumenter for maskinvareakselerasjon anbefales ikke. Med mindre det foreligger et spesifikt behov, bør du velge forhåndsinnstillingen som samsvarer med maskinvaren din."
+ },
+ "onvif": {
+ "autotrackingNoZones": "Autosporing krever minst én sone. Definer en sone for dette kameraet under Masker / Soner, og angi den deretter som en påkrevd sone nedenfor."
}
},
"maintenance": {
@@ -1830,7 +2075,14 @@
},
"onvif": {
"profileAuto": "Auto",
- "profileLoading": "Laster profiler..."
+ "profileLoading": "Laster profiler...",
+ "autotracking": {
+ "zooming": {
+ "disabled": "Deaktivert",
+ "absolute": "Absolutt",
+ "relative": "Relativ"
+ }
+ }
},
"confirmReset": "Bekreft nullstilling",
"resetToDefaultDescription": "Dette vil nullstille alle innstillinger i denne seksjonen til standardverdiene. Denne handlingen kan ikke angres.",
@@ -1877,7 +2129,9 @@
"saveAllPartial_one": "{{successCount}} av {{totalCount}} seksjoner lagret. {{failCount}} feilet.",
"saveAllPartial_other": "{{successCount}} av {{totalCount}} seksjoner lagret. {{failCount}} feilet.",
"saveAllSuccess_one": "{{count}} seksjon ble lagret.",
- "saveAllSuccess_other": "{{count}} seksjoner ble lagret."
+ "saveAllSuccess_other": "{{count}} seksjoner ble lagret.",
+ "saveAllSuccessRestartRequired_one": "Lagret {{count}} seksjon. Start Frigate på nytt for å aktivere endringene.",
+ "saveAllSuccessRestartRequired_other": "Alle {{count}} seksjoner ble lagret. Start Frigate på nytt for å aktivere endringene."
},
"cameraConfig": {
"toast": {
@@ -1894,5 +2148,104 @@
"bl": "Nederst til venstre",
"tr": "Øverst til høyre",
"tl": "Øverst til venstre"
+ },
+ "birdseye": {
+ "trackingMode": {
+ "objects": "Objekter",
+ "motion": "Bevegelse",
+ "continuous": "Kontinuerlig"
+ },
+ "cameraOrder": {
+ "label": "Kamerarekkefølge",
+ "description": "Dra kameraene for å angi rekkefølgen deres i Fugleperspektiv-oppsettet.",
+ "reorderHandle": "Dra for å endre rekkefølge",
+ "saving": "Lagrer…",
+ "saved": "Lagret"
+ }
+ },
+ "snapshot": {
+ "retainMode": {
+ "all": "Alle",
+ "motion": "Bevegelse",
+ "active_objects": "Aktive objekter"
+ }
+ },
+ "ui": {
+ "timeFormat": {
+ "browser": "Nettleser",
+ "12hour": "12 timer",
+ "24hour": "24 timer"
+ },
+ "TimeOrDateStyle": {
+ "full": "Full",
+ "long": "Lang",
+ "medium": "Middels",
+ "short": "Kort"
+ },
+ "unitSystem": {
+ "metric": "Metrisk",
+ "imperial": "Imperial"
+ }
+ },
+ "review": {
+ "imageSource": {
+ "recordings": "Opptak",
+ "previews": "Forhåndsvisninger"
+ }
+ },
+ "logger": {
+ "logLevel": {
+ "debug": "Debug",
+ "info": "Info",
+ "warning": "Advarsel",
+ "error": "Feil",
+ "critical": "Kritisk"
+ }
+ },
+ "modelSize": {
+ "small": "Liten",
+ "large": "Stor"
+ },
+ "retainMode": {
+ "all": "Alle",
+ "motion": "Bevegelse",
+ "active_objects": "Aktive objekter"
+ },
+ "previewQuality": {
+ "very_high": "Svært høy",
+ "high": "Høy",
+ "medium": "Middels",
+ "low": "Lav",
+ "very_low": "Svært lav"
+ },
+ "menuDot": {
+ "overrideGlobal": "Denne seksjonen overstyrer den globale konfigurasjonen",
+ "overrideProfile": "Denne seksjonen overstyres av {{profile}}-profilen",
+ "unsaved": "Denne seksjonen har ulagrede endringer"
+ },
+ "detectorsAndModel": {
+ "title": "Detektorer og modell",
+ "description": "Konfigurer detektor-bakenden som kjører objektdeteksjon, og modellen den bruker. Endringer lagres sammen slik at detektoren og modellen forblir synkronisert.",
+ "cardTitles": {
+ "detector": "Detektor-maskinvare",
+ "model": "Deteksjonsmodell"
+ },
+ "tabs": {
+ "plus": "Frigate+",
+ "custom": "Egendefinert modell"
+ },
+ "mismatch": {
+ "warning": "Den gjeldende Frigate+-modellen \"{{model}}\" krever {{required}}-detektoren. Velg en kompatibel modell nedenfor eller bytt til Egendefinert modell før du lagrer."
+ },
+ "plusModel": {
+ "requiresDetector": "Krever: {{detector}}",
+ "noModelSelected": "Velg en Frigate+ modell"
+ },
+ "toast": {
+ "saveSuccess": "Innstillinger for detektorer og modell er lagret. Start Frigate på nytt for å aktivere endringene.",
+ "saveError": "Kunne ikke lagre innstillinger for detektorer og modell"
+ },
+ "unsavedChanges": "Ulagrede endringer for detektorer og modell",
+ "restartRequired": "Omstart kreves (detektor eller modell er endret)"
}
}
diff --git a/web/public/locales/nb-NO/views/system.json b/web/public/locales/nb-NO/views/system.json
index 374e6457b6..f849d700e7 100644
--- a/web/public/locales/nb-NO/views/system.json
+++ b/web/public/locales/nb-NO/views/system.json
@@ -170,7 +170,22 @@
"tips": {
"title": "Kamerainformasjon"
},
- "aspectRatio": "bildeforhold"
+ "aspectRatio": "bildeforhold",
+ "keyframes": {
+ "error": "Keyframe-intervallet (~{{seconds}}s) overskrider lengden på opptakssegmentet ({{segmentTime}}s). Enkelte segmenter kan mangle en keyframe, noe som ødelegger avspillingen. Deaktiver smart / +kodek'en på kameraet eller forkort keyframe-intervallet.",
+ "warning": "Spredte eller variable ke,yframes (lengste avstand ~{{seconds}}s), sannsynligvis på grunn av en smart-kodek (H.264+/H.265+). Dette er ikke anbefalt.",
+ "unknown": "Kunne ikke bestemme keyframe-intervallet.",
+ "ok": "Keyframes hvert ~{{seconds}}s, optimalt for opptak og avspilling.",
+ "gap": "Keyframe-intervall (min / snitt / maks):",
+ "keyframeCount": "Registrerte keyframes:",
+ "stillAnalyzing": "Analyserer keyframes fremdeles...",
+ "analyzing": "Analyserer keyframes... {{seconds}} sekunder gjenstår",
+ "title": "Keyframe-analyse",
+ "recordStream": "Opptaksstrøm:",
+ "observedDuration": "Observert varighet:",
+ "segmentLength": "Lengde på opptakssegment:",
+ "recordDisabled": "Opptak er deaktivert for dette kameraet."
+ }
},
"framesAndDetections": "Bilder / Deteksjoner",
"title": "Kameraer",
@@ -210,6 +225,9 @@
"expectedFps": "Forventet BPS",
"reconnectsLastHour": "Gjentatte tilkoblinger (siste time)",
"stallsLastHour": "Avbrudd (siste time)"
+ },
+ "noCameras": {
+ "title": "Ingen kameraer funnet"
}
},
"enrichments": {
diff --git a/web/public/locales/ne/audio.json b/web/public/locales/ne/audio.json
new file mode 100644
index 0000000000..795ce510df
--- /dev/null
+++ b/web/public/locales/ne/audio.json
@@ -0,0 +1,21 @@
+{
+ "speech": "बोली",
+ "bicycle": "साइकल",
+ "yell": "चिच्याउनु",
+ "car": "कार",
+ "bellow": "तलतिर",
+ "motorcycle": "मोटरसाइकल",
+ "whoop": "हुप (Whoop)",
+ "whispering": "सानो बोल्दै",
+ "babbling": "बडबडाउँदै",
+ "bus": "बस",
+ "laughter": "हाँसो",
+ "train": "रेल",
+ "snicker": "स्निकर",
+ "boat": "डुङ्गा",
+ "crying": "रुँदै",
+ "singing": "गाउँदै",
+ "choir": "गायन यन्त्र",
+ "yodeling": "योडेलिङ",
+ "chant": "मन्त्र"
+}
diff --git a/web/public/locales/ne/common.json b/web/public/locales/ne/common.json
new file mode 100644
index 0000000000..ec2203d22c
--- /dev/null
+++ b/web/public/locales/ne/common.json
@@ -0,0 +1,18 @@
+{
+ "time": {
+ "untilForRestart": "फ्रिगेट पुनः सुरु नभएसम्म।",
+ "untilRestart": "पुन: सुरु नभएसम्म",
+ "never": "कहिल्यै होइन",
+ "ago": "{{timeAgo}} अघि",
+ "untilForTime": "{{time}} सम्म",
+ "justNow": "भर्खरै",
+ "today": "आज",
+ "yesterday": "हिजो",
+ "last7": "पछिल्लो ७ दिन",
+ "last14": "पछिल्लो १४ दिन",
+ "last30": "पछिल्लो ३० दिन",
+ "thisWeek": "यो हप्ता",
+ "lastWeek": "गत हप्ता",
+ "thisMonth": "यो महिना"
+ }
+}
diff --git a/web/public/locales/ne/components/auth.json b/web/public/locales/ne/components/auth.json
new file mode 100644
index 0000000000..e61a7b778c
--- /dev/null
+++ b/web/public/locales/ne/components/auth.json
@@ -0,0 +1,16 @@
+{
+ "form": {
+ "user": "प्रयोगकर्ता नाम",
+ "password": "पासवर्ड",
+ "login": "लगइन",
+ "firstTimeLogin": "पहिलो पटक लग इन गर्ने प्रयास गर्दै हुनुहुन्छ? प्रमाणपत्रहरू फ्रिगेट लगहरूमा छापिएका हुन्छन्।",
+ "errors": {
+ "usernameRequired": "प्रयोगकर्ता नाम आवश्यक छ",
+ "passwordRequired": "पासवर्ड आवश्यक छ",
+ "rateLimit": "दर सीमा नाघ्यो। पछि फेरि प्रयास गर्नुहोस्।",
+ "loginFailed": "लगइन असफल भयो",
+ "unknownError": "अज्ञात त्रुटि। लगहरू जाँच गर्नुहोस्",
+ "webUnknownError": "अज्ञात त्रुटि। कन्सोल लगहरू जाँच गर्नुहोस्।"
+ }
+ }
+}
diff --git a/web/public/locales/ne/components/camera.json b/web/public/locales/ne/components/camera.json
new file mode 100644
index 0000000000..59a1682243
--- /dev/null
+++ b/web/public/locales/ne/components/camera.json
@@ -0,0 +1,28 @@
+{
+ "group": {
+ "label": "क्यामेरा समूहहरू",
+ "add": "क्यामेरा समूह थप्नुहोस्",
+ "edit": "क्यामेरा समूह सम्पादन गर्नुहोस्",
+ "delete": {
+ "label": "क्यामेरा समूह मेटाउनुहोस्",
+ "confirm": {
+ "title": "मेटाउने पुष्टि गर्नुहोस्",
+ "desc": "के तपाईं क्यामेरा समूह {{name}} मेटाउन निश्चित हुनुहुन्छ?"
+ }
+ },
+ "name": {
+ "label": "नाम",
+ "placeholder": "नाम प्रविष्ट गर्नुहोस्…",
+ "errorMessage": {
+ "mustLeastCharacters": "क्यामेरा समूहको नाम कम्तिमा २ वर्णको हुनुपर्छ।",
+ "exists": "क्यामेरा समूहको नाम पहिले नै अवस्थित छ।",
+ "nameMustNotPeriod": "क्यामेरा समूहको नाममा पूर्णविराम हुनुहुँदैन।",
+ "invalid": "क्यामेरा समूहको नाम अमान्य छ।"
+ }
+ },
+ "cameras": {
+ "label": "क्यामेराहरू",
+ "desc": "यस समूहको लागि क्यामेराहरू चयन गर्नुहोस्।"
+ }
+ }
+}
diff --git a/web/public/locales/ne/components/dialog.json b/web/public/locales/ne/components/dialog.json
new file mode 100644
index 0000000000..25cdda520d
--- /dev/null
+++ b/web/public/locales/ne/components/dialog.json
@@ -0,0 +1,34 @@
+{
+ "restart": {
+ "title": "के तपाईं फ्रिगेट पुन: सुरु गर्न चाहनुहुन्छ?",
+ "description": "यसले फ्रिगेट पुन: सुरु हुँदा केही समयको लागि रोक्नेछ।",
+ "button": "पुनः सुरु",
+ "restarting": {
+ "title": "फ्रिगेट पुन: सुरु हुँदैछ",
+ "content": "यो पृष्ठ {{countdown}} सेकेन्डमा पुन: लोड हुनेछ।",
+ "button": "अहिले नै जबरजस्ती पुन: लोड गर्नुहोस्"
+ }
+ },
+ "explore": {
+ "plus": {
+ "submitToPlus": {
+ "label": "फ्रिगेट+ मा पेश गर्नुहोस्",
+ "desc": "तपाईंले बेवास्ता गर्न चाहनुभएको स्थानहरूमा रहेका वस्तुहरू गलत सकारात्मक होइनन्। तिनीहरूलाई गलत सकारात्मकको रूपमा पेश गर्नाले मोडेल भ्रमित हुनेछ।"
+ },
+ "review": {
+ "question": {
+ "label": "फ्रिगेट प्लसको लागि यो लेबल पुष्टि गर्नुहोस्",
+ "ask_a": "के यो वस्तु {{label}} हो?",
+ "ask_an": "के यो वस्तु {{label}} हो?",
+ "ask_full": "के यो वस्तु {{untranslatedLabel}} ({{translatedLabel}}) हो?"
+ },
+ "state": {
+ "submitted": "पेश गरियो"
+ }
+ }
+ },
+ "video": {
+ "viewInHistory": "इतिहासमा हेर्नुहोस्"
+ }
+ }
+}
diff --git a/web/public/locales/ne/components/filter.json b/web/public/locales/ne/components/filter.json
new file mode 100644
index 0000000000..40a28af632
--- /dev/null
+++ b/web/public/locales/ne/components/filter.json
@@ -0,0 +1,30 @@
+{
+ "filter": "फिल्टर गर्नुहोस्",
+ "classes": {
+ "label": "कक्षाहरू",
+ "all": {
+ "title": "सबै कक्षाहरू"
+ },
+ "count_one": "{{count}} कक्षा",
+ "count_other": "{{count}} कक्षाहरू"
+ },
+ "labels": {
+ "label": "लेबलहरू",
+ "all": {
+ "title": "सबै लेबलहरू",
+ "short": "लेबलहरू"
+ },
+ "count_one": "{{count}} लेबल",
+ "count_other": "{{count}} लेबलहरू"
+ },
+ "zones": {
+ "label": "क्षेत्रहरू",
+ "all": {
+ "title": "सबै क्षेत्रहरू",
+ "short": "क्षेत्रहरू"
+ }
+ },
+ "dates": {
+ "selectPreset": "प्रिसेट चयन गर्नुहोस्…"
+ }
+}
diff --git a/web/public/locales/ne/components/icons.json b/web/public/locales/ne/components/icons.json
new file mode 100644
index 0000000000..208c39a2a7
--- /dev/null
+++ b/web/public/locales/ne/components/icons.json
@@ -0,0 +1,8 @@
+{
+ "iconPicker": {
+ "selectIcon": "आइकन चयन गर्नुहोस्",
+ "search": {
+ "placeholder": "आइकन खोज्नुहोस्…"
+ }
+ }
+}
diff --git a/web/public/locales/ne/components/input.json b/web/public/locales/ne/components/input.json
new file mode 100644
index 0000000000..c1990b9937
--- /dev/null
+++ b/web/public/locales/ne/components/input.json
@@ -0,0 +1,10 @@
+{
+ "button": {
+ "downloadVideo": {
+ "label": "डाउनलोड भिडियो",
+ "toast": {
+ "success": "तपाईंको समीक्षा वस्तुको भिडियो डाउनलोड हुन थालेको छ।"
+ }
+ }
+ }
+}
diff --git a/web/public/locales/ne/components/player.json b/web/public/locales/ne/components/player.json
new file mode 100644
index 0000000000..ffb97d5402
--- /dev/null
+++ b/web/public/locales/ne/components/player.json
@@ -0,0 +1,26 @@
+{
+ "noPreviewFound": "कुनै पूर्वावलोकन फेला परेन",
+ "noPreviewFoundFor": "{{cameraName}} को लागि कुनै पूर्वावलोकन फेला परेन।",
+ "submitFrigatePlus": {
+ "title": "यो फ्रेम Frigate+ मा बुझाउने हो?",
+ "submit": "पेश गर्नुहोस्",
+ "previewError": "स्न्यापसट पूर्वावलोकन लोड गर्न सकिएन। रेकर्डिङ यस समयमा उपलब्ध नहुन सक्छ।"
+ },
+ "noRecordingsFoundForThisTime": "यस समयको लागि कुनै रेकर्डिङ फेला परेन",
+ "livePlayerRequiredIOSVersion": "यस लाइभ स्ट्रिम प्रकारको लागि iOS १७.१ वा सोभन्दा माथिको संस्करण आवश्यक छ।",
+ "streamOffline": {
+ "title": "अफलाइन स्ट्रिम गर्नुहोस्",
+ "desc": "{{cameraName}} detect स्ट्रिममा कुनै पनि फ्रेमहरू प्राप्त भएका छैनन्, त्रुटि लगहरू जाँच गर्नुहोस्"
+ },
+ "cameraDisabled": "क्यामेरा असक्षम पारिएको छ",
+ "stats": {
+ "streamType": {
+ "title": "स्ट्रिम प्रकार:",
+ "short": "प्रकार"
+ },
+ "bandwidth": {
+ "title": "ब्यान्डविथ:",
+ "short": "ब्यान्डविथ"
+ }
+ }
+}
diff --git a/web/public/locales/ne/config/cameras.json b/web/public/locales/ne/config/cameras.json
new file mode 100644
index 0000000000..3179ad38dd
--- /dev/null
+++ b/web/public/locales/ne/config/cameras.json
@@ -0,0 +1,33 @@
+{
+ "label": "क्यामेरा कन्फिग",
+ "name": {
+ "label": "क्यामेराको नाम",
+ "description": "क्यामेराको नाम आवश्यक छ"
+ },
+ "friendly_name": {
+ "label": "मैत्रीपूर्ण नाम",
+ "description": "फ्रिगेट UI मा प्रयोग गरिएको क्यामेरा मैत्री नाम"
+ },
+ "enabled": {
+ "label": "सक्षम पारिएको",
+ "description": "सक्षम पारिएको"
+ },
+ "audio": {
+ "label": "अडियो पत्ता लगाउने सुविधा",
+ "description": "यस क्यामेराको लागि अडियो-आधारित घटना पत्ता लगाउने सेटिङहरू।",
+ "enabled": {
+ "label": "अडियो पत्ता लगाउने सुविधा सक्षम पार्नुहोस्",
+ "description": "यस क्यामेराको लागि अडियो घटना पत्ता लगाउने सुविधा सक्षम वा असक्षम पार्नुहोस्।"
+ },
+ "max_not_heard": {
+ "label": "समयसीमा समाप्त गर्नुहोस्",
+ "description": "अडियो घटना समाप्त हुनुभन्दा पहिले कन्फिगर गरिएको अडियो प्रकार बिना सेकेन्डको मात्रा।"
+ },
+ "min_volume": {
+ "label": "न्यूनतम भोल्युम"
+ }
+ },
+ "zones": {
+ "label": "क्षेत्रहरू"
+ }
+}
diff --git a/web/public/locales/ne/config/global.json b/web/public/locales/ne/config/global.json
new file mode 100644
index 0000000000..fe3f978e70
--- /dev/null
+++ b/web/public/locales/ne/config/global.json
@@ -0,0 +1,42 @@
+{
+ "version": {
+ "label": "हालको कन्फिगरेसन संस्करण",
+ "description": "माइग्रेसन वा ढाँचा परिवर्तनहरू पत्ता लगाउन मद्दत गर्न सक्रिय कन्फिगरेसनको संख्यात्मक वा स्ट्रिङ संस्करण।"
+ },
+ "safe_mode": {
+ "label": "सुरक्षित मोड",
+ "description": "सक्षम हुँदा, समस्या निवारणको लागि कम सुविधाहरूको साथ सुरक्षित मोडमा फ्रिगेट सुरु गर्नुहोस्।"
+ },
+ "environment_vars": {
+ "label": "वातावरणीय चरहरू",
+ "description": "होम असिस्टेन्ट ओएसमा फ्रिगेट प्रक्रियाको लागि सेट गर्नुपर्ने वातावरण चरहरूको कुञ्जी/मान जोडीहरू। गैर-HAOS प्रयोगकर्ताहरूले यसको सट्टा डकर वातावरण चर कन्फिगरेसन प्रयोग गर्नुपर्छ।"
+ },
+ "logger": {
+ "label": "लगिङ",
+ "description": "पूर्वनिर्धारित लग शब्दावली र प्रति-घटक लग स्तर ओभरराइडहरू नियन्त्रण गर्दछ।",
+ "default": {
+ "label": "लगिङ स्तर",
+ "description": "पूर्वनिर्धारित विश्वव्यापी लग शब्दावली (डिबग, जानकारी, चेतावनी, त्रुटि)।"
+ },
+ "logs": {
+ "label": "प्रति-प्रक्रिया लग स्तर",
+ "description": "विशिष्ट मोड्युलहरूको लागि शब्दावली बढाउन वा घटाउन प्रति-घटक लग स्तर ओभरराइड हुन्छ।"
+ }
+ },
+ "audio": {
+ "label": "अडियो पत्ता लगाउने सुविधा",
+ "enabled": {
+ "label": "अडियो पत्ता लगाउने सुविधा सक्षम पार्नुहोस्"
+ },
+ "max_not_heard": {
+ "label": "समयसीमा समाप्त गर्नुहोस्",
+ "description": "अडियो घटना समाप्त हुनुभन्दा पहिले कन्फिगर गरिएको अडियो प्रकार बिना सेकेन्डको मात्रा।"
+ },
+ "min_volume": {
+ "label": "न्यूनतम भोल्युम"
+ }
+ },
+ "auth": {
+ "label": "प्रमाणीकरण"
+ }
+}
diff --git a/web/public/locales/ne/config/groups.json b/web/public/locales/ne/config/groups.json
new file mode 100644
index 0000000000..f3ea1a4af6
--- /dev/null
+++ b/web/public/locales/ne/config/groups.json
@@ -0,0 +1,44 @@
+{
+ "audio": {
+ "global": {
+ "detection": "विश्वव्यापी पत्ता-लगाउने",
+ "sensitivity": "विश्वव्यापी संवेदनशीलता"
+ },
+ "cameras": {
+ "detection": "पत्ता-लगाउने",
+ "sensitivity": "संवेदनशीलता"
+ }
+ },
+ "timestamp_style": {
+ "global": {
+ "appearance": "विश्वव्यापी उपस्थिति"
+ },
+ "cameras": {
+ "appearance": "उपस्थिति"
+ }
+ },
+ "motion": {
+ "global": {
+ "sensitivity": "विश्वव्यापी संवेदनशीलता",
+ "algorithm": "विश्वव्यापी एल्गोरिथम"
+ },
+ "cameras": {
+ "sensitivity": "संवेदनशीलता",
+ "algorithm": "एल्गोरिथ्म"
+ }
+ },
+ "snapshots": {
+ "global": {
+ "display": "विश्वव्यापी प्रदर्शन"
+ },
+ "cameras": {
+ "display": "प्रदर्शन"
+ }
+ },
+ "detect": {
+ "global": {
+ "resolution": "विश्वव्यापी रिजोल्युसन",
+ "tracking": "विश्वव्यापी ट्र्याकिङ"
+ }
+ }
+}
diff --git a/web/public/locales/ne/config/validation.json b/web/public/locales/ne/config/validation.json
new file mode 100644
index 0000000000..ead6ccf550
--- /dev/null
+++ b/web/public/locales/ne/config/validation.json
@@ -0,0 +1,16 @@
+{
+ "minimum": "कम्तिमा हुनुपर्छ {{limit}}",
+ "maximum": "बढीमा हुनुपर्छ {{limit}}",
+ "exclusiveMinimum": "{{limit}} भन्दा बढी हुनुपर्छ",
+ "exclusiveMaximum": ".{{limit}} भन्दा कम हुनुपर्छ",
+ "minLength": "कम्तिमा {{limit}} वर्ण(हरू) हुनुपर्छ।",
+ "maxLength": "बढीमा {{limit}} वर्ण(हरू) हुनु पर्छ",
+ "minItems": "कम्तिमा {{limit}} वस्तुहरू हुनुपर्छ",
+ "maxItems": "बढीमा {{limit}} वस्तुहरू हुनुपर्छ",
+ "pattern": "अमान्य ढाँचा",
+ "required": "यो क्षेत्र आवश्यक छ",
+ "type": "अमान्य मान प्रकार",
+ "enum": "अनुमति दिइएको मानहरू मध्ये एक हुनुपर्छ",
+ "const": "मान अपेक्षित स्थिरांकसँग मेल खाँदैन",
+ "uniqueItems": "सबै वस्तुहरू अद्वितीय हुनुपर्छ"
+}
diff --git a/web/public/locales/ne/objects.json b/web/public/locales/ne/objects.json
new file mode 100644
index 0000000000..e1826aaad9
--- /dev/null
+++ b/web/public/locales/ne/objects.json
@@ -0,0 +1,16 @@
+{
+ "person": "व्यक्ति",
+ "bicycle": "साइकल",
+ "car": "कार",
+ "motorcycle": "मोटरसाइकल",
+ "airplane": "हवाइजहाज",
+ "bus": "बस",
+ "train": "रेल",
+ "boat": "डुङ्गा",
+ "traffic_light": "ट्राफिक लाइट",
+ "fire_hydrant": "आगो निभाउने यन्त्र",
+ "street_sign": "सडक चिन्ह",
+ "stop_sign": "रोक चिन्ह",
+ "parking_meter": "पार्किङ मिटर",
+ "bench": "बेन्च"
+}
diff --git a/web/public/locales/ne/views/chat.json b/web/public/locales/ne/views/chat.json
new file mode 100644
index 0000000000..1afa99cc5e
--- /dev/null
+++ b/web/public/locales/ne/views/chat.json
@@ -0,0 +1,15 @@
+{
+ "documentTitle": "च्याट - फ्रिगेट",
+ "title": "फ्रिगेट च्याट",
+ "subtitle": "क्यामेरा व्यवस्थापन र अन्तर्दृष्टिको लागि तपाईंको एआई सहायक",
+ "placeholder": "सोध्नुहोस्...",
+ "error": "केही गडबड भयो। कृपया फेरि प्रयास गर्नुहोस्।",
+ "processing": "प्रशोधन गर्दै...",
+ "toolsUsed": "प्रयोग गरिएको: {{tools}}",
+ "showTools": "उपकरणहरू देखाउनुहोस् ({{count}})",
+ "hideTools": "उपकरणहरू लुकाउनुहोस्",
+ "call": "कल गर्नुहोस्",
+ "result": "नतिजा",
+ "arguments": "तर्कहरू:",
+ "response": "प्रतिक्रिया:"
+}
diff --git a/web/public/locales/ne/views/classificationModel.json b/web/public/locales/ne/views/classificationModel.json
new file mode 100644
index 0000000000..e37b8ee94c
--- /dev/null
+++ b/web/public/locales/ne/views/classificationModel.json
@@ -0,0 +1,25 @@
+{
+ "documentTitle": "वर्गीकरण मोडेलहरू - फ्रिगेट",
+ "details": {
+ "scoreInfo": "स्कोरले यस वस्तुको सबै पत्ता लगाउने कार्यहरूमा औसत वर्गीकरण विश्वासलाई प्रतिनिधित्व गर्दछ।",
+ "none": "कुनै पनि होइन",
+ "unknown": "अज्ञात"
+ },
+ "description": {
+ "invalidName": "अमान्य नाम। नामहरूमा अक्षर, संख्या, खाली ठाउँ, अपोस्ट्रोफी, अन्डरस्कोर र हाइफन मात्र समावेश हुन सक्छन्।"
+ },
+ "button": {
+ "deleteClassificationAttempts": "वर्गीकरण छविहरू मेटाउनुहोस्",
+ "renameCategory": "वर्गको नाम बदल्नुहोस्",
+ "deleteCategory": "कक्षा मेटाउनुहोस्",
+ "deleteImages": "छविहरू मेटाउनुहोस्",
+ "trainModel": "रेल मोडेल",
+ "addClassification": "वर्गीकरण थप्नुहोस्",
+ "deleteModels": "मोडेलहरू मेटाउनुहोस्",
+ "editModel": "मोडेल सम्पादन गर्नुहोस्"
+ },
+ "tooltip": {
+ "trainingInProgress": "मोडेल हाल प्रशिक्षणमा छिन्",
+ "noNewImages": "तालिम दिनको लागि कुनै नयाँ तस्बिरहरू छैनन्। पहिले डेटासेटमा थप तस्बिरहरू वर्गीकृत गर्नुहोस्।"
+ }
+}
diff --git a/web/public/locales/ne/views/configEditor.json b/web/public/locales/ne/views/configEditor.json
new file mode 100644
index 0000000000..ac03412677
--- /dev/null
+++ b/web/public/locales/ne/views/configEditor.json
@@ -0,0 +1,18 @@
+{
+ "documentTitle": "कन्फिग सम्पादक - फ्रिगेट",
+ "configEditor": "कन्फिग सम्पादक",
+ "safeConfigEditor": "कन्फिग सम्पादक (सुरक्षित मोड)",
+ "safeModeDescription": "कन्फिग प्रमाणीकरण त्रुटिको कारणले फ्रिगेट सुरक्षित मोडमा छ।",
+ "copyConfig": "कन्फिग प्रतिलिपि गर्नुहोस्",
+ "saveAndRestart": "बचत गर्नुहोस् र पुन: सुरु गर्नुहोस्",
+ "saveOnly": "बचत मात्र",
+ "confirm": "बचत नगरी बाहिर निस्कने हो?",
+ "toast": {
+ "success": {
+ "copyToClipboard": "कन्फिगरेसन क्लिपबोर्डमा प्रतिलिपि गरियो।"
+ },
+ "error": {
+ "savingError": "कन्फिगरेसन बचत गर्दा त्रुटि भयो"
+ }
+ }
+}
diff --git a/web/public/locales/ne/views/events.json b/web/public/locales/ne/views/events.json
new file mode 100644
index 0000000000..b04c4d7f7b
--- /dev/null
+++ b/web/public/locales/ne/views/events.json
@@ -0,0 +1,23 @@
+{
+ "alerts": "अलर्टहरू",
+ "detections": "पत्ता लगाउने",
+ "motion": {
+ "label": "गति",
+ "only": "गति मात्र"
+ },
+ "allCameras": "सबै क्यामेराहरू",
+ "empty": {
+ "alert": "समीक्षा गर्न कुनै अलर्टहरू छैनन्",
+ "detection": "समीक्षा गर्न कुनै पनि पत्ता लगाइएको छैन",
+ "motion": "गतिसम्बन्धी कुनै डेटा फेला परेन",
+ "recordingsDisabled": {
+ "title": "रेकर्डिङहरू सक्षम पारिएको हुनुपर्छ",
+ "description": "क्यामेराको लागि रेकर्डिङ सक्षम पारिएको बेला मात्र समीक्षा वस्तुहरू सिर्जना गर्न सकिन्छ।"
+ }
+ },
+ "timeline": {
+ "label": "समयरेखा",
+ "aria": "टाइमलाइन चयन गर्नुहोस्"
+ },
+ "zoomIn": "जुम इन गर्नुहोस्"
+}
diff --git a/web/public/locales/ne/views/explore.json b/web/public/locales/ne/views/explore.json
new file mode 100644
index 0000000000..80ca127f6f
--- /dev/null
+++ b/web/public/locales/ne/views/explore.json
@@ -0,0 +1,28 @@
+{
+ "details": {
+ "timestamp": "टाइमस्ट्याम्प"
+ },
+ "documentTitle": "अन्वेषण गर्नुहोस् - फ्रिगेट",
+ "generativeAI": "जेनेरेटिभ एआई",
+ "exploreMore": "थप {{label}} वस्तुहरू अन्वेषण गर्नुहोस्",
+ "exploreIsUnavailable": {
+ "title": "अन्वेषण उपलब्ध छैन",
+ "embeddingsReindexing": {
+ "context": "ट्र्याक गरिएका वस्तु इम्बेडिङहरूले पुन: अनुक्रमणिका समाप्त गरेपछि अन्वेषण प्रयोग गर्न सकिन्छ।",
+ "startingUp": "सुरु गर्दै…",
+ "estimatedTime": "अनुमानित बाँकी समय:",
+ "finishingShortly": "चाँडै नै समाप्त हुँदैछ",
+ "step": {
+ "thumbnailsEmbedded": "इम्बेड गरिएका थम्बनेलहरू: ",
+ "descriptionsEmbedded": "इम्बेड गरिएका विवरणहरू: ",
+ "trackedObjectsProcessed": "ट्र्याक गरिएका वस्तुहरू प्रशोधन गरियो: "
+ }
+ },
+ "downloadingModels": {
+ "context": "फ्रिगेटले सिमान्टिक खोज सुविधालाई समर्थन गर्न आवश्यक इम्बेडिङ मोडेलहरू डाउनलोड गर्दैछ। तपाईंको नेटवर्क जडानको गतिमा निर्भर गर्दै यसले धेरै मिनेट लिन सक्छ।",
+ "setup": {
+ "visionModel": "भिजन मोडेल"
+ }
+ }
+ }
+}
diff --git a/web/public/locales/ne/views/exports.json b/web/public/locales/ne/views/exports.json
new file mode 100644
index 0000000000..2afa762d39
--- /dev/null
+++ b/web/public/locales/ne/views/exports.json
@@ -0,0 +1,24 @@
+{
+ "search": "खोज्नुहोस्",
+ "noExports": "कुनै निर्यात भेटिएन",
+ "headings": {
+ "cases": "केसहरू",
+ "uncategorizedExports": "वर्गीकृत नगरिएका निर्यातहरू"
+ },
+ "documentTitle": "निर्यात - फ्रिगेट",
+ "deleteExport": {
+ "label": "निर्यात मेटाउनुहोस्",
+ "desc": "के तपाईं {{exportName}} मेटाउन चाहनुहुन्छ?"
+ },
+ "editExport": {
+ "title": "निर्यातको नाम बदल्नुहोस्",
+ "desc": "यो निर्यातको लागि नयाँ नाम प्रविष्ट गर्नुहोस्।",
+ "saveExport": "निर्यात बचत गर्नुहोस्"
+ },
+ "tooltip": {
+ "shareExport": "निर्यात सेयर गर्नुहोस्",
+ "downloadVideo": "भिडियो डाउनलोड गर्नुहोस्",
+ "editName": "नाम सम्पादन गर्नुहोस्",
+ "deleteExport": "निर्यात मेटाउनुहोस्"
+ }
+}
diff --git a/web/public/locales/ne/views/faceLibrary.json b/web/public/locales/ne/views/faceLibrary.json
new file mode 100644
index 0000000000..1728f993cd
--- /dev/null
+++ b/web/public/locales/ne/views/faceLibrary.json
@@ -0,0 +1,26 @@
+{
+ "description": {
+ "addFace": "आफ्नो पहिलो तस्विर अपलोड गरेर अनुहार पुस्तकालयमा नयाँ संग्रह थप्नुहोस्।",
+ "placeholder": "यो सङ्ग्रहको लागि नाम प्रविष्ट गर्नुहोस्",
+ "invalidName": "अमान्य नाम। नामहरूमा अक्षर, संख्या, खाली ठाउँ, अपोस्ट्रोफी, अन्डरस्कोर र हाइफन मात्र समावेश हुन सक्छन्।",
+ "nameCannotContainHash": "नाममा # हुन सक्दैन।"
+ },
+ "details": {
+ "unknown": "अज्ञात",
+ "timestamp": "टाइमस्ट्याम्प",
+ "scoreInfo": "स्कोर भनेको सबै अनुहारको स्कोरको भारित औसत हो, जुन प्रत्येक छविमा अनुहारको आकारद्वारा भारित हुन्छ।"
+ },
+ "documentTitle": "फेस लाइब्रेरी - फ्रिगेट",
+ "uploadFaceImage": {
+ "title": "अनुहारको छवि अपलोड गर्नुहोस्",
+ "desc": "अनुहारहरू स्क्यान गर्न र {{pageToggle}} को लागि समावेश गर्न एउटा छवि अपलोड गर्नुहोस्"
+ },
+ "collections": "सङ्ग्रहहरू",
+ "createFaceLibrary": {
+ "new": "नयाँ अनुहार सिर्जना गर्नुहोस्",
+ "nextSteps": "बलियो जग निर्माण गर्न:प्रत्येक पत्ता लागेको व्यक्तिको लागि छविहरू चयन गर्न र तालिम दिन हालसालैको पहिचान ट्याब प्रयोग गर्नुहोस्। उत्तम परिणामहरूको लागि सिधा-अन छविहरूमा ध्यान केन्द्रित गर्नुहोस्; कोणमा अनुहारहरू खिच्ने तालिम छविहरूबाट बच्नुहोस्। "
+ },
+ "steps": {
+ "faceName": "अनुहारको नाम प्रविष्ट गर्नुहोस्"
+ }
+}
diff --git a/web/public/locales/ne/views/live.json b/web/public/locales/ne/views/live.json
new file mode 100644
index 0000000000..2a4a191d62
--- /dev/null
+++ b/web/public/locales/ne/views/live.json
@@ -0,0 +1,34 @@
+{
+ "documentTitle": {
+ "default": "प्रत्यक्ष - फ्रिगेट",
+ "withCamera": "{{camera}} - प्रत्यक्ष - फ्रिगेट"
+ },
+ "lowBandwidthMode": "कम-ब्यान्डविथ मोड",
+ "twoWayTalk": {
+ "enable": "दुईतर्फी कुराकानी सक्षम पार्नुहोस्",
+ "disable": "दुईतर्फी कुराकानी असक्षम पार्नुहोस्"
+ },
+ "cameraAudio": {
+ "enable": "क्यामेरा अडियो सक्षम पार्नुहोस्",
+ "disable": "क्यामेरा अडियो असक्षम पार्नुहोस्"
+ },
+ "ptz": {
+ "move": {
+ "clickMove": {
+ "label": "क्यामेरालाई केन्द्रमा राख्न फ्रेममा क्लिक गर्नुहोस्",
+ "enable": "सार्न क्लिक गर्नुहोस् सक्षम पार्नुहोस्",
+ "enableWithZoom": "सार्न क्लिक गर्नुहोस् / जुम गर्न तान्नुहोस् सक्षम गर्नुहोस्",
+ "disable": "सार्न क्लिक गर्ने सुविधा असक्षम पार्नुहोस्"
+ },
+ "left": {
+ "label": "PTZ क्यामेरालाई बायाँतिर सार्नुहोस्"
+ },
+ "up": {
+ "label": "PTZ क्यामेरा माथि सार्नुहोस्"
+ },
+ "down": {
+ "label": "PTZ क्यामेरा तल सार्नुहोस्"
+ }
+ }
+ }
+}
diff --git a/web/public/locales/ne/views/motionSearch.json b/web/public/locales/ne/views/motionSearch.json
new file mode 100644
index 0000000000..22533b1412
--- /dev/null
+++ b/web/public/locales/ne/views/motionSearch.json
@@ -0,0 +1,17 @@
+{
+ "documentTitle": "गति खोज - फ्रिगेट",
+ "title": "गति खोज",
+ "description": "रुचिको क्षेत्र परिभाषित गर्न बहुभुज कोर्नुहोस्, र त्यो क्षेत्र भित्र गति परिवर्तनहरू खोज्नको लागि समय दायरा निर्दिष्ट गर्नुहोस्।",
+ "selectCamera": "गति खोज लोड हुँदैछ",
+ "startSearch": "खोज सुरु गर्नुहोस्",
+ "searchStarted": "खोजी सुरु भयो",
+ "searchCancelled": "खोज रद्द गरियो",
+ "cancelSearch": "रद्द गर्नुहोस्",
+ "searching": "खोजी भइरहेको छ।",
+ "searchComplete": "खोज पूरा भयो",
+ "noResultsYet": "चयन गरिएको क्षेत्रमा चाल परिवर्तनहरू फेला पार्न खोज चलाउनुहोस्",
+ "noChangesFound": "चयन गरिएको क्षेत्रमा कुनै पिक्सेल परिवर्तनहरू फेला परेनन्",
+ "changesFound_one": "{{count}} गति परिवर्तन फेला पर्यो",
+ "changesFound_other": "{{count}} गति परिवर्तनहरू फेला परे",
+ "framesProcessed": "{{count}} फ्रेमहरू प्रशोधन गरियो"
+}
diff --git a/web/public/locales/ne/views/recording.json b/web/public/locales/ne/views/recording.json
new file mode 100644
index 0000000000..03ee1d4b3b
--- /dev/null
+++ b/web/public/locales/ne/views/recording.json
@@ -0,0 +1,12 @@
+{
+ "export": "निर्यात",
+ "filter": "फिल्टर गर्नुहोस्",
+ "calendar": "कैलेंडर",
+ "filters": "फिल्टरहरू",
+ "toast": {
+ "error": {
+ "noValidTimeSelected": "कुनै मान्य समय दायरा चयन गरिएको छैन",
+ "endTimeMustAfterStartTime": "अन्त्य समय सुरु समय पछि हुनुपर्छ"
+ }
+ }
+}
diff --git a/web/public/locales/ne/views/replay.json b/web/public/locales/ne/views/replay.json
new file mode 100644
index 0000000000..a9185bb087
--- /dev/null
+++ b/web/public/locales/ne/views/replay.json
@@ -0,0 +1,20 @@
+{
+ "title": "डिबग उत्तर",
+ "description": "डिबगिङको लागि क्यामेरा रेकर्डिङहरू पुन: प्ले गर्नुहोस्। वस्तु सूचीले पत्ता लगाइएका वस्तुहरूको समय-ढिलाइ भएको सारांश देखाउँछ र सन्देश ट्याबले रिप्ले फुटेजबाट फ्रिगेटको आन्तरिक सन्देशहरूको स्ट्रिम देखाउँछ।",
+ "websocket_messages": "सन्देशहरू",
+ "dialog": {
+ "title": "डिबग रिप्ले सुरु गर्नुहोस्",
+ "description": "वस्तु पत्ता लगाउने र ट्र्याकिङ समस्याहरू डिबग गर्न ऐतिहासिक फुटेज लुप गर्ने अस्थायी रिप्ले क्यामेरा सिर्जना गर्नुहोस्। रिप्ले क्यामेरामा स्रोत क्यामेरा जस्तै पत्ता लगाउने कन्फिगरेसन हुनेछ। सुरु गर्न समय दायरा छनौट गर्नुहोस्।",
+ "camera": "स्रोत क्यामेरा",
+ "timeRange": "समय दायरा",
+ "preset": {
+ "1m": "अन्तिम १ मिनेट",
+ "5m": "अन्तिम ५ मिनेट",
+ "timeline": "टाइमलाइनबाट",
+ "custom": "अनुकूलन"
+ },
+ "startButton": "रिप्ले सुरु गर्नुहोस्",
+ "selectFromTimeline": "चयन गर्नुहोस्",
+ "starting": "रिप्ले सुरु गर्दै..."
+ }
+}
diff --git a/web/public/locales/ne/views/search.json b/web/public/locales/ne/views/search.json
new file mode 100644
index 0000000000..185843f08a
--- /dev/null
+++ b/web/public/locales/ne/views/search.json
@@ -0,0 +1,22 @@
+{
+ "search": "खोज्नुहोस्",
+ "savedSearches": "सुरक्षित गरिएका खोजहरू",
+ "searchFor": "खोज्नुहोस् {{inputValue}}",
+ "button": {
+ "clear": "खोज खाली गर्नुहोस्",
+ "save": "खोज बचत गर्नुहोस्",
+ "delete": "सुरक्षित गरिएको खोज मेटाउनुहोस्",
+ "filterInformation": "फिल्टर जानकारी",
+ "filterActive": "फिल्टरहरू सक्रिय छन्"
+ },
+ "trackedObjectId": "ट्र्याक गरिएको वस्तु ID",
+ "filter": {
+ "label": {
+ "cameras": "क्यामेराहरू",
+ "labels": "लेबलहरू",
+ "zones": "क्षेत्रहरू",
+ "sub_labels": "उप लेबलहरू",
+ "attributes": "विशेषताहरू"
+ }
+ }
+}
diff --git a/web/public/locales/ne/views/settings.json b/web/public/locales/ne/views/settings.json
new file mode 100644
index 0000000000..0ce812e838
--- /dev/null
+++ b/web/public/locales/ne/views/settings.json
@@ -0,0 +1,17 @@
+{
+ "documentTitle": {
+ "default": "सेटिङहरू - फ्रिगेट",
+ "authentication": "प्रमाणीकरण सेटिङहरू - फ्रिगेट",
+ "cameraManagement": "क्यामेराहरू व्यवस्थापन गर्नुहोस् - फ्रिगेट",
+ "cameraReview": "क्यामेरा समीक्षा सेटिङहरू - फ्रिगेट",
+ "enrichments": "संवर्धन सेटिङहरू - फ्रिगेट",
+ "masksAndZones": "मास्क र जोन सम्पादक - फ्रिगेट",
+ "motionTuner": "मोशन ट्युनर - फ्रिगेट",
+ "object": "डिबग - फ्रिगेट",
+ "general": "UI सेटिङहरू - फ्रिगेट",
+ "globalConfig": "विश्वव्यापी कन्फिगरेसन - फ्रिगेट",
+ "cameraConfig": "क्यामेरा कन्फिगरेसन - फ्रिगेट",
+ "frigatePlus": "फ्रिगेट+ सेटिङहरू - फ्रिगेट",
+ "notifications": "सूचना सेटिङहरू - फ्रिगेट"
+ }
+}
diff --git a/web/public/locales/ne/views/system.json b/web/public/locales/ne/views/system.json
new file mode 100644
index 0000000000..e399283e5f
--- /dev/null
+++ b/web/public/locales/ne/views/system.json
@@ -0,0 +1,24 @@
+{
+ "documentTitle": {
+ "cameras": "क्यामेरा तथ्याङ्क - फ्रिगेट",
+ "storage": "भण्डारण तथ्याङ्क - फ्रिगेट",
+ "general": "सामान्य तथ्याङ्क - फ्रिगेट",
+ "enrichments": "संवर्धन तथ्याङ्क - फ्रिगेट",
+ "logs": {
+ "frigate": "फ्रिगेट लगहरू - फ्रिगेट",
+ "go2rtc": "Go2RTC लगहरू - फ्रिगेट",
+ "nginx": "Nginx लगहरू - फ्रिगेट",
+ "websocket": "सन्देश लगहरू - फ्रिगेट"
+ }
+ },
+ "title": "प्रणाली",
+ "metrics": "प्रणाली मेट्रिक्स",
+ "logs": {
+ "websocket": {
+ "label": "सन्देशहरू",
+ "pause": "पज गर्नुहोस्",
+ "resume": "पुनःसुरु गर्नुहोस्",
+ "clear": "खाली गर्नुहोस्"
+ }
+ }
+}
diff --git a/web/public/locales/nl/common.json b/web/public/locales/nl/common.json
index 45b7d93d87..d9630b7928 100644
--- a/web/public/locales/nl/common.json
+++ b/web/public/locales/nl/common.json
@@ -123,7 +123,19 @@
"unselect": "Deselecteren",
"next": "Volgende",
"deleteNow": "Nu verwijderen",
- "continue": "Doorgaan"
+ "continue": "Doorgaan",
+ "add": "Toevoegen",
+ "undo": "Ongedaan maken",
+ "copiedToClipboard": "Gekopieerd naar klembord",
+ "applying": "Verwerken…",
+ "modified": "Gewijzigd",
+ "overridden": "Overschreven",
+ "resetToGlobal": "Reset naar Globaal",
+ "resetToDefault": "Terugzetten naar Standaard",
+ "saveAll": "Alles Opslaan",
+ "savingAll": "Alles aan het opslaan…",
+ "undoAll": "Alles ongedaan maken",
+ "retry": "Opnieuw proberen"
},
"unit": {
"speed": {
@@ -203,7 +215,9 @@
"gl": "Galego (Galicisch)",
"id": "Bahasa Indonesia (Indonesisch)",
"ur": "اردو (Urdu)",
- "hr": "Hrvatski (Kroatisch)"
+ "hr": "Hrvatski (Kroatisch)",
+ "zhHant": "繁體中文 (Traditioneel Chinees)",
+ "bs": "Bosanksi (Bosnisch)"
},
"darkMode": {
"label": "Donkere modus",
@@ -254,7 +268,10 @@
"anonymous": "anoniem"
},
"classification": "Classificatie",
- "profiles": "Profielen"
+ "profiles": "Profielen",
+ "actions": "Acties",
+ "features": "Functies",
+ "chat": "Chat"
},
"toast": {
"copyUrlToClipboard": "URL naar klembord gekopieerd.",
@@ -263,7 +280,8 @@
"error": {
"title": "Opslaan van configuratiewijzigingen mislukt: {{errorMessage}}",
"noMessage": "Het opslaan van configuratiewijzigingen is mislukt"
- }
+ },
+ "success": "De configuratie aanpassingen zijn succesvol opgeslagen."
}
},
"role": {
@@ -307,5 +325,10 @@
"field": {
"optional": "Optioneel",
"internalID": "De interne ID die Frigate gebruikt in de configuratie en database"
- }
+ },
+ "credentialField": {
+ "savedPlaceholder": "Opgeslagen - leeg laten om huidige te behouden"
+ },
+ "no_items": "Geen items",
+ "validation_errors": "Validatie fouten"
}
diff --git a/web/public/locales/nl/components/camera.json b/web/public/locales/nl/components/camera.json
index 1b840478d5..54de6104ea 100644
--- a/web/public/locales/nl/components/camera.json
+++ b/web/public/locales/nl/components/camera.json
@@ -68,7 +68,10 @@
},
"birdseye": "Birdseye"
},
- "icon": "Icon"
+ "icon": "Icon",
+ "showAll": "Toon alle camera groepen",
+ "showLess": "Minder laten zien",
+ "editGroups": "Bewerken Camera Groepen"
},
"debug": {
"options": {
@@ -82,6 +85,7 @@
"zones": "Zones",
"boundingBox": "Objectkader",
"timestamp": "Tijdstempel",
- "regions": "Regio's"
+ "regions": "Regio's",
+ "paths": "Paden"
}
}
diff --git a/web/public/locales/nl/components/dialog.json b/web/public/locales/nl/components/dialog.json
index 0f8cc90521..a69f382f37 100644
--- a/web/public/locales/nl/components/dialog.json
+++ b/web/public/locales/nl/components/dialog.json
@@ -67,17 +67,60 @@
"endTimeMustAfterStartTime": "Eindtijd moet na starttijd zijn"
},
"success": "Export is succesvol gestart. Bekijk het bestand op de exportpagina.",
- "view": "Weergeven"
+ "view": "Weergeven",
+ "queued": "Export staat in de wachtrij. Bekijk voortgang op de exports pagina."
},
"fromTimeline": {
"saveExport": "Export opslaan",
- "previewExport": "Export vooraf bekijken"
+ "previewExport": "Export vooraf bekijken",
+ "useThisRange": "Gebruik dit bereik",
+ "queueingExport": "Export in wachtrij zetten..."
},
"export": "Exporteren",
"selectOrExport": "Selecteren of exporteren",
"case": {
"label": "Dossier",
- "placeholder": "Selecteer een dossier"
+ "placeholder": "Selecteer een dossier",
+ "newCaseOption": "Maak nieuwe case",
+ "newCaseNamePlaceholder": "Nieuwe case naam",
+ "newCaseDescriptionPlaceholder": "Case beschrijving",
+ "nonAdminHelp": "Een nieuwe case wordt gemaakt voor deze exports."
+ },
+ "queueing": "Export in wachtrij zetten...",
+ "tabs": {
+ "export": "Enkele Camera",
+ "multiCamera": "Multi-Camera"
+ },
+ "multiCamera": {
+ "timeRange": "Tijdspanne",
+ "selectFromTimeline": "Selecteer van tijdslijn",
+ "cameraSelection": "Camera's",
+ "cameraSelectionHelp": "Camera’s met getrackte objecten in deze tijdspanne zijn vooraf geselecteerd",
+ "checkingActivity": "Camera-activiteit controleren...",
+ "noCameras": "Geen camera's beschikbaar",
+ "detectionCount_one": "1 gevolgd object",
+ "detectionCount_other": "{{count}} gevolgde objecten",
+ "nameLabel": "Exportnaam",
+ "namePlaceholder": "Optionele basisnaam voor deze exporten",
+ "queueingButton": "Exporten in wachtrij plaatsen...",
+ "exportButton_one": "Export 1 Camera",
+ "exportButton_other": "{{count}} camera's exporteren"
+ },
+ "multi": {
+ "title_one": "Review 1 exporteren",
+ "title_other": "{{count}} reviews exporteren",
+ "description": "Exporteer alle geselecteerde reviews. Alle exports worden samengevoegd in één case.",
+ "descriptionNoCase": "Exporteer elke geselecteerde review.",
+ "toast": {
+ "startedNoCase_one": "1 export gestart.",
+ "startedNoCase_other": "{{count}} Exports gestart.",
+ "partial": "{{successful}} van {{total}} exports gestart. Mislukte items: {{failedItems}}",
+ "failed": "Het starten van {{total}} exports is mislukt. Mislukt: {{failedItems}}"
+ },
+ "exportingButton": "Exporteren...",
+ "exportButton_one": "Exporteer 1 revisie",
+ "exportButton_other": "Exporteer {{count}} revisies",
+ "caseNamePlaceholder": "Inspecteer export - {{date}}"
}
},
"streaming": {
@@ -125,6 +168,14 @@
"error": "Verwijderen mislukt: {{error}}",
"success": "De videobeelden die aan de geselecteerde beoordelingsitems zijn gekoppeld, zijn succesvol verwijderd."
}
+ },
+ "shareTimestamp": {
+ "label": "Tijdstempel delen",
+ "title": "Tijdstempel delen",
+ "description": "Deel een URL met tijdstempel van de huidige positie in de afspeler of kies een aangepaste tijdstempel. Let op: dit is geen openbare deel-URL en is alleen toegankelijk voor gebruikers die toegang hebben tot Frigate en deze camera.",
+ "custom": "Aangepaste tijdstempel",
+ "button": "Deel tijdstempel URL",
+ "shareTitle": "Frigate Beoordelingstijdstempel: {{camera}}"
}
},
"imagePicker": {
diff --git a/web/public/locales/nl/components/player.json b/web/public/locales/nl/components/player.json
index ff0dd10655..511c32eea5 100644
--- a/web/public/locales/nl/components/player.json
+++ b/web/public/locales/nl/components/player.json
@@ -30,7 +30,8 @@
},
"submitFrigatePlus": {
"title": "Dit frame indienen bij Frigate+?",
- "submit": "Indienen"
+ "submit": "Indienen",
+ "previewError": "Het was niet mogelijk om de snapshot preview te laden. De opname is mogelijk niet beschikbaar op dit moment."
},
"streamOffline": {
"title": "Stream is Offline",
@@ -47,5 +48,6 @@
"error": {
"submitFrigatePlusFailed": "Het is niet gelukt om een frame naar Frigate+ te sturen"
}
- }
+ },
+ "cameraOff": "De camera staat uit"
}
diff --git a/web/public/locales/nl/config/cameras.json b/web/public/locales/nl/config/cameras.json
index 96b78e382f..772bd75b5c 100644
--- a/web/public/locales/nl/config/cameras.json
+++ b/web/public/locales/nl/config/cameras.json
@@ -13,27 +13,31 @@
"description": "Geactiveerd"
},
"audio": {
- "label": "Audiogebeurtenissen",
- "description": "Audio-instellingen voor gebeurtenisdetectie van deze camera.",
+ "label": "Audio events",
+ "description": "Instellingen voor audio-gebaseerde detectie voor deze camera.",
"enabled": {
"label": "Geluiddetectie inschakelen",
- "description": "Audio‑gebeurtenisdetectie voor deze camera in- of uitschakelen."
+ "description": "Schakel de detectie van audio-events voor deze camera in of uit."
},
"max_not_heard": {
- "label": "Einde timeout",
- "description": "Hoeveelheid secondes zonder de geconfigureerde audio soort, voordat de geluids gebeurtenis is beindigd."
+ "label": "Einde time-out",
+ "description": "Aantal seconden zonder het geconfigureerde audiotype voordat de audio-event wordt beëindigd."
},
"min_volume": {
- "label": "Minimale volume",
- "description": "Minimale RMS-volumedrempel die nodig is om audiodetectie te starten; Hoe lager de waarde, hoe gevoeliger de detectie (bijvoorbeeld, 200 hoog, 500 gemiddeld, 1000 laag)."
+ "label": "Minimumvolume",
+ "description": "Minimale RMS-volumedrempel die nodig is om audiodetectie te starten; hoe lager de waarde, hoe gevoeliger de detectie (bijvoorbeeld, 200 hoog, 500 gemiddeld, 1000 laag)."
},
"listen": {
"label": "Luistercategorieën",
"description": "Lijst van luistercategorie gebeurtenissen voor detectie (zoals: blaffen, band_alarm, schreeuw, praten, roepen)."
},
"filters": {
- "label": "Geluids filters",
- "description": "Instellingen per audiotype, waaronder betrouwbaarheidsdrempels, ter vermindering van foutieve detecties."
+ "label": "Geluidsfilters",
+ "description": "Instellingen per audiotype, waaronder betrouwbaarheidsdrempels, ter vermindering van foutieve detecties.",
+ "threshold": {
+ "label": "Minimale audiobetrouwbaarheid",
+ "description": "Minimale betrouwbaarheidsdrempel voor de audiogebeurtenis om te worden geteld."
+ }
},
"enabled_in_config": {
"label": "Originele audio-instelling",
@@ -45,7 +49,7 @@
}
},
"audio_transcription": {
- "label": "Audio‑transcriptie",
+ "label": "Audiotranscriptie",
"description": "Instellingen voor live en spraakgestuurde audiotranscriptie voor gebeurtenissen en live ondertitels.",
"enabled": {
"label": "Spraaktranscriptie inschakelen",
@@ -60,14 +64,14 @@
}
},
"birdseye": {
- "label": "Overzichtsweergave",
+ "label": "Birdseye-overzicht",
"description": "Instellingen voor de overzichtsweergave die meerdere camerafeeds combineert tot één lay‑out.",
"enabled": {
- "label": "Activeer overzichtsweergave",
+ "label": "Birdseye-overzicht inschakelen",
"description": "De overzichtsweergavefunctie in- of uitschakelen."
},
"mode": {
- "label": "Volgmodus",
+ "label": "Weergavemodus",
"description": "Modus voor het opnemen van camera’s in overzichtsweergave: ‘objecten’, ‘beweging’ of ‘continu’."
},
"order": {
@@ -76,18 +80,18 @@
}
},
"detect": {
- "label": "Detectie object",
+ "label": "Objectdetectie",
"description": "Instellingen voor de detectierol om objecten te detecteren en trackers te starten.",
"enabled": {
- "label": "Detectie aan",
+ "label": "Detectie inschakelen",
"description": "Objectdetectie voor deze camera in- of uitschakelen. Detectie moet zijn ingeschakeld om objecttracking te laten werken."
},
"height": {
- "label": "Detectie hoogte",
+ "label": "Detectiehoogte",
"description": "De hoogte in pixels van frames voor de detectiestream. Laat dit veld leeg om de standaardresolutie te gebruiken."
},
"width": {
- "label": "Detectie breedte",
+ "label": "Detectiebreedte",
"description": "De breedte in pixels van frames voor de detectiestream. Laat dit veld leeg om de standaardresolutie te gebruiken."
},
"fps": {
@@ -121,10 +125,18 @@
"description": "Standaardlimiet voor het aantal frames dat een stilstaand object wordt gevolgd voordat wordt gestopt."
},
"objects": {
- "label": "Object‑maximum aantal frames",
- "description": "Per‑object overschrijden voor het maximum aantal frames voor tracking van stationaire objecten."
+ "label": "Maximaal aantal frames per object",
+ "description": "Maximum aantal frames per object bij het volgen van stilstaande objecten."
}
+ },
+ "classifier": {
+ "label": "Visuele classifier inschakelen",
+ "description": "Gebruik een visuele classifier om echt stilstaande objecten te detecteren, zelfs wanneer detectiekaders licht verschuiven."
}
+ },
+ "annotation_offset": {
+ "label": "Annotatie-offset",
+ "description": "Milliseconden om detectieannotaties te verschuiven voor betere uitlijning van tijdlijn-detectiekaders met opnames; kan positief of negatief zijn."
}
},
"profiles": {
@@ -148,5 +160,663 @@
"label": "Minimale oppervlakte van het object"
}
}
+ },
+ "mqtt": {
+ "label": "MQTT"
+ },
+ "notifications": {
+ "label": "Meldingen",
+ "enabled": {
+ "label": "Meldingen inschakelen"
+ },
+ "email": {
+ "label": "Melding email",
+ "description": "E-mailadres voor pushmeldingen of vereist door bepaalde meldingsproviders."
+ },
+ "cooldown": {
+ "label": "Wachttijd",
+ "description": "Wachttijd (seconden) tussen meldingen om spammen te voorkomen."
+ },
+ "enabled_in_config": {
+ "label": "Originele meldingsstatus",
+ "description": "Geeft aan of meldingen waren ingeschakeld in de originele statische configuratie."
+ }
+ },
+ "ffmpeg": {
+ "label": "FFmpeg",
+ "description": "FFmpeg-instellingen inclusief binaire pad, argumenten, hardwareversnellingsopties en uitvoerargumenten per rol.",
+ "path": {
+ "label": "FFmpeg-pad",
+ "description": "Pad naar het te gebruiken FFmpeg-binaire bestand of een versie-alias (\"5.0\" of \"7.0\")."
+ },
+ "global_args": {
+ "label": "FFmpeg globale argumenten",
+ "description": "Globale argumenten voor FFmpeg-processen."
+ },
+ "hwaccel_args": {
+ "label": "Hardwareversnellingsargumenten",
+ "description": "Hardwareversnellingsargumenten voor FFmpeg. Provider-specifieke presets worden aanbevolen."
+ },
+ "input_args": {
+ "label": "Invoerargumenten",
+ "description": "Invoerargumenten voor FFmpeg-invoerstromen."
+ },
+ "output_args": {
+ "label": "Uitvoerargumenten",
+ "description": "Standaard uitvoerargumenten voor verschillende FFmpeg-rollen zoals detectie en opname.",
+ "detect": {
+ "label": "Uitvoerargumenten voor detectie",
+ "description": "Standaard uitvoerargumenten voor streams met detectierol."
+ },
+ "record": {
+ "label": "Uitvoerargumenten voor opname",
+ "description": "Standaard uitvoerargumenten voor streams met opnamerol."
+ }
+ },
+ "retry_interval": {
+ "label": "FFmpeg-herverbindingstijd",
+ "description": "Seconden wachten voor een herverbindingspoging na een mislukte camerastream. Standaard is 10."
+ },
+ "apple_compatibility": {
+ "label": "Apple-compatibiliteit",
+ "description": "HEVC-tagging inschakelen voor betere Apple-spelercompatibiliteit bij het opnemen van H.265."
+ },
+ "gpu": {
+ "label": "GPU-index",
+ "description": "Standaard GPU-index voor hardwareversnelling indien beschikbaar."
+ },
+ "inputs": {
+ "label": "Camera-invoer",
+ "description": "Lijst van invoerstream-definities (paden en rollen) voor deze camera.",
+ "path": {
+ "label": "Invoerpad",
+ "description": "URL of pad van de camera-invoerstroom."
+ },
+ "roles": {
+ "label": "Invoerrollen",
+ "description": "Rollen voor deze invoerstroom."
+ },
+ "global_args": {
+ "label": "FFmpeg globale argumenten",
+ "description": "FFmpeg globale argumenten voor deze invoerstroom."
+ },
+ "hwaccel_args": {
+ "label": "Hardwareversnellingsargumenten",
+ "description": "Hardwareversnellingsargumenten voor deze invoerstroom."
+ },
+ "input_args": {
+ "label": "Invoerargumenten",
+ "description": "Invoerargumenten specifiek voor deze stream."
+ }
+ }
+ },
+ "live": {
+ "label": "Live weergave",
+ "streams": {
+ "label": "Live streamnamen",
+ "description": "Koppeling van geconfigureerde streamnamen aan restream/go2rtc-namen voor live weergave."
+ },
+ "height": {
+ "label": "Live hoogte",
+ "description": "Hoogte (pixels) voor weergave van de jsmpeg-livestream in de webinterface; moet ≤ hoogte van de detectiestream zijn."
+ },
+ "quality": {
+ "label": "Live kwaliteit",
+ "description": "Coderingskwaliteit voor de jsmpeg-stream (1 hoogste, 31 laagste)."
+ }
+ },
+ "motion": {
+ "label": "Bewegingsdetectie",
+ "enabled": {
+ "label": "Bewegingsdetectie inschakelen"
+ },
+ "threshold": {
+ "label": "Bewegingsdrempel",
+ "description": "Pixelverschildrempel voor de bewegingsdetector; hogere waarden verminderen de gevoeligheid (bereik 1-255)."
+ },
+ "lightning_threshold": {
+ "label": "Bliksemdrempel",
+ "description": "Drempel om korte lichtflitsen te detecteren en te negeren (lager is gevoeliger, waarden tussen 0,3 en 1,0). Dit voorkomt bewegingsdetectie niet volledig; het zorgt er alleen voor dat de detector stopt met het analyseren van extra frames zodra de drempel wordt overschreden. Op beweging gebaseerde opnames worden tijdens deze gebeurtenissen nog steeds aangemaakt."
+ },
+ "skip_motion_threshold": {
+ "label": "Drempel voor overgeslagen beweging",
+ "description": "Als ingesteld op een waarde tussen 0,0 en 1,0, en meer dan dit deel van het beeld verandert in één frame, geeft de detector geen bewegingsvakken terug en kalibreert hij direct opnieuw. Dit bespaart CPU en vermindert vals-positieven bij bliksem, stormen e.d., maar kan echte gebeurtenissen zoals PTZ-tracking missen. De afweging is tussen het weggooien van enkele megabytes opnames versus het bekijken van een paar korte clips. Leeg laten (None) om deze functie uit te schakelen."
+ },
+ "improve_contrast": {
+ "label": "Contrast verbeteren",
+ "description": "Contrastverbetering op frames toepassen vóór bewegingsanalyse om detectie te verbeteren."
+ },
+ "contour_area": {
+ "label": "Contouroppervlakte",
+ "description": "Minimale contouroppervlakte in pixels voor een bewegingscontour om te worden geteld."
+ },
+ "delta_alpha": {
+ "label": "Delta-alfa",
+ "description": "Alpha-mengfactor voor frameverschil bij bewegingsberekening."
+ },
+ "frame_alpha": {
+ "label": "Frame-alfa",
+ "description": "Alpha-waarde voor het mengen van frames bij bewegingsvoorverwerking."
+ },
+ "frame_height": {
+ "label": "Framehoogte",
+ "description": "Hoogte in pixels waarnaar frames worden geschaald bij het berekenen van beweging."
+ },
+ "mask": {
+ "label": "Maskercoördinaten",
+ "description": "Geordende x,y-coördinaten die het bewegingsmaskeerpolygoon definiëren voor het in- of uitsluiten van gebieden."
+ },
+ "mqtt_off_delay": {
+ "label": "MQTT uit-vertraging",
+ "description": "Seconden wachten na de laatste beweging vóór publicatie van een MQTT 'off'-status."
+ },
+ "enabled_in_config": {
+ "label": "Originele bewegingsstatus",
+ "description": "Geeft aan of bewegingsdetectie was ingeschakeld in de originele statische configuratie."
+ },
+ "raw_mask": {
+ "label": "Onbewerkt masker"
+ }
+ },
+ "objects": {
+ "label": "Objecten",
+ "description": "Standaardinstellingen voor objectvolging, inclusief te volgen labels en per-object filters.",
+ "track": {
+ "label": "Te volgen objecten"
+ },
+ "filters": {
+ "label": "Objectfilters",
+ "description": "Filters op gedetecteerde objecten om vals-positieven te verminderen (oppervlakte, verhouding, betrouwbaarheid).",
+ "min_area": {
+ "label": "Minimale objectoppervlakte",
+ "description": "Minimale detectiekaderoppervlakte (pixels of percentage) voor dit objecttype. Kan pixels (int) of percentage (float tussen 0,000001 en 0,99) zijn."
+ },
+ "max_area": {
+ "label": "Maximale objectoppervlakte",
+ "description": "Maximale detectiekaderoppervlakte (pixels of percentage) voor dit objecttype. Kan pixels (int) of percentage (float tussen 0,000001 en 0,99) zijn."
+ },
+ "min_ratio": {
+ "label": "Minimale beeldverhouding",
+ "description": "Minimale breedte/hoogte-verhouding voor het detectiekader om te kwalificeren."
+ },
+ "max_ratio": {
+ "label": "Maximale beeldverhouding",
+ "description": "Maximale breedte/hoogte-verhouding voor het detectiekader om te kwalificeren."
+ },
+ "threshold": {
+ "label": "Betrouwbaarheidsdrempel",
+ "description": "Gemiddelde detectiebetrouwbaarheidsdrempel om een object als terecht positief te beschouwen."
+ },
+ "min_score": {
+ "label": "Minimale betrouwbaarheid",
+ "description": "Minimale detectiebetrouwbaarheid in één frame om het object te tellen."
+ },
+ "mask": {
+ "label": "Filtermasker",
+ "description": "Polygooncoördinaten die aangeven waar dit filter van toepassing is in het frame."
+ },
+ "raw_mask": {
+ "label": "Onbewerkt masker"
+ }
+ },
+ "mask": {
+ "label": "Objectmasker",
+ "description": "Maskeerpolygoon om objectdetectie in bepaalde gebieden te voorkomen."
+ },
+ "raw_mask": {
+ "label": "Onbewerkt masker"
+ },
+ "genai": {
+ "label": "GenAI-objectconfiguratie",
+ "description": "GenAI-opties voor het beschrijven van gevolgde objecten en het versturen van frames voor generatie.",
+ "enabled": {
+ "label": "GenAI inschakelen",
+ "description": "GenAI-beschrijvingen voor gevolgde objecten standaard inschakelen."
+ },
+ "use_snapshot": {
+ "label": "Snapshots gebruiken",
+ "description": "Objectsnapshots gebruiken in plaats van miniaturen voor GenAI-beschrijving."
+ },
+ "prompt": {
+ "label": "Bijschriftprompt",
+ "description": "Standaard promptsjabloon voor het genereren van beschrijvingen met GenAI."
+ },
+ "object_prompts": {
+ "label": "Objectprompts",
+ "description": "Prompts per object voor het aanpassen van GenAI-uitvoer voor specifieke labels."
+ },
+ "objects": {
+ "label": "GenAI-objecten",
+ "description": "Lijst van objectlabels die standaard naar GenAI worden gestuurd."
+ },
+ "required_zones": {
+ "label": "Vereiste zones",
+ "description": "Zones die objecten moeten betreden om in aanmerking te komen voor GenAI-beschrijving."
+ },
+ "debug_save_thumbnails": {
+ "label": "Snapshots opslaan",
+ "description": "Snapshots die naar GenAI worden gestuurd opslaan voor foutopsporing."
+ },
+ "send_triggers": {
+ "label": "GenAI-triggers",
+ "description": "Bepaalt wanneer frames naar GenAI worden gestuurd (bij einde, na updates, enz.).",
+ "tracked_object_end": {
+ "label": "Sturen bij beëindiging",
+ "description": "Een verzoek naar GenAI sturen wanneer het gevolgde object eindigt."
+ },
+ "after_significant_updates": {
+ "label": "Vroege GenAI-trigger",
+ "description": "Een verzoek naar GenAI sturen na een bepaald aantal significante updates voor het gevolgde object."
+ }
+ },
+ "enabled_in_config": {
+ "label": "Originele GenAI-status",
+ "description": "Geeft aan of GenAI was ingeschakeld in de originele statische configuratie."
+ }
+ }
+ },
+ "record": {
+ "label": "Opname",
+ "enabled": {
+ "label": "Opname inschakelen"
+ },
+ "expire_interval": {
+ "label": "Opruiminterval opnames",
+ "description": "Minuten tussen opruimrondes die verlopen opnamesegmenten verwijderen."
+ },
+ "continuous": {
+ "label": "Continue bewaring",
+ "description": "Aantal dagen om opnames te bewaren ongeacht gevolgde objecten of beweging. Stel 0 in om alleen opnames van meldingen en detecties te bewaren.",
+ "days": {
+ "label": "Bewaardagen",
+ "description": "Dagen om opnames te bewaren."
+ }
+ },
+ "motion": {
+ "label": "Bewegingsretentie",
+ "description": "Aantal dagen om opnames veroorzaakt door beweging te bewaren, ongeacht gevolgde objecten. Stel 0 in om alleen opnames van meldingen en detecties te bewaren.",
+ "days": {
+ "label": "Bewaardagen",
+ "description": "Dagen om opnames te bewaren."
+ }
+ },
+ "detections": {
+ "label": "Detectieretentie",
+ "description": "Opname-retentie-instellingen voor detectiegebeurtenissen inclusief pre/post-captureduur.",
+ "pre_capture": {
+ "label": "Seconden vóór opname",
+ "description": "Aantal seconden vóór de detectiegebeurtenis om op te nemen in de opname."
+ },
+ "post_capture": {
+ "label": "Seconden na opname",
+ "description": "Aantal seconden na de detectiegebeurtenis om op te nemen in de opname."
+ },
+ "retain": {
+ "label": "Gebeurtenisbewaring",
+ "description": "Bewaarinstellingen voor opnames van detectiegebeurtenissen.",
+ "days": {
+ "label": "Bewaardagen",
+ "description": "Aantal dagen om opnames van detectiegebeurtenissen te bewaren."
+ },
+ "mode": {
+ "label": "Bewaarmodus",
+ "description": "Bewaarmodus: all (alle segmenten), motion (segmenten met beweging) of active_objects (segmenten met actieve objecten)."
+ }
+ }
+ },
+ "alerts": {
+ "label": "Meldingsbewaring",
+ "description": "Opname-retentie-instellingen voor alertgebeurtenissen inclusief pre/post-captureduur.",
+ "pre_capture": {
+ "label": "Seconden vóór opname",
+ "description": "Aantal seconden vóór de detectiegebeurtenis om op te nemen in de opname."
+ },
+ "post_capture": {
+ "label": "Seconden na opname",
+ "description": "Aantal seconden na de detectiegebeurtenis om op te nemen in de opname."
+ },
+ "retain": {
+ "label": "Gebeurtenisbewaring",
+ "description": "Bewaarinstellingen voor opnames van detectiegebeurtenissen.",
+ "days": {
+ "label": "Bewaardagen",
+ "description": "Aantal dagen om opnames van detectiegebeurtenissen te bewaren."
+ },
+ "mode": {
+ "label": "Bewaarmodus",
+ "description": "Bewaarmodus: all (alle segmenten), motion (segmenten met beweging) of active_objects (segmenten met actieve objecten)."
+ }
+ }
+ },
+ "export": {
+ "label": "Exportconfiguratie",
+ "description": "Instellingen voor het exporteren van opnames, zoals timelapse en hardwareversnelling.",
+ "hwaccel_args": {
+ "label": "Hardwareversnellingsargumenten voor export",
+ "description": "Hardwareversnellingsargumenten voor export/transcodering."
+ },
+ "max_concurrent": {
+ "label": "Maximaal aantal gelijktijdige exports",
+ "description": "Maximum aantal exporttaken dat tegelijk wordt verwerkt."
+ }
+ },
+ "preview": {
+ "label": "Voorbeeldconfiguratie",
+ "description": "Instellingen voor de kwaliteit van opnamevoorbeelden in de UI.",
+ "quality": {
+ "label": "Voorbeeldkwaliteit",
+ "description": "Kwaliteitsniveau voor voorbeelden (very_low, low, medium, high, very_high)."
+ }
+ },
+ "enabled_in_config": {
+ "label": "Originele opnamestatus",
+ "description": "Geeft aan of opname was ingeschakeld in de originele statische configuratie."
+ }
+ },
+ "review": {
+ "label": "Beoordeling",
+ "alerts": {
+ "label": "Meldingsconfiguratie",
+ "description": "Instellingen voor welke gevolgde objecten alerts genereren en hoe alerts worden bewaard.",
+ "enabled": {
+ "label": "Alerts inschakelen"
+ },
+ "labels": {
+ "label": "Meldingslabels",
+ "description": "Lijst met objectlabels die kwalificeren als meldingen (bijv. auto, persoon)."
+ },
+ "required_zones": {
+ "label": "Vereiste zones",
+ "description": "Zones die een object moet betreden om als melding te worden beschouwd; leeg laten voor elke zone."
+ },
+ "enabled_in_config": {
+ "label": "Originele meldingsstatus",
+ "description": "Geeft aan of meldingen oorspronkelijk waren ingeschakeld in de statische configuratie."
+ },
+ "cutoff_time": {
+ "label": "Afsluitingstijd meldingen",
+ "description": "Seconden wachten na het uitblijven van melding veroorzakende activiteit voordat een melding wordt afgesloten."
+ }
+ },
+ "detections": {
+ "label": "Detectieconfiguratie",
+ "description": "Instellingen voor welke gevolgde objecten detecties genereren en hoe detecties worden bewaard.",
+ "enabled": {
+ "label": "Detecties inschakelen"
+ },
+ "labels": {
+ "label": "Detectielabels",
+ "description": "Lijst met objectlabels die kwalificeren als detectiegebeurtenissen."
+ },
+ "required_zones": {
+ "label": "Vereiste zones",
+ "description": "Zones die een object moet betreden om als detectie te worden beschouwd; leeg laten voor elke zone."
+ },
+ "cutoff_time": {
+ "label": "Afsluitingstijd detecties",
+ "description": "Seconden wachten na het uitblijven van detectie veroorzakende activiteit voordat een detectie wordt afgesloten."
+ },
+ "enabled_in_config": {
+ "label": "Originele detectiestatus",
+ "description": "Geeft aan of detecties oorspronkelijk waren ingeschakeld in de statische configuratie."
+ }
+ },
+ "genai": {
+ "label": "GenAI-configuratie",
+ "description": "Beheert het gebruik van generatieve AI voor het produceren van beschrijvingen en samenvattingen van beoordelingsitems.",
+ "enabled": {
+ "label": "GenAI-beschrijvingen inschakelen",
+ "description": "Door GenAI gegenereerde beschrijvingen en samenvattingen voor beoordelingsitems in- of uitschakelen."
+ },
+ "alerts": {
+ "label": "GenAI inschakelen voor meldingen",
+ "description": "GenAI gebruiken voor het genereren van beschrijvingen bij meldingsitems."
+ },
+ "detections": {
+ "label": "GenAI inschakelen voor detecties",
+ "description": "GenAI gebruiken voor het genereren van beschrijvingen bij detectiebeoordelingen."
+ },
+ "image_source": {
+ "label": "Afbeeldingsbron voor beoordeling",
+ "description": "Bron van afbeeldingen naar GenAI ('preview' of 'recordings'); 'recordings' gebruikt hogere kwaliteit maar meer tokens."
+ },
+ "additional_concerns": {
+ "label": "Aanvullende aandachtspunten",
+ "description": "Een lijst met aanvullende aandachtspunten die GenAI moet meenemen bij het beoordelen van activiteit op deze camera."
+ },
+ "debug_save_thumbnails": {
+ "label": "Snapshots opslaan",
+ "description": "Snapshots die naar de GenAI-provider worden gestuurd opslaan voor foutopsporing."
+ },
+ "enabled_in_config": {
+ "label": "Originele GenAI-status",
+ "description": "Geeft aan of GenAI-beoordeling oorspronkelijk was ingeschakeld in de statische configuratie."
+ },
+ "preferred_language": {
+ "label": "Voorkeurstaal",
+ "description": "Voorkeurstaal voor gegenereerde antwoorden van de GenAI-provider."
+ },
+ "activity_context_prompt": {
+ "label": "Activiteitscontextprompt",
+ "description": "Aangepaste prompt die beschrijft wat wel en niet verdachte activiteit is, als context voor GenAI-samenvattingen."
+ }
+ }
+ },
+ "snapshots": {
+ "label": "Snapshots",
+ "enabled": {
+ "label": "Snapshots inschakelen"
+ },
+ "timestamp": {
+ "label": "Tijdstempel-overlay",
+ "description": "Een tijdstempel op API-snapshots weergeven."
+ },
+ "bounding_box": {
+ "label": "Detectiekader-overlay",
+ "description": "Detectiekaders voor gevolgde objecten tekenen op API-snapshots."
+ },
+ "crop": {
+ "label": "Snapshot bijsnijden",
+ "description": "API-snapshots bijsnijden tot het detectiekader van het gedetecteerde object."
+ },
+ "required_zones": {
+ "label": "Vereiste zones",
+ "description": "Zones die een object moet betreden voordat een snapshot wordt opgeslagen."
+ },
+ "height": {
+ "label": "Snapshothoogte",
+ "description": "Hoogte (pixels) om API-snapshots naar te schalen; leeg laten om de originele grootte te behouden."
+ },
+ "retain": {
+ "label": "Snapshot-bewaring",
+ "description": "Bewaarinstellingen voor snapshots inclusief standaarddagen en per-object overschrijvingen.",
+ "default": {
+ "label": "Standaard retentie",
+ "description": "Standaard aantal dagen om snapshots te bewaren."
+ },
+ "mode": {
+ "label": "Bewaarmodus",
+ "description": "Bewaarmodus: all (alle segmenten), motion (segmenten met beweging) of active_objects (segmenten met actieve objecten)."
+ },
+ "objects": {
+ "label": "Objectbewaring",
+ "description": "Objectspecifieke overschrijvingen voor het aantal bewaardagen van snapshots."
+ }
+ },
+ "quality": {
+ "label": "Snapshotkwaliteit",
+ "description": "Coderingskwaliteit voor opgeslagen snapshots (0-100)."
+ }
+ },
+ "timestamp_style": {
+ "label": "Tijdstempelstijl",
+ "position": {
+ "label": "Tijdstempelpositie",
+ "description": "Positie van de tijdstempel op de afbeelding (tl/tr/bl/br)."
+ },
+ "format": {
+ "label": "Tijdstempelformaat",
+ "description": "Datumtijdformaatstring voor tijdstempels (Python datetime-formaatcodes)."
+ },
+ "color": {
+ "label": "Tijdstempelkleur",
+ "description": "RGB-kleurwaarden voor de tijdstempeltekst (alle waarden 0-255).",
+ "red": {
+ "label": "Rood",
+ "description": "Roodcomponent (0-255) voor de tijdstempelkleur."
+ },
+ "green": {
+ "label": "Groen",
+ "description": "Groencomponent (0-255) voor de tijdstempelkleur."
+ },
+ "blue": {
+ "label": "Blauw",
+ "description": "Blauwcomponent (0-255) voor de tijdstempelkleur."
+ }
+ },
+ "thickness": {
+ "label": "Tijdstempeldikte",
+ "description": "Lijndikte van de tijdstempeltekst."
+ },
+ "effect": {
+ "label": "Tijdstempeleffect",
+ "description": "Visueel effect voor de tijdstempeltekst (geen, effen, schaduw)."
+ }
+ },
+ "semantic_search": {
+ "label": "Semantisch zoeken",
+ "triggers": {
+ "label": "Triggers",
+ "description": "Acties en matchcriteria voor cameraspecifieke semantisch-zoeken-triggers.",
+ "friendly_name": {
+ "label": "Weergavenaam",
+ "description": "Optionele weergavenaam voor deze trigger in de UI."
+ },
+ "enabled": {
+ "label": "Trigger inschakelen",
+ "description": "Deze semantisch-zoeken-trigger in- of uitschakelen."
+ },
+ "type": {
+ "label": "Triggertype",
+ "description": "Type trigger: 'thumbnail' (vergelijk met afbeelding) of 'description' (vergelijk met tekst)."
+ },
+ "data": {
+ "label": "Triggerinhoud",
+ "description": "Tekstzin of miniatuur-ID om te vergelijken met gevolgde objecten."
+ },
+ "threshold": {
+ "label": "Triggerdrempel",
+ "description": "Minimale gelijkenisscore (0-1) om deze trigger te activeren."
+ },
+ "actions": {
+ "label": "Triggeracties",
+ "description": "Lijst van uit te voeren acties bij triggermatch (melding, sub_label, attribuut)."
+ }
+ }
+ },
+ "face_recognition": {
+ "label": "Gezichtsherkenning",
+ "enabled": {
+ "label": "Gezichtsherkenning inschakelen"
+ },
+ "min_area": {
+ "label": "Minimale gezichtsoppervlakte",
+ "description": "Minimale oppervlakte (pixels) van een gedetecteerd gezichtskader om herkenning te proberen."
+ }
+ },
+ "lpr": {
+ "label": "Kentekenherkenning",
+ "description": "Instellingen voor kentekenherkenning inclusief detectiedrempels, opmaak en bekende kentekens.",
+ "enabled": {
+ "label": "LPR inschakelen"
+ },
+ "min_area": {
+ "label": "Minimale kentekenoppervlakte",
+ "description": "Minimale kentekenoppervlakte (pixels) om herkenning te proberen."
+ },
+ "enhancement": {
+ "label": "Verbeteringsniveau",
+ "description": "Verbeteringsniveau (0-10) voor kentekenuitsneden vóór OCR; hogere waarden verbeteren niet altijd het resultaat; niveaus boven 5 werken mogelijk alleen voor nachtelijke kentekens en moeten voorzichtig worden gebruikt."
+ },
+ "expire_time": {
+ "label": "Vervaltijd in seconden",
+ "description": "Tijd in seconden waarna een niet-gezien kenteken vervalt uit de tracker (alleen voor dedicated LPR-camera's)."
+ }
+ },
+ "onvif": {
+ "label": "ONVIF",
+ "description": "ONVIF-verbindings- en PTZ-autovolgingsinstellingen voor deze camera.",
+ "host": {
+ "label": "ONVIF-host",
+ "description": "Host (en optioneel schema) voor de ONVIF-dienst van deze camera."
+ },
+ "port": {
+ "label": "ONVIF-poort",
+ "description": "Poortnummer voor de ONVIF-dienst."
+ },
+ "user": {
+ "label": "ONVIF-gebruikersnaam",
+ "description": "Gebruikersnaam voor ONVIF-authenticatie; sommige apparaten vereisen de admin-gebruiker voor ONVIF."
+ },
+ "password": {
+ "label": "ONVIF-wachtwoord",
+ "description": "Wachtwoord voor ONVIF-authenticatie."
+ },
+ "tls_insecure": {
+ "label": "TLS-verificatie uitschakelen",
+ "description": "TLS-verificatie overslaan en digest-authenticatie uitschakelen voor ONVIF (onveilig; alleen in veilige netwerken)."
+ },
+ "profile": {
+ "label": "ONVIF-profiel",
+ "description": "Specifiek ONVIF-mediaprofiel voor PTZ-besturing, gekoppeld via token of naam. Indien niet ingesteld, wordt het eerste profiel met geldige PTZ-configuratie automatisch geselecteerd."
+ },
+ "autotracking": {
+ "label": "Automatisch volgen",
+ "description": "Bewegende objecten automatisch volgen en gecentreerd houden in het beeld via PTZ-camerabewegingen.",
+ "enabled": {
+ "label": "Automatisch volgen inschakelen",
+ "description": "Automatisch PTZ-camera volgen van gedetecteerde objecten in- of uitschakelen."
+ },
+ "calibrate_on_startup": {
+ "label": "Kalibreren bij opstarten",
+ "description": "PTZ-motorsnelheden meten bij opstarten voor nauwkeurigere volging. Frigate werkt de configuratie bij met movement_weights na kalibratie."
+ },
+ "zooming": {
+ "label": "Zoommodus",
+ "description": "Zoomgedrag instellen: disabled (alleen pan/tilt), absolute (meest compatibel) of relative (gelijktijdig pan/tilt/zoom)."
+ },
+ "zoom_factor": {
+ "label": "Zoomfactor",
+ "description": "Zoomniveau voor gevolgde objecten instellen. Lagere waarden tonen meer van de scène; hogere waarden zoomen verder in maar kunnen de volging verliezen. Waarden tussen 0,1 en 0,75."
+ },
+ "track": {
+ "label": "Gevolgde objecten",
+ "description": "Lijst van objecttypen die automatisch volgen activeren."
+ },
+ "required_zones": {
+ "label": "Vereiste zones",
+ "description": "Objecten moeten een van deze zones betreden voordat automatisch volgen begint."
+ },
+ "return_preset": {
+ "label": "Terugkeer-voorinstelling",
+ "description": "ONVIF-voorkeuzeinstelling in de camerafirmware om naar terug te keren na het volgen."
+ },
+ "timeout": {
+ "label": "Terugkeertimeout",
+ "description": "Dit aantal seconden wachten na het verliezen van de volging voordat de camera naar de voorkeuze-positie terugkeert."
+ },
+ "movement_weights": {
+ "label": "Bewegingsgewichten",
+ "description": "Kalibratiewaarden automatisch gegenereerd door camerakalbratie. Niet handmatig aanpassen."
+ },
+ "enabled_in_config": {
+ "label": "Originele autovolgstatus",
+ "description": "Intern veld om bij te houden of automatisch volgen was ingeschakeld in de configuratie."
+ }
+ },
+ "ignore_time_mismatch": {
+ "label": "Tijdsverschil negeren",
+ "description": "Tijdsynchronisatieverschillen tussen camera en Frigate-server negeren voor ONVIF-communicatie."
+ }
}
}
diff --git a/web/public/locales/nl/config/global.json b/web/public/locales/nl/config/global.json
index adc9aa42d9..8da53d6c88 100644
--- a/web/public/locales/nl/config/global.json
+++ b/web/public/locales/nl/config/global.json
@@ -1,24 +1,29 @@
{
"audio": {
- "label": "Audiogebeurtenissen",
+ "label": "Audio events",
"enabled": {
- "label": "Geluiddetectie inschakelen"
+ "label": "Geluiddetectie inschakelen",
+ "description": "Audioeventdetectie voor alle camera's in- of uitschakelen; kan per camera worden overschreven."
},
"max_not_heard": {
- "label": "Einde timeout",
- "description": "Hoeveelheid secondes zonder de geconfigureerde audio soort, voordat de geluids gebeurtenis is beindigd."
+ "label": "Einde time-out",
+ "description": "Aantal seconden zonder het geconfigureerde audiotype voordat de audio-event wordt beëindigd."
},
"min_volume": {
- "label": "Minimale volume",
- "description": "Minimale RMS-volumedrempel die nodig is om audiodetectie te starten; Hoe lager de waarde, hoe gevoeliger de detectie (bijvoorbeeld, 200 hoog, 500 gemiddeld, 1000 laag)."
+ "label": "Minimumvolume",
+ "description": "Minimale RMS-volumedrempel die nodig is om audiodetectie te starten; hoe lager de waarde, hoe gevoeliger de detectie (bijvoorbeeld, 200 hoog, 500 gemiddeld, 1000 laag)."
},
"listen": {
"label": "Luistercategorieën",
"description": "Lijst van luistercategorie gebeurtenissen voor detectie (zoals: blaffen, band_alarm, schreeuw, praten, roepen)."
},
"filters": {
- "label": "Geluids filters",
- "description": "Instellingen per audiotype, waaronder betrouwbaarheidsdrempels, ter vermindering van foutieve detecties."
+ "label": "Geluidsfilters",
+ "description": "Instellingen per audiotype, waaronder betrouwbaarheidsdrempels, ter vermindering van foutieve detecties.",
+ "threshold": {
+ "label": "Minimale audiobetrouwbaarheid",
+ "description": "Minimale betrouwbaarheidsdrempel voor de audiogebeurtenis om te worden geteld."
+ }
},
"enabled_in_config": {
"label": "Originele audio-instelling",
@@ -27,44 +32,98 @@
"num_threads": {
"label": "Detectiethreads",
"description": "Aantal threads voor audiodetectieverwerking."
- }
+ },
+ "description": "Instellingen voor audiogebaseerde gebeurtenisdetectie voor alle camera's; kan per camera worden overschreven."
},
"audio_transcription": {
- "label": "Audio‑transcriptie",
+ "label": "Audiotranscriptie",
"description": "Instellingen voor live en spraakgestuurde audiotranscriptie voor gebeurtenissen en live ondertitels.",
"live_enabled": {
"label": "Live transcriptie",
"description": "Live streaming‑transcriptie van audio inschakelen tijdens ontvangst."
+ },
+ "enabled": {
+ "label": "Audiotranscriptie inschakelen",
+ "description": "Automatische audiotranscriptie voor alle camera's in- of uitschakelen; kan per camera worden overschreven."
+ },
+ "language": {
+ "label": "Transcriptietaal",
+ "description": "Taalcode voor transcriptie/vertaling (bijv. 'nl' voor Nederlands). Zie https://whisper-api.com/docs/languages/ voor ondersteunde taalcodes."
+ },
+ "device": {
+ "label": "Transcriptieapparaat",
+ "description": "Apparaat (CPU/GPU) voor het uitvoeren van het transcriptiemodel. Momenteel worden alleen NVIDIA CUDA GPU's ondersteund voor transcriptie."
+ },
+ "model_size": {
+ "label": "Modelgrootte",
+ "description": "Modelgrootte voor offline audiotranscriptie."
}
},
"birdseye": {
- "label": "Overzichtsweergave",
+ "label": "Birdseye-overzicht",
"description": "Instellingen voor de overzichtsweergave die meerdere camerafeeds combineert tot één lay‑out.",
"enabled": {
- "label": "Activeer overzichtsweergave",
+ "label": "Birdseye-overzicht inschakelen",
"description": "De overzichtsweergavefunctie in- of uitschakelen."
},
"mode": {
- "label": "Volgmodus",
+ "label": "Weergavemodus",
"description": "Modus voor het opnemen van camera’s in overzichtsweergave: ‘objecten’, ‘beweging’ of ‘continu’."
},
"order": {
"label": "Positie",
"description": "Numerieke positie die de volgorde van de camera in de overzichtsweergave lay-out bepaalt."
+ },
+ "restream": {
+ "label": "RTSP-herstreaming",
+ "description": "De Birdseye-uitvoer herstreamen als RTSP-feed; hierdoor blijft Birdseye continu actief."
+ },
+ "width": {
+ "label": "Breedte",
+ "description": "Uitvoerbreedte (pixels) van het samengestelde Birdseye-frame."
+ },
+ "height": {
+ "label": "Hoogte",
+ "description": "Uitvoerhoogte (pixels) van het samengestelde Birdseye-frame."
+ },
+ "quality": {
+ "label": "Coderingskwaliteit",
+ "description": "Coderingskwaliteit van de Birdseye MPEG-1-feed (1 = hoogste kwaliteit, 31 = laagste)."
+ },
+ "inactivity_threshold": {
+ "label": "Inactiviteitsdrempel",
+ "description": "Seconden inactiviteit waarna een camera niet meer in Birdseye wordt getoond."
+ },
+ "layout": {
+ "label": "Lay-out",
+ "description": "Lay-outopties voor de Birdseye-samenstelling.",
+ "scaling_factor": {
+ "label": "Schaalfactor",
+ "description": "Schaalfactor voor de lay-outcalculator (bereik 1,0 tot 5,0)."
+ },
+ "max_cameras": {
+ "label": "Maximum camera's",
+ "description": "Maximaal aantal camera's dat tegelijk in Birdseye wordt weergegeven; toont de meest recente camera's."
+ }
+ },
+ "idle_heartbeat_fps": {
+ "label": "Inactief heartbeat-FPS",
+ "description": "Frames per seconde voor het opnieuw verzenden van het laatste Birdseye-frame tijdens inactiviteit; stel 0 in om uit te schakelen."
}
},
"detect": {
- "label": "Detectie object",
+ "label": "Objectdetectie",
"description": "Instellingen voor de detectierol om objecten te detecteren en trackers te starten.",
"enabled": {
- "label": "Detectie aan"
+ "label": "Detectie inschakelen",
+ "description": "Objectdetectie voor alle camera's in- of uitschakelen; kan per camera worden overschreven."
},
"height": {
- "label": "Detectie hoogte",
+ "label": "Detectiehoogte",
"description": "De hoogte in pixels van frames voor de detectiestream. Laat dit veld leeg om de standaardresolutie te gebruiken."
},
"width": {
- "label": "Detectie breedte",
+ "label": "Detectiebreedte",
"description": "De breedte in pixels van frames voor de detectiestream. Laat dit veld leeg om de standaardresolutie te gebruiken."
},
"fps": {
@@ -98,72 +157,1444 @@
"description": "Standaardlimiet voor het aantal frames dat een stilstaand object wordt gevolgd voordat wordt gestopt."
},
"objects": {
- "label": "Object‑maximum aantal frames",
- "description": "Per‑object overschrijden voor het maximum aantal frames voor tracking van stationaire objecten."
+ "label": "Maximaal aantal frames per object",
+ "description": "Maximum aantal frames per object bij het volgen van stilstaande objecten."
}
+ },
+ "classifier": {
+ "label": "Visuele classifier inschakelen",
+ "description": "Gebruik een visuele classifier om echt stilstaande objecten te detecteren, zelfs wanneer detectiekaders licht verschuiven."
}
+ },
+ "annotation_offset": {
+ "label": "Annotatie-offset",
+ "description": "Milliseconden om detectieannotaties te verschuiven voor betere uitlijning van tijdlijn-detectiekaders met opnames; kan positief of negatief zijn."
}
},
"version": {
"description": "Numerieke of string-versie van de actieve configuratie om migraties of formaatwijzigingen te helpen detecteren.",
- "label": "Huidige configuratie versie"
+ "label": "Huidige config-versie"
},
"safe_mode": {
"label": "Veilige modus",
- "description": "Wanneer ingeschakeld, start Frigate in veilige modus met verminderde functionaliteit voor probleemoplossing."
+ "description": "Wanneer ingeschakeld, start Frigate op in veilige modus met beperkte functies voor probleemoplossing."
},
"environment_vars": {
"label": "Omgevingsvariabelen",
- "description": "Sleutel/waarde paren van omgevingsvariabelen voor het Frigate proces in Home Assistant OS. Niet-HAOS gebruikers moeten in plaats hiervan Docker omgevingsvariabelen gebruiken."
+ "description": "Sleutel/waarde-paren van omgevingsvariabelen die ingesteld worden voor het Frigate-proces in Home Assistant OS. Gebruikers zonder HAOS moeten in plaats daarvan de Docker-omgevingsvariabelenconfiguratie gebruiken."
},
"auth": {
"label": "Authenticatie",
"enabled": {
- "label": "Authenticatie aanzetten",
+ "label": "Authenticatie inschakelen",
"description": "Schakel native authenticatie in voor de Frigate UI."
},
"reset_admin_password": {
- "label": "Reset admin wachtwoord",
- "description": "Indien waar, reset het admin gebruiker wachtwoord tijdens opstarten en print het nieuwe wachtwoord in het logboek."
+ "label": "Adminwachtwoord resetten",
+ "description": "Indien waar, reset het wachtwoord van de admingebruiker tijdens opstarten en print het nieuwe wachtwoord in het logboek."
},
- "description": "Authenticatie en sessie-gerelateerde instellingen inclusief cookie en tempo limiet opties.",
+ "description": "Authenticatie- en sessie-instellingen inclusief cookie- en snelheidsbeperkingsopties.",
"cookie_name": {
- "label": "JWT cookie naam",
+ "label": "JWT-cookienaam",
"description": "Naam van de gebruikte cookie om de JWT token voor native authenticatie op te slaan."
},
"cookie_secure": {
- "label": "Veilige cookie instelling",
+ "label": "Secure-cookievlag",
"description": "Stel de veilige instelling in op de auth cookie; moet waar zijn indien TLS in gebruik."
},
"session_length": {
- "label": "Sessie duratie",
- "description": "Sessie duratie in seconden voor JWT-gebaseerde sessies."
+ "label": "Sessieduur",
+ "description": "Sessieduur in seconden voor JWT-gebaseerde sessies."
},
"refresh_time": {
- "label": "Sessie ververs scherm",
- "description": "Als een sessie binnen dit aantal seconden verloopt, ververs het tot volledige duratie."
+ "label": "Sessie-verversperiode",
+ "description": "Als een sessie binnen dit aantal seconden verloopt, wordt de sessie verlengd tot de volledige duur."
},
"failed_login_rate_limit": {
- "label": "Gefaalde log-in pogingen",
- "description": "Tempo-limiet regels voor gefaalde inlogpogingen om brute-force aanvallen te beperken."
+ "label": "Limieten voor mislukte inlogpogingen",
+ "description": "Rate-limitregels voor mislukte inlogpogingen om brute-forceaanvallen te beperken."
},
"trusted_proxies": {
- "label": "Vertrouwde proxies"
+ "label": "Vertrouwde proxies",
+ "description": "Lijst met vertrouwde proxy-IP's die worden gebruikt bij het bepalen van het client-IP voor rate limiting."
+ },
+ "hash_iterations": {
+ "label": "Hash-iteraties",
+ "description": "Aantal PBKDF2-SHA256-iteraties voor het hashen van gebruikerswachtwoorden."
+ },
+ "roles": {
+ "label": "Roltoewijzingen",
+ "description": "Koppel rollen aan cameralijsten. Een lege lijst geeft de rol toegang tot alle camera's."
+ },
+ "admin_first_time_login": {
+ "label": "Eerste keer admin-vlag",
+ "description": "Wanneer ingeschakeld kan de UI een helplink tonen op de inlogpagina om gebruikers te informeren hoe ze kunnen inloggen na een admin-wachtwoordreset. "
}
},
"logger": {
"default": {
"label": "Loggingsniveau",
- "description": "Standaard globale logboek detailniveau (debug, info, waarschuwing, fout)."
+ "description": "Standaard globale logdetailniveau (debug, info, warning, error)."
},
"label": "Logging",
"logs": {
- "label": "Per-proces logboek niveau",
- "description": "Per-component logboekniveau afwijkingen om detailniveau te vergroten of verkleinen per specifieke module."
+ "label": "Logboekniveau per proces",
+ "description": "Logboekniveau-afwijkingen per component om het detailniveau per specifieke module te verhogen of verlagen."
},
- "description": "Beheert het standaard logboek detailniveau en afwijkende instellingen per logboek."
+ "description": "Beheert het standaard logdetailniveau en afwijkende instellingen per logboek."
},
"profiles": {
- "label": "Profielen"
+ "label": "Profielen",
+ "description": "Benoemde profieldefinities met weergavenamen. Cameraprofielen moeten verwijzen naar hier gedefinieerde namen.",
+ "friendly_name": {
+ "label": "Weergavenaam",
+ "description": "Weergavenaam voor dit profiel in de UI."
+ }
+ },
+ "database": {
+ "label": "Database",
+ "description": "Instellingen voor de SQLite-database die Frigate gebruikt om gevolgde objecten en opname-metadata op te slaan.",
+ "path": {
+ "label": "Databasepad",
+ "description": "Bestandssysteempad waar het Frigate SQLite-databasebestand wordt opgeslagen."
+ }
+ },
+ "go2rtc": {
+ "label": "go2rtc",
+ "description": "Instellingen voor de geïntegreerde go2rtc-restreaming-service voor het doorzenden en omzetten van live streams."
+ },
+ "mqtt": {
+ "label": "MQTT",
+ "description": "Instellingen voor het verbinden met en publiceren van telemetrie, snapshots en gebeurtenisdetails naar een MQTT-broker.",
+ "enabled": {
+ "label": "MQTT inschakelen",
+ "description": "MQTT-integratie voor status, gebeurtenissen en snapshots in- of uitschakelen."
+ },
+ "host": {
+ "label": "MQTT-host",
+ "description": "Hostnaam of IP-adres van de MQTT-broker."
+ },
+ "port": {
+ "label": "MQTT-poort",
+ "description": "Poort van de MQTT-broker (gewoonlijk 1883 voor gewoon MQTT)."
+ },
+ "topic_prefix": {
+ "label": "Topic-prefix",
+ "description": "MQTT-topic-prefix voor alle Frigate-topics; moet uniek zijn bij meerdere instanties."
+ },
+ "client_id": {
+ "label": "Client-ID",
+ "description": "Client-ID voor verbinding met de MQTT-broker; moet uniek zijn per instantie."
+ },
+ "stats_interval": {
+ "label": "Statistiekeninterval",
+ "description": "Interval in seconden voor het publiceren van systeem- en camerastatistieken naar MQTT."
+ },
+ "user": {
+ "label": "MQTT-gebruikersnaam",
+ "description": "Optionele MQTT-gebruikersnaam; kan via omgevingsvariabelen of secrets worden opgegeven."
+ },
+ "password": {
+ "label": "MQTT-wachtwoord",
+ "description": "Optioneel MQTT-wachtwoord; kan via omgevingsvariabelen of secrets worden opgegeven."
+ },
+ "tls_ca_certs": {
+ "label": "TLS CA-certificaten",
+ "description": "Pad naar het CA-certificaat voor TLS-verbindingen met de broker (voor zelfondertekende certificaten)."
+ },
+ "tls_client_cert": {
+ "label": "Clientcertificaat",
+ "description": "Pad naar het clientcertificaat voor wederzijdse TLS-authenticatie; stel geen gebruiker/wachtwoord in bij gebruik van clientcertificaten."
+ },
+ "tls_client_key": {
+ "label": "Clientsleutel",
+ "description": "Pad naar de privésleutel van het clientcertificaat."
+ },
+ "tls_insecure": {
+ "label": "Onveilige TLS",
+ "description": "Onveilige TLS-verbindingen toestaan door hostnaamverificatie over te slaan (niet aanbevolen)."
+ },
+ "qos": {
+ "label": "MQTT QoS-niveau",
+ "description": "QoS-niveau voor MQTT-publicaties/abonnementen (0, 1 of 2)."
+ }
+ },
+ "notifications": {
+ "label": "Meldingen",
+ "description": "Instellingen om meldingen voor alle camera's in te schakelen en te beheren; kan per camera worden overschreven.",
+ "enabled": {
+ "label": "Meldingen inschakelen",
+ "description": "Meldingen voor alle camera's in- of uitschakelen; kan per camera worden overschreven."
+ },
+ "email": {
+ "label": "Melding email",
+ "description": "E-mailadres voor pushmeldingen of vereist door bepaalde meldingsproviders."
+ },
+ "cooldown": {
+ "label": "Wachttijd",
+ "description": "Wachttijd (seconden) tussen meldingen om spammen te voorkomen."
+ },
+ "enabled_in_config": {
+ "label": "Originele meldingsstatus",
+ "description": "Geeft aan of meldingen waren ingeschakeld in de originele statische configuratie."
+ }
+ },
+ "networking": {
+ "label": "Netwerken",
+ "description": "Netwerkinstellingen zoals IPv6-ondersteuning voor Frigate-eindpunten.",
+ "ipv6": {
+ "label": "IPv6-configuratie",
+ "description": "IPv6-instellingen voor Frigate-netwerkdiensten.",
+ "enabled": {
+ "label": "IPv6 inschakelen",
+ "description": "IPv6-ondersteuning voor Frigate-diensten (API en UI) inschakelen waar van toepassing."
+ }
+ },
+ "listen": {
+ "label": "Luisterpoortenconfiguratie",
+ "description": "Configuratie van interne en externe luisterpoorten. Dit is voor gevorderde gebruikers. In de meeste gevallen wordt aanbevolen de poortensectie in het Docker Compose-bestand aan te passen.",
+ "internal": {
+ "label": "Interne poort",
+ "description": "Interne luisterpoort voor Frigate (standaard 5000)."
+ },
+ "external": {
+ "label": "Externe poort",
+ "description": "Externe luisterpoort voor Frigate (standaard 8971)."
+ }
+ }
+ },
+ "proxy": {
+ "label": "Proxy",
+ "description": "Instellingen voor het integreren van Frigate achter een reverse proxy die geauthenticeerde gebruikersheaders doorgeeft.",
+ "header_map": {
+ "label": "Headertoewijzing",
+ "description": "Inkomende proxyheaders koppelen aan Frigate gebruikers- en rolvelden voor proxy-authenticatie.",
+ "user": {
+ "label": "Gebruikersheader",
+ "description": "Header met de geauthenticeerde gebruikersnaam van de upstream-proxy."
+ },
+ "role": {
+ "label": "Rolheader",
+ "description": "Header met de rol of groepen van de geauthenticeerde gebruiker van de upstream-proxy."
+ },
+ "role_map": {
+ "label": "Roltoewijzing",
+ "description": "Koppel upstream-groepswaarden aan Frigate-rollen (bijv. admingroepen aan de adminrol)."
+ }
+ },
+ "logout_url": {
+ "label": "Uitlog-URL",
+ "description": "URL waarnaar gebruikers worden doorgestuurd bij uitloggen via de proxy."
+ },
+ "auth_secret": {
+ "label": "Proxygeheim",
+ "description": "Optioneel geheim dat wordt gecontroleerd tegen de X-Proxy-Secret-header om vertrouwde proxies te verifiëren."
+ },
+ "default_role": {
+ "label": "Standaardrol",
+ "description": "Standaardrol toegewezen aan proxy-geauthenticeerde gebruikers wanneer geen roltoewijzing van toepassing is (admin of viewer)."
+ },
+ "separator": {
+ "label": "Scheidingsteken",
+ "description": "Scheidingsteken voor meerdere waarden in proxyheaders."
+ }
+ },
+ "telemetry": {
+ "label": "Telemetrie",
+ "description": "Opties voor systeemtelemetrie en statistieken, inclusief GPU- en netwerkbandbreedtebewaking.",
+ "network_interfaces": {
+ "label": "Netwerkinterfaces",
+ "description": "Lijst met netwerkinterfacenaamprefixen voor bandbreedtestatistieken."
+ },
+ "stats": {
+ "label": "Systeemstatistieken",
+ "description": "Opties voor het in- of uitschakelen van het verzamelen van systeem- en GPU-statistieken.",
+ "amd_gpu_stats": {
+ "label": "AMD GPU-statistieken",
+ "description": "Verzameling van AMD GPU-statistieken inschakelen indien een AMD GPU aanwezig is."
+ },
+ "intel_gpu_stats": {
+ "label": "Intel GPU-statistieken",
+ "description": "Verzameling van Intel GPU-statistieken inschakelen indien een Intel GPU aanwezig is."
+ },
+ "network_bandwidth": {
+ "label": "Netwerkbandbreedte",
+ "description": "Per-proces netwerkbandbreedtebewaking voor camera-ffmpeg-processen en detectoren inschakelen (vereist Linux-capabilities)."
+ },
+ "intel_gpu_device": {
+ "label": "Intel GPU-apparaat",
+ "description": "PCI-busadres of DRM-apparaatpad (bijv. /dev/dri/card1) om Intel GPU-statistieken aan een specifiek apparaat te koppelen bij meerdere GPU's."
+ }
+ },
+ "version_check": {
+ "label": "Versiecontrole",
+ "description": "Een uitgaande controle inschakelen om te detecteren of een nieuwere Frigate-versie beschikbaar is."
+ }
+ },
+ "tls": {
+ "label": "TLS",
+ "description": "TLS-instellingen voor de Frigate-webservice (poort 8971).",
+ "enabled": {
+ "label": "TLS inschakelen",
+ "description": "TLS inschakelen voor de Frigate-webinterface en API op de geconfigureerde TLS-poort."
+ }
+ },
+ "ui": {
+ "label": "UI",
+ "description": "Gebruikersinterfacevoorkeuren zoals tijdzone, tijd/datumopmaak en eenheden.",
+ "timezone": {
+ "label": "Tijdzone",
+ "description": "Optionele tijdzone voor weergave in de UI (standaard browsertijd indien niet ingesteld)."
+ },
+ "time_format": {
+ "label": "Tijdnotatie",
+ "description": "Tijdnotatie voor de UI (browser, 12-uurs of 24-uurs)."
+ },
+ "date_style": {
+ "label": "Datumstijl",
+ "description": "Datumstijl voor de UI (vol, lang, middel, kort)."
+ },
+ "time_style": {
+ "label": "Tijdstijl",
+ "description": "Tijdstijl voor de UI (vol, lang, middel, kort)."
+ },
+ "unit_system": {
+ "label": "Eenhedensysteem",
+ "description": "Eenhedensysteem voor weergave (metrisch of imperiaal) in de UI en MQTT."
+ }
+ },
+ "detectors": {
+ "label": "Detector hardware",
+ "description": "Configuratie voor objectdetectors (CPU, GPU, ONNX-backends) en detector-specifieke modelinstellingen.",
+ "type": {
+ "label": "Type"
+ },
+ "model": {
+ "label": "Detector-specifieke modelconfiguratie",
+ "description": "Detector-specifieke modelconfiguratie-opties (pad, invoergrootte, enz.).",
+ "path": {
+ "label": "Pad naar aangepast objectdetectormodel",
+ "description": "Pad naar een aangepast detectiemodel (of plus:// voor Frigate+-modellen)."
+ },
+ "labelmap_path": {
+ "label": "Labelmap voor aangepaste objectdetector",
+ "description": "Pad naar een labelmap-bestand dat numerieke klassen koppelt aan string-labels voor de detector."
+ },
+ "width": {
+ "label": "Invoerbreedte objectdetectiemodel",
+ "description": "Breedte van de modelinvoertensor in pixels."
+ },
+ "height": {
+ "label": "Invoerhoogte objectdetectiemodel",
+ "description": "Hoogte van de modelinvoertensor in pixels."
+ },
+ "labelmap": {
+ "label": "Labelmap-aanpassing",
+ "description": "Overschrijvingen of herwijzingen om samen te voegen met de standaard labelmap."
+ },
+ "attributes_map": {
+ "label": "Koppeling van objectlabels aan attribuutlabels",
+ "description": "Koppeling tussen objectlabels en attribuutlabels voor metadata (bijv. 'car' -> ['license_plate'])."
+ },
+ "input_tensor": {
+ "label": "Invoertensorvorm van het model",
+ "description": "Tensorformaat dat het model verwacht: 'nhwc' of 'nchw'."
+ },
+ "input_pixel_format": {
+ "label": "Invoerpixelkleurformaat van het model",
+ "description": "Pixelkleurruimte die het model verwacht: 'rgb', 'bgr' of 'yuv'."
+ },
+ "input_dtype": {
+ "label": "Invoergegevenstype van het model",
+ "description": "Gegevenstype van de modelinvoertensor (bijv. 'float32')."
+ },
+ "model_type": {
+ "label": "Modeltype voor objectdetectie",
+ "description": "Modelarchitectuurtype van de detector (ssd, yolox, yolonas) voor optimalisatie door sommige detectors."
+ }
+ },
+ "model_path": {
+ "label": "Detector-specifiek modelpad",
+ "description": "Bestandspad naar het detector-modelbinaire bestand, indien vereist door de gekozen detector."
+ },
+ "axengine": {
+ "label": "AXEngine NPU",
+ "description": "AXERA AX650N/AX8850N NPU-detector die gecompileerde .axmodel-bestanden uitvoert via de AXEngine-runtime."
+ },
+ "cpu": {
+ "label": "CPU",
+ "description": "CPU TFLite-detector die TensorFlow Lite-modellen uitvoert op de host-CPU zonder hardwareversnelling. Niet aanbevolen.",
+ "num_threads": {
+ "label": "Aantal detectiethreads",
+ "description": "Het aantal threads voor CPU-gebaseerde inferentie."
+ }
+ },
+ "deepstack": {
+ "label": "DeepStack",
+ "description": "DeepStack/CodeProject.AI-detector die afbeeldingen naar een externe DeepStack HTTP API stuurt voor inferentie. Niet aanbevolen.",
+ "api_url": {
+ "label": "DeepStack API URL",
+ "description": "De URL van de DeepStack API."
+ },
+ "api_timeout": {
+ "label": "DeepStack API-timeout (in seconden)",
+ "description": "Maximale toegestane tijd voor een DeepStack API-verzoek."
+ },
+ "api_key": {
+ "label": "DeepStack API-sleutel (indien vereist)",
+ "description": "Optionele API-sleutel voor geauthenticeerde DeepStack-diensten."
+ }
+ },
+ "degirum": {
+ "label": "DeGirum",
+ "description": "DeGirum-detector voor het uitvoeren van modellen via DeGirum-cloud of lokale inferentiediensten.",
+ "location": {
+ "label": "Locatie van inferentie-engine",
+ "description": "Locatie van de DeGirum-inferentie-engine (bijv. '@cloud', '127.0.0.1')."
+ },
+ "zoo": {
+ "label": "Model Zoo",
+ "description": "Pad of URL naar de DeGirum model zoo."
+ },
+ "token": {
+ "label": "DeGirum-cloudtoken",
+ "description": "Token voor toegang tot de DeGirum-cloud."
+ }
+ },
+ "edgetpu": {
+ "label": "EdgeTPU",
+ "description": "EdgeTPU-detector die TensorFlow Lite-modellen uitvoert die zijn gecompileerd voor Coral EdgeTPU via de EdgeTPU-delegate.",
+ "device": {
+ "label": "Apparaattype",
+ "description": "Het apparaat voor EdgeTPU-inferentie (bijv. 'usb', 'pci')."
+ }
+ },
+ "hailo8l": {
+ "label": "Hailo-8/Hailo-8L",
+ "description": "Hailo-8/Hailo-8L-detector die HEF-modellen en de HailoRT SDK gebruikt voor inferentie op Hailo-hardware.",
+ "device": {
+ "label": "Apparaattype",
+ "description": "Het apparaat voor Hailo-inferentie (bijv. 'PCIe', 'M.2')."
+ }
+ },
+ "memryx": {
+ "label": "MemryX",
+ "description": "MemryX MX3-detector die gecompileerde DFP-modellen uitvoert op MemryX-accelerators.",
+ "device": {
+ "label": "Apparaatpad",
+ "description": "Het apparaat voor MemryX-inferentie (bijv. 'PCIe')."
+ }
+ },
+ "onnx": {
+ "label": "ONNX",
+ "description": "ONNX-detector voor het uitvoeren van ONNX-modellen; gebruikt beschikbare versnellingsbackends (CUDA/ROCm/OpenVINO) indien beschikbaar.",
+ "device": {
+ "label": "Apparaattype",
+ "description": "Het apparaat voor ONNX-inferentie (bijv. 'AUTO', 'CPU', 'GPU')."
+ }
+ },
+ "openvino": {
+ "label": "OpenVINO",
+ "description": "OpenVINO-detector voor AMD- en Intel-CPU's, Intel GPU's en Intel VPU-hardware.",
+ "device": {
+ "label": "Apparaattype",
+ "description": "Het apparaat voor OpenVINO-inferentie (bijv. 'CPU', 'GPU', 'NPU')."
+ }
+ },
+ "rknn": {
+ "label": "RKNN",
+ "description": "RKNN-detector voor Rockchip NPU's; voert gecompileerde RKNN-modellen uit op Rockchip-hardware.",
+ "num_cores": {
+ "label": "Aantal te gebruiken NPU-kernen.",
+ "description": "Het aantal te gebruiken NPU-kernen (0 voor automatisch)."
+ }
+ },
+ "synaptics": {
+ "label": "Synaptics",
+ "description": "Synaptics NPU-detector voor modellen in .synap-formaat via de Synap SDK op Synaptics-hardware."
+ },
+ "teflon_tfl": {
+ "label": "Teflon",
+ "description": "Teflon delegate-detector voor TFLite via de Mesa Teflon delegate-bibliotheek voor GPU-versnelling."
+ },
+ "tensorrt": {
+ "label": "TensorRT",
+ "description": "TensorRT-detector voor Nvidia Jetson-apparaten via geserialiseerde TensorRT-engines voor versnelde inferentie.",
+ "device": {
+ "label": "GPU-apparaatindex",
+ "description": "De te gebruiken GPU-apparaatindex."
+ }
+ },
+ "zmq": {
+ "label": "ZMQ IPC",
+ "description": "ZMQ IPC-detector die inferentie uitbesteedt aan een extern proces via een ZeroMQ IPC-eindpunt.",
+ "endpoint": {
+ "label": "ZMQ IPC-eindpunt",
+ "description": "Het ZMQ-eindpunt waarmee verbinding wordt gemaakt."
+ },
+ "request_timeout_ms": {
+ "label": "ZMQ-verzoektimeout in milliseconden",
+ "description": "Timeout voor ZMQ-verzoeken in milliseconden."
+ },
+ "linger_ms": {
+ "label": "ZMQ-socket linger in milliseconden",
+ "description": "Socket linger-periode in milliseconden."
+ }
+ }
+ },
+ "model": {
+ "label": "Detectie model",
+ "description": "Instellingen voor het configureren van een aangepast objectdetectiemodel en de invoervorm.",
+ "path": {
+ "label": "Pad naar aangepast objectdetectormodel",
+ "description": "Pad naar een aangepast detectiemodel (of plus:// voor Frigate+-modellen)."
+ },
+ "labelmap_path": {
+ "label": "Labelmap voor aangepaste objectdetector",
+ "description": "Pad naar een labelmap-bestand dat numerieke klassen koppelt aan string-labels voor de detector."
+ },
+ "width": {
+ "label": "Invoerbreedte objectdetectiemodel",
+ "description": "Breedte van de modelinvoertensor in pixels."
+ },
+ "height": {
+ "label": "Invoerhoogte objectdetectiemodel",
+ "description": "Hoogte van de modelinvoertensor in pixels."
+ },
+ "labelmap": {
+ "label": "Labelmap-aanpassing",
+ "description": "Overschrijvingen of herwijzingen om samen te voegen met de standaard labelmap."
+ },
+ "attributes_map": {
+ "label": "Koppeling van objectlabels aan attribuutlabels",
+ "description": "Koppeling tussen objectlabels en attribuutlabels voor metadata (bijv. 'car' -> ['license_plate'])."
+ },
+ "input_tensor": {
+ "label": "Invoertensorvorm van het model",
+ "description": "Tensorformaat dat het model verwacht: 'nhwc' of 'nchw'."
+ },
+ "input_pixel_format": {
+ "label": "Invoerpixelkleurformaat van het model",
+ "description": "Pixelkleurruimte die het model verwacht: 'rgb', 'bgr' of 'yuv'."
+ },
+ "input_dtype": {
+ "label": "Invoergegevenstype van het model",
+ "description": "Gegevenstype van de modelinvoertensor (bijv. 'float32')."
+ },
+ "model_type": {
+ "label": "Modeltype voor objectdetectie",
+ "description": "Modelarchitectuurtype van de detector (ssd, yolox, yolonas) voor optimalisatie door sommige detectors."
+ }
+ },
+ "genai": {
+ "label": "Generatieve AI-configuratie",
+ "description": "Instellingen voor geïntegreerde generatieve AI-providers voor het genereren van objectbeschrijvingen en beoordelingssamenvattingen.",
+ "api_key": {
+ "label": "API-sleutel",
+ "description": "API-sleutel vereist door sommige providers (kan ook via omgevingsvariabelen worden ingesteld)."
+ },
+ "base_url": {
+ "label": "Basis-URL",
+ "description": "Basis-URL voor zelf-gehoste of compatibele providers (bijv. een Ollama-instantie)."
+ },
+ "model": {
+ "label": "Model",
+ "description": "Het model van de provider voor het genereren van beschrijvingen of samenvattingen."
+ },
+ "provider": {
+ "label": "Provider",
+ "description": "De te gebruiken GenAI-provider (bijv. ollama, gemini, openai)."
+ },
+ "roles": {
+ "label": "Rollen",
+ "description": "GenAI-rollen (chat, beschrijvingen, inbeddingen); één provider per rol."
+ },
+ "provider_options": {
+ "label": "Provideropties",
+ "description": "Aanvullende provider-specifieke opties voor de GenAI-client."
+ },
+ "runtime_options": {
+ "label": "Runtime-opties",
+ "description": "Runtime-opties die bij elke inferentieaanroep aan de provider worden meegegeven."
+ }
+ },
+ "ffmpeg": {
+ "label": "FFmpeg",
+ "description": "FFmpeg-instellingen inclusief binaire pad, argumenten, hardwareversnellingsopties en uitvoerargumenten per rol.",
+ "path": {
+ "label": "FFmpeg-pad",
+ "description": "Pad naar het te gebruiken FFmpeg-binaire bestand of een versie-alias (\"5.0\" of \"7.0\")."
+ },
+ "global_args": {
+ "label": "FFmpeg globale argumenten",
+ "description": "Globale argumenten voor FFmpeg-processen."
+ },
+ "hwaccel_args": {
+ "label": "Hardwareversnellingsargumenten",
+ "description": "Hardwareversnellingsargumenten voor FFmpeg. Provider-specifieke presets worden aanbevolen."
+ },
+ "input_args": {
+ "label": "Invoerargumenten",
+ "description": "Invoerargumenten voor FFmpeg-invoerstromen."
+ },
+ "output_args": {
+ "label": "Uitvoerargumenten",
+ "description": "Standaard uitvoerargumenten voor verschillende FFmpeg-rollen zoals detectie en opname.",
+ "detect": {
+ "label": "Uitvoerargumenten voor detectie",
+ "description": "Standaard uitvoerargumenten voor streams met detectierol."
+ },
+ "record": {
+ "label": "Uitvoerargumenten voor opname",
+ "description": "Standaard uitvoerargumenten voor streams met opnamerol."
+ }
+ },
+ "retry_interval": {
+ "label": "FFmpeg-herverbindingstijd",
+ "description": "Seconden wachten voor een herverbindingspoging na een mislukte camerastream. Standaard is 10."
+ },
+ "apple_compatibility": {
+ "label": "Apple-compatibiliteit",
+ "description": "HEVC-tagging inschakelen voor betere Apple-spelercompatibiliteit bij het opnemen van H.265."
+ },
+ "gpu": {
+ "label": "GPU-index",
+ "description": "Standaard GPU-index voor hardwareversnelling indien beschikbaar."
+ },
+ "inputs": {
+ "label": "Camera-invoer",
+ "description": "Lijst van invoerstream-definities (paden en rollen) voor deze camera.",
+ "path": {
+ "label": "Invoerpad",
+ "description": "URL of pad van de camera-invoerstroom."
+ },
+ "roles": {
+ "label": "Invoerrollen",
+ "description": "Rollen voor deze invoerstroom."
+ },
+ "global_args": {
+ "label": "FFmpeg globale argumenten",
+ "description": "FFmpeg globale argumenten voor deze invoerstroom."
+ },
+ "hwaccel_args": {
+ "label": "Hardwareversnellingsargumenten",
+ "description": "Hardwareversnellingsargumenten voor deze invoerstroom."
+ },
+ "input_args": {
+ "label": "Invoerargumenten",
+ "description": "Invoerargumenten specifiek voor deze stream."
+ }
+ }
+ },
+ "live": {
+ "label": "Live weergave",
+ "description": "Instellingen voor de jsmpeg-livestream-resolutie en -kwaliteit. Dit heeft geen invloed op gerestreamde camera's die go2rtc gebruiken voor live weergave.",
+ "streams": {
+ "label": "Live streamnamen",
+ "description": "Koppeling van geconfigureerde streamnamen aan restream/go2rtc-namen voor live weergave."
+ },
+ "height": {
+ "label": "Live hoogte",
+ "description": "Hoogte (pixels) voor weergave van de jsmpeg-livestream in de webinterface; moet ≤ hoogte van de detectiestream zijn."
+ },
+ "quality": {
+ "label": "Live kwaliteit",
+ "description": "Coderingskwaliteit voor de jsmpeg-stream (1 hoogste, 31 laagste)."
+ }
+ },
+ "motion": {
+ "label": "Bewegingsdetectie",
+ "description": "Standaard bewegingsdetectie-instellingen die worden toegepast op camera's tenzij per camera overschreven.",
+ "enabled": {
+ "label": "Bewegingsdetectie inschakelen",
+ "description": "Bewegingsdetectie voor alle camera's in- of uitschakelen; kan per camera worden overschreven."
+ },
+ "threshold": {
+ "label": "Bewegingsdrempel",
+ "description": "Pixelverschildrempel voor de bewegingsdetector; hogere waarden verminderen de gevoeligheid (bereik 1-255)."
+ },
+ "lightning_threshold": {
+ "label": "Bliksemdrempel",
+ "description": "Drempel om korte lichtflitsen te detecteren en te negeren (lager is gevoeliger, waarden tussen 0,3 en 1,0). Dit voorkomt bewegingsdetectie niet volledig; het zorgt er alleen voor dat de detector stopt met het analyseren van extra frames zodra de drempel wordt overschreden. Op beweging gebaseerde opnames worden tijdens deze gebeurtenissen nog steeds aangemaakt."
+ },
+ "skip_motion_threshold": {
+ "label": "Drempel voor overgeslagen beweging",
+ "description": "Als ingesteld op een waarde tussen 0,0 en 1,0, en meer dan dit deel van het beeld verandert in één frame, geeft de detector geen bewegingsvakken terug en kalibreert hij direct opnieuw. Dit bespaart CPU en vermindert vals-positieven bij bliksem, stormen e.d., maar kan echte gebeurtenissen zoals PTZ-tracking missen. De afweging is tussen het weggooien van enkele megabytes opnames versus het bekijken van een paar korte clips. Leeg laten (None) om deze functie uit te schakelen."
+ },
+ "improve_contrast": {
+ "label": "Contrast verbeteren",
+ "description": "Contrastverbetering op frames toepassen vóór bewegingsanalyse om detectie te verbeteren."
+ },
+ "contour_area": {
+ "label": "Contouroppervlakte",
+ "description": "Minimale contouroppervlakte in pixels voor een bewegingscontour om te worden geteld."
+ },
+ "delta_alpha": {
+ "label": "Delta-alfa",
+ "description": "Alpha-mengfactor voor frameverschil bij bewegingsberekening."
+ },
+ "frame_alpha": {
+ "label": "Frame-alfa",
+ "description": "Alpha-waarde voor het mengen van frames bij bewegingsvoorverwerking."
+ },
+ "frame_height": {
+ "label": "Framehoogte",
+ "description": "Hoogte in pixels waarnaar frames worden geschaald bij het berekenen van beweging."
+ },
+ "mask": {
+ "label": "Maskercoördinaten",
+ "description": "Geordende x,y-coördinaten die het bewegingsmaskeerpolygoon definiëren voor het in- of uitsluiten van gebieden."
+ },
+ "mqtt_off_delay": {
+ "label": "MQTT uit-vertraging",
+ "description": "Seconden wachten na de laatste beweging vóór publicatie van een MQTT 'off'-status."
+ },
+ "enabled_in_config": {
+ "label": "Originele bewegingsstatus",
+ "description": "Geeft aan of bewegingsdetectie was ingeschakeld in de originele statische configuratie."
+ },
+ "raw_mask": {
+ "label": "Onbewerkt masker"
+ }
+ },
+ "objects": {
+ "label": "Objecten",
+ "description": "Standaardinstellingen voor objectvolging, inclusief te volgen labels en per-object filters.",
+ "track": {
+ "label": "Te volgen objecten",
+ "description": "Lijst met objectlabels om te volgen voor alle camera's; kan per camera worden overschreven."
+ },
+ "filters": {
+ "label": "Objectfilters",
+ "description": "Filters op gedetecteerde objecten om vals-positieven te verminderen (oppervlakte, verhouding, betrouwbaarheid).",
+ "min_area": {
+ "label": "Minimale objectoppervlakte",
+ "description": "Minimale detectiekaderoppervlakte (pixels of percentage) voor dit objecttype. Kan pixels (int) of percentage (float tussen 0,000001 en 0,99) zijn."
+ },
+ "max_area": {
+ "label": "Maximale objectoppervlakte",
+ "description": "Maximale detectiekaderoppervlakte (pixels of percentage) voor dit objecttype. Kan pixels (int) of percentage (float tussen 0,000001 en 0,99) zijn."
+ },
+ "min_ratio": {
+ "label": "Minimale beeldverhouding",
+ "description": "Minimale breedte/hoogte-verhouding voor het detectiekader om te kwalificeren."
+ },
+ "max_ratio": {
+ "label": "Maximale beeldverhouding",
+ "description": "Maximale breedte/hoogte-verhouding voor het detectiekader om te kwalificeren."
+ },
+ "threshold": {
+ "label": "Betrouwbaarheidsdrempel",
+ "description": "Gemiddelde detectiebetrouwbaarheidsdrempel om een object als terecht positief te beschouwen."
+ },
+ "min_score": {
+ "label": "Minimale betrouwbaarheid",
+ "description": "Minimale detectiebetrouwbaarheid in één frame om het object te tellen."
+ },
+ "mask": {
+ "label": "Filtermasker",
+ "description": "Polygooncoördinaten die aangeven waar dit filter van toepassing is in het frame."
+ },
+ "raw_mask": {
+ "label": "Onbewerkt masker"
+ }
+ },
+ "mask": {
+ "label": "Objectmasker",
+ "description": "Maskeerpolygoon om objectdetectie in bepaalde gebieden te voorkomen."
+ },
+ "raw_mask": {
+ "label": "Onbewerkt masker"
+ },
+ "genai": {
+ "label": "GenAI-objectconfiguratie",
+ "description": "GenAI-opties voor het beschrijven van gevolgde objecten en het versturen van frames voor generatie.",
+ "enabled": {
+ "label": "GenAI inschakelen",
+ "description": "GenAI-beschrijvingen voor gevolgde objecten standaard inschakelen."
+ },
+ "use_snapshot": {
+ "label": "Snapshots gebruiken",
+ "description": "Objectsnapshots gebruiken in plaats van miniaturen voor GenAI-beschrijving."
+ },
+ "prompt": {
+ "label": "Bijschriftprompt",
+ "description": "Standaard promptsjabloon voor het genereren van beschrijvingen met GenAI."
+ },
+ "object_prompts": {
+ "label": "Objectprompts",
+ "description": "Prompts per object voor het aanpassen van GenAI-uitvoer voor specifieke labels."
+ },
+ "objects": {
+ "label": "GenAI-objecten",
+ "description": "Lijst van objectlabels die standaard naar GenAI worden gestuurd."
+ },
+ "required_zones": {
+ "label": "Vereiste zones",
+ "description": "Zones die objecten moeten betreden om in aanmerking te komen voor GenAI-beschrijving."
+ },
+ "debug_save_thumbnails": {
+ "label": "Snapshots opslaan",
+ "description": "Snapshots die naar GenAI worden gestuurd opslaan voor foutopsporing."
+ },
+ "send_triggers": {
+ "label": "GenAI-triggers",
+ "description": "Bepaalt wanneer frames naar GenAI worden gestuurd (bij einde, na updates, enz.).",
+ "tracked_object_end": {
+ "label": "Sturen bij beëindiging",
+ "description": "Een verzoek naar GenAI sturen wanneer het gevolgde object eindigt."
+ },
+ "after_significant_updates": {
+ "label": "Vroege GenAI-trigger",
+ "description": "Een verzoek naar GenAI sturen na een bepaald aantal significante updates voor het gevolgde object."
+ }
+ },
+ "enabled_in_config": {
+ "label": "Originele GenAI-status",
+ "description": "Geeft aan of GenAI was ingeschakeld in de originele statische configuratie."
+ }
+ }
+ },
+ "record": {
+ "label": "Opname",
+ "description": "Opname- en bewaarinstellingen die worden toegepast op camera's tenzij per camera overschreven.",
+ "enabled": {
+ "label": "Opname inschakelen",
+ "description": "Opname voor alle camera's in- of uitschakelen; kan per camera worden overschreven."
+ },
+ "expire_interval": {
+ "label": "Opruiminterval opnames",
+ "description": "Minuten tussen opruimrondes die verlopen opnamesegmenten verwijderen."
+ },
+ "continuous": {
+ "label": "Continue bewaring",
+ "description": "Aantal dagen om opnames te bewaren ongeacht gevolgde objecten of beweging. Stel 0 in om alleen opnames van meldingen en detecties te bewaren.",
+ "days": {
+ "label": "Bewaardagen",
+ "description": "Dagen om opnames te bewaren."
+ }
+ },
+ "motion": {
+ "label": "Bewegingsretentie",
+ "description": "Aantal dagen om opnames veroorzaakt door beweging te bewaren, ongeacht gevolgde objecten. Stel 0 in om alleen opnames van meldingen en detecties te bewaren.",
+ "days": {
+ "label": "Bewaardagen",
+ "description": "Dagen om opnames te bewaren."
+ }
+ },
+ "detections": {
+ "label": "Detectieretentie",
+ "description": "Opname-retentie-instellingen voor detectiegebeurtenissen inclusief pre/post-captureduur.",
+ "pre_capture": {
+ "label": "Seconden vóór opname",
+ "description": "Aantal seconden vóór de detectiegebeurtenis om op te nemen in de opname."
+ },
+ "post_capture": {
+ "label": "Seconden na opname",
+ "description": "Aantal seconden na de detectiegebeurtenis om op te nemen in de opname."
+ },
+ "retain": {
+ "label": "Gebeurtenisbewaring",
+ "description": "Bewaarinstellingen voor opnames van detectiegebeurtenissen.",
+ "days": {
+ "label": "Bewaardagen",
+ "description": "Aantal dagen om opnames van detectiegebeurtenissen te bewaren."
+ },
+ "mode": {
+ "label": "Bewaarmodus",
+ "description": "Bewaarmodus: all (alle segmenten), motion (segmenten met beweging) of active_objects (segmenten met actieve objecten)."
+ }
+ }
+ },
+ "alerts": {
+ "label": "Meldingsbewaring",
+ "description": "Opname-retentie-instellingen voor alertgebeurtenissen inclusief pre/post-captureduur.",
+ "pre_capture": {
+ "label": "Seconden vóór opname",
+ "description": "Aantal seconden vóór de detectiegebeurtenis om op te nemen in de opname."
+ },
+ "post_capture": {
+ "label": "Seconden na opname",
+ "description": "Aantal seconden na de detectiegebeurtenis om op te nemen in de opname."
+ },
+ "retain": {
+ "label": "Gebeurtenisbewaring",
+ "description": "Bewaarinstellingen voor opnames van detectiegebeurtenissen.",
+ "days": {
+ "label": "Bewaardagen",
+ "description": "Aantal dagen om opnames van detectiegebeurtenissen te bewaren."
+ },
+ "mode": {
+ "label": "Bewaarmodus",
+ "description": "Bewaarmodus: all (alle segmenten), motion (segmenten met beweging) of active_objects (segmenten met actieve objecten)."
+ }
+ }
+ },
+ "export": {
+ "label": "Exportconfiguratie",
+ "description": "Instellingen voor het exporteren van opnames, zoals timelapse en hardwareversnelling.",
+ "hwaccel_args": {
+ "label": "Hardwareversnellingsargumenten voor export",
+ "description": "Hardwareversnellingsargumenten voor export/transcodering."
+ },
+ "max_concurrent": {
+ "label": "Maximaal aantal gelijktijdige exports",
+ "description": "Maximum aantal exporttaken dat tegelijk wordt verwerkt."
+ }
+ },
+ "preview": {
+ "label": "Voorbeeldconfiguratie",
+ "description": "Instellingen voor de kwaliteit van opnamevoorbeelden in de UI.",
+ "quality": {
+ "label": "Voorbeeldkwaliteit",
+ "description": "Kwaliteitsniveau voor voorbeelden (very_low, low, medium, high, very_high)."
+ }
+ },
+ "enabled_in_config": {
+ "label": "Originele opnamestatus",
+ "description": "Geeft aan of opname was ingeschakeld in de originele statische configuratie."
+ }
+ },
+ "review": {
+ "label": "Beoordeling",
+ "description": "Instellingen voor meldingen, detecties en GenAI-beoordelingssamenvattingen in de UI en opslag.",
+ "alerts": {
+ "label": "Meldingsconfiguratie",
+ "description": "Instellingen voor welke gevolgde objecten alerts genereren en hoe alerts worden bewaard.",
+ "enabled": {
+ "label": "Alerts inschakelen",
+ "description": "Genereren van meldingen voor alle camera's in- of uitschakelen; kan per camera worden overschreven."
+ },
+ "labels": {
+ "label": "Meldingslabels",
+ "description": "Lijst met objectlabels die kwalificeren als meldingen (bijv. auto, persoon)."
+ },
+ "required_zones": {
+ "label": "Vereiste zones",
+ "description": "Zones die een object moet betreden om als melding te worden beschouwd; leeg laten voor elke zone."
+ },
+ "enabled_in_config": {
+ "label": "Originele meldingsstatus",
+ "description": "Geeft aan of meldingen oorspronkelijk waren ingeschakeld in de statische configuratie."
+ },
+ "cutoff_time": {
+ "label": "Afsluitingstijd meldingen",
+ "description": "Seconden wachten na het uitblijven van melding veroorzakende activiteit voordat een melding wordt afgesloten."
+ }
+ },
+ "detections": {
+ "label": "Detectieconfiguratie",
+ "description": "Instellingen voor welke gevolgde objecten detecties genereren en hoe detecties worden bewaard.",
+ "enabled": {
+ "label": "Detecties inschakelen",
+ "description": "Detecties voor alle camera's in- of uitschakelen; kan per camera worden overschreven."
+ },
+ "labels": {
+ "label": "Detectielabels",
+ "description": "Lijst met objectlabels die kwalificeren als detectiegebeurtenissen."
+ },
+ "required_zones": {
+ "label": "Vereiste zones",
+ "description": "Zones die een object moet betreden om als detectie te worden beschouwd; leeg laten voor elke zone."
+ },
+ "cutoff_time": {
+ "label": "Afsluitingstijd detecties",
+ "description": "Seconden wachten na het uitblijven van detectie veroorzakende activiteit voordat een detectie wordt afgesloten."
+ },
+ "enabled_in_config": {
+ "label": "Originele detectiestatus",
+ "description": "Geeft aan of detecties oorspronkelijk waren ingeschakeld in de statische configuratie."
+ }
+ },
+ "genai": {
+ "label": "GenAI-configuratie",
+ "description": "Beheert het gebruik van generatieve AI voor het produceren van beschrijvingen en samenvattingen van beoordelingsitems.",
+ "enabled": {
+ "label": "GenAI-beschrijvingen inschakelen",
+ "description": "Door GenAI gegenereerde beschrijvingen en samenvattingen voor beoordelingsitems in- of uitschakelen."
+ },
+ "alerts": {
+ "label": "GenAI inschakelen voor meldingen",
+ "description": "GenAI gebruiken voor het genereren van beschrijvingen bij meldingsitems."
+ },
+ "detections": {
+ "label": "GenAI inschakelen voor detecties",
+ "description": "GenAI gebruiken voor het genereren van beschrijvingen bij detectiebeoordelingen."
+ },
+ "image_source": {
+ "label": "Afbeeldingsbron voor beoordeling",
+ "description": "Bron van afbeeldingen naar GenAI ('preview' of 'recordings'); 'recordings' gebruikt hogere kwaliteit maar meer tokens."
+ },
+ "additional_concerns": {
+ "label": "Aanvullende aandachtspunten",
+ "description": "Een lijst met aanvullende aandachtspunten die GenAI moet meenemen bij het beoordelen van activiteit op deze camera."
+ },
+ "debug_save_thumbnails": {
+ "label": "Snapshots opslaan",
+ "description": "Snapshots die naar de GenAI-provider worden gestuurd opslaan voor foutopsporing."
+ },
+ "enabled_in_config": {
+ "label": "Originele GenAI-status",
+ "description": "Geeft aan of GenAI-beoordeling oorspronkelijk was ingeschakeld in de statische configuratie."
+ },
+ "preferred_language": {
+ "label": "Voorkeurstaal",
+ "description": "Voorkeurstaal voor gegenereerde antwoorden van de GenAI-provider."
+ },
+ "activity_context_prompt": {
+ "label": "Activiteitscontextprompt",
+ "description": "Aangepaste prompt die beschrijft wat wel en niet verdachte activiteit is, als context voor GenAI-samenvattingen."
+ }
+ }
+ },
+ "snapshots": {
+ "label": "Snapshots",
+ "description": "Instellingen voor API-gegenereerde snapshots van gevolgde objecten voor alle camera's; kan per camera worden overschreven.",
+ "enabled": {
+ "label": "Snapshots inschakelen",
+ "description": "Het opslaan van snapshots voor alle camera's in- of uitschakelen; kan per camera worden overschreven."
+ },
+ "timestamp": {
+ "label": "Tijdstempel-overlay",
+ "description": "Een tijdstempel op API-snapshots weergeven."
+ },
+ "bounding_box": {
+ "label": "Detectiekader-overlay",
+ "description": "Detectiekaders voor gevolgde objecten tekenen op API-snapshots."
+ },
+ "crop": {
+ "label": "Snapshot bijsnijden",
+ "description": "API-snapshots bijsnijden tot het detectiekader van het gedetecteerde object."
+ },
+ "required_zones": {
+ "label": "Vereiste zones",
+ "description": "Zones die een object moet betreden voordat een snapshot wordt opgeslagen."
+ },
+ "height": {
+ "label": "Snapshothoogte",
+ "description": "Hoogte (pixels) om API-snapshots naar te schalen; leeg laten om de originele grootte te behouden."
+ },
+ "retain": {
+ "label": "Snapshot-bewaring",
+ "description": "Bewaarinstellingen voor snapshots inclusief standaarddagen en per-object overschrijvingen.",
+ "default": {
+ "label": "Standaard retentie",
+ "description": "Standaard aantal dagen om snapshots te bewaren."
+ },
+ "mode": {
+ "label": "Bewaarmodus",
+ "description": "Bewaarmodus: all (alle segmenten), motion (segmenten met beweging) of active_objects (segmenten met actieve objecten)."
+ },
+ "objects": {
+ "label": "Objectbewaring",
+ "description": "Objectspecifieke overschrijvingen voor het aantal bewaardagen van snapshots."
+ }
+ },
+ "quality": {
+ "label": "Snapshotkwaliteit",
+ "description": "Coderingskwaliteit voor opgeslagen snapshots (0-100)."
+ }
+ },
+ "timestamp_style": {
+ "label": "Tijdstempelstijl",
+ "description": "Stijlopties voor tijdstempels in de feed, toegepast op de debugweergave en snapshots.",
+ "position": {
+ "label": "Tijdstempelpositie",
+ "description": "Positie van de tijdstempel op de afbeelding (tl/tr/bl/br)."
+ },
+ "format": {
+ "label": "Tijdstempelformaat",
+ "description": "Datumtijdformaatstring voor tijdstempels (Python datetime-formaatcodes)."
+ },
+ "color": {
+ "label": "Tijdstempelkleur",
+ "description": "RGB-kleurwaarden voor de tijdstempeltekst (alle waarden 0-255).",
+ "red": {
+ "label": "Rood",
+ "description": "Roodcomponent (0-255) voor de tijdstempelkleur."
+ },
+ "green": {
+ "label": "Groen",
+ "description": "Groencomponent (0-255) voor de tijdstempelkleur."
+ },
+ "blue": {
+ "label": "Blauw",
+ "description": "Blauwcomponent (0-255) voor de tijdstempelkleur."
+ }
+ },
+ "thickness": {
+ "label": "Tijdstempeldikte",
+ "description": "Lijndikte van de tijdstempeltekst."
+ },
+ "effect": {
+ "label": "Tijdstempeleffect",
+ "description": "Visueel effect voor de tijdstempeltekst (geen, effen, schaduw)."
+ }
+ },
+ "classification": {
+ "label": "Objectclassificatie",
+ "description": "Instellingen voor classificatiemodellen die worden gebruikt om objectlabels of statusclassificatie te verfijnen.",
+ "bird": {
+ "label": "Vogelclassificatieconfiguratie",
+ "description": "Instellingen specifiek voor vogelclassificatiemodellen.",
+ "enabled": {
+ "label": "Vogelclassificatie",
+ "description": "Vogelclassificatie in- of uitschakelen."
+ },
+ "threshold": {
+ "label": "Minimale score",
+ "description": "Minimale classificatiescore om een vogelclassificatie te accepteren."
+ }
+ },
+ "custom": {
+ "label": "Aangepaste classificatiemodellen",
+ "description": "Configuratie voor aangepaste classificatiemodellen voor object- of statusdetectie.",
+ "enabled": {
+ "label": "Model inschakelen",
+ "description": "Het aangepaste classificatiemodel in- of uitschakelen."
+ },
+ "name": {
+ "label": "Modelnaam",
+ "description": "Identifier van het te gebruiken aangepaste classificatiemodel."
+ },
+ "threshold": {
+ "label": "Scoredrempel",
+ "description": "Scoredrempel voor het wijzigen van de classificatiestatus."
+ },
+ "save_attempts": {
+ "label": "Opgeslagen pogingen",
+ "description": "Aantal classificatiepogingen dat wordt bijgehouden voor de recente classificaties in de UI."
+ },
+ "object_config": {
+ "objects": {
+ "label": "Objecten classificeren",
+ "description": "Lijst van objecttypen waarop objectclassificatie wordt uitgevoerd."
+ },
+ "classification_type": {
+ "label": "Classificatietype",
+ "description": "Toegepast classificatietype: 'sub_label' (voegt sub_label toe) of andere ondersteunde typen."
+ }
+ },
+ "state_config": {
+ "cameras": {
+ "label": "Classificatiecamera's",
+ "description": "Per-camera bijsnijdinstellingen voor statusclassificatie.",
+ "crop": {
+ "label": "Classificatie-uitsnede",
+ "description": "Bijsnijdcoördinaten voor classificatie op deze camera."
+ }
+ },
+ "motion": {
+ "label": "Uitvoeren bij beweging",
+ "description": "Indien ingeschakeld, classificatie uitvoeren wanneer beweging wordt gedetecteerd in het opgegeven bijsnijdgebied."
+ },
+ "interval": {
+ "label": "Classificatie-interval",
+ "description": "Interval (seconden) tussen periodieke classificatierondes voor statusclassificatie."
+ }
+ }
+ }
+ },
+ "semantic_search": {
+ "label": "Semantisch zoeken",
+ "description": "Instellingen voor semantisch zoeken, dat objectinbeddingen opbouwt en bevraagt om vergelijkbare items te vinden.",
+ "enabled": {
+ "label": "Semantisch zoeken inschakelen",
+ "description": "De semantisch zoeken-functie in- of uitschakelen."
+ },
+ "reindex": {
+ "label": "Herindexeren bij opstarten",
+ "description": "Een volledige herindexering van historische gevolgde objecten in de inbeddingsdatabase starten."
+ },
+ "model": {
+ "label": "Semantisch zoekmodel of GenAI-providernaam",
+ "description": "Het inbeddingsmodel voor semantisch zoeken (bijv. 'jinav1'), of de naam van een GenAI-provider met de inbeddingsrol."
+ },
+ "model_size": {
+ "label": "Modelgrootte",
+ "description": "Selecteer modelgrootte; 'small' draait op CPU en 'large' vereist doorgaans een GPU."
+ },
+ "device": {
+ "label": "Apparaat",
+ "description": "Dit is een overschrijving om een specifiek apparaat te targeten. Zie https://onnxruntime.ai/docs/execution-providers/ voor meer informatie"
+ },
+ "triggers": {
+ "label": "Triggers",
+ "description": "Acties en matchcriteria voor cameraspecifieke semantisch-zoeken-triggers.",
+ "friendly_name": {
+ "label": "Weergavenaam",
+ "description": "Optionele weergavenaam voor deze trigger in de UI."
+ },
+ "enabled": {
+ "label": "Trigger inschakelen",
+ "description": "Deze semantisch-zoeken-trigger in- of uitschakelen."
+ },
+ "type": {
+ "label": "Triggertype",
+ "description": "Type trigger: 'thumbnail' (vergelijk met afbeelding) of 'description' (vergelijk met tekst)."
+ },
+ "data": {
+ "label": "Triggerinhoud",
+ "description": "Tekstzin of miniatuur-ID om te vergelijken met gevolgde objecten."
+ },
+ "threshold": {
+ "label": "Triggerdrempel",
+ "description": "Minimale gelijkenisscore (0-1) om deze trigger te activeren."
+ },
+ "actions": {
+ "label": "Triggeracties",
+ "description": "Lijst van uit te voeren acties bij triggermatch (melding, sub_label, attribuut)."
+ }
+ }
+ },
+ "face_recognition": {
+ "label": "Gezichtsherkenning",
+ "description": "Instellingen voor gezichtsdetectie en -herkenning voor alle camera's; kan per camera worden overschreven.",
+ "enabled": {
+ "label": "Gezichtsherkenning inschakelen",
+ "description": "Gezichtsherkenning voor alle camera's in- of uitschakelen; kan per camera worden overschreven."
+ },
+ "model_size": {
+ "label": "Modelgrootte",
+ "description": "Modelgrootte voor gezichtsinbeddingen (small/large); groter vereist mogelijk een GPU."
+ },
+ "unknown_score": {
+ "label": "Drempel voor onbekende score",
+ "description": "Afstandsdrempel waaronder een gezicht als mogelijke match wordt beschouwd (hoger = strikter)."
+ },
+ "detection_threshold": {
+ "label": "Detectiedrempel",
+ "description": "Minimale detectiebetrouwbaarheid om een gezichtsdetectie als geldig te beschouwen."
+ },
+ "recognition_threshold": {
+ "label": "Herkenningsdrempel",
+ "description": "Gezichtsinbeddingsafstandsdrempel om twee gezichten als match te beschouwen."
+ },
+ "min_area": {
+ "label": "Minimale gezichtsoppervlakte",
+ "description": "Minimale oppervlakte (pixels) van een gedetecteerd gezichtskader om herkenning te proberen."
+ },
+ "min_faces": {
+ "label": "Minimum gezichten",
+ "description": "Minimum aantal gezichtsherkeningen vereist voordat een herkend sub-label aan een persoon wordt toegekend."
+ },
+ "save_attempts": {
+ "label": "Opgeslagen pogingen",
+ "description": "Aantal gezichtsherkenningspogingen dat wordt bijgehouden voor de recente herkenningen in de UI."
+ },
+ "blur_confidence_filter": {
+ "label": "Vaagheidsbetrouwbaarheidsfilter",
+ "description": "Betrouwbaarheidsscores aanpassen op basis van beeldvaagheid om vals-positieven bij slechte gezichtskwaliteit te verminderen."
+ },
+ "device": {
+ "label": "Apparaat",
+ "description": "Dit is een overschrijving om een specifiek apparaat te targeten. Zie https://onnxruntime.ai/docs/execution-providers/ voor meer informatie"
+ }
+ },
+ "lpr": {
+ "label": "Kentekenherkenning",
+ "description": "Instellingen voor kentekenherkenning inclusief detectiedrempels, opmaak en bekende kentekens.",
+ "enabled": {
+ "label": "LPR inschakelen",
+ "description": "Kentekenherkenning voor alle camera's in- of uitschakelen; kan per camera worden overschreven."
+ },
+ "model_size": {
+ "label": "Modelgrootte",
+ "description": "Modelgrootte voor tekstdetectie/-herkenning. De meeste gebruikers moeten 'small' gebruiken."
+ },
+ "detection_threshold": {
+ "label": "Detectiedrempel",
+ "description": "Detectiebetrouwbaarheidsdrempel om OCR te starten op een vermoedelijk kenteken."
+ },
+ "min_area": {
+ "label": "Minimale kentekenoppervlakte",
+ "description": "Minimale kentekenoppervlakte (pixels) om herkenning te proberen."
+ },
+ "recognition_threshold": {
+ "label": "Herkenningsdrempel",
+ "description": "Betrouwbaarheidsdrempel voor herkende kentekentekst om als sub-label toe te voegen."
+ },
+ "min_plate_length": {
+ "label": "Minimale kentekenlengte",
+ "description": "Minimum aantal tekens dat een herkend kenteken moet bevatten om geldig te zijn."
+ },
+ "format": {
+ "label": "Regex voor kentekenformaat",
+ "description": "Optionele regex om herkende kentekens te valideren tegen een verwacht formaat."
+ },
+ "match_distance": {
+ "label": "Overeenkomstafstand",
+ "description": "Aantal toegestane tekenfouten bij vergelijking van gedetecteerde kentekens met bekende kentekens."
+ },
+ "known_plates": {
+ "label": "Bekende kentekens",
+ "description": "Lijst met kentekens of regex-patronen om specifiek te volgen of meldingen voor te genereren."
+ },
+ "enhancement": {
+ "label": "Verbeteringsniveau",
+ "description": "Verbeteringsniveau (0-10) voor kentekenuitsneden vóór OCR; hogere waarden verbeteren niet altijd het resultaat; niveaus boven 5 werken mogelijk alleen voor nachtelijke kentekens en moeten voorzichtig worden gebruikt."
+ },
+ "debug_save_plates": {
+ "label": "Kentekenplaten opslaan voor foutopsporing",
+ "description": "Kentekenuitsneden opslaan voor foutopsporing van LPR-prestaties."
+ },
+ "device": {
+ "label": "Apparaat",
+ "description": "Dit is een overschrijving om een specifiek apparaat te targeten. Zie https://onnxruntime.ai/docs/execution-providers/ voor meer informatie"
+ },
+ "replace_rules": {
+ "label": "Vervangingsregels",
+ "description": "Regex-vervangingsregels voor het normaliseren van gedetecteerde kentekenstrings vóór vergelijking.",
+ "pattern": {
+ "label": "Regex-patroon"
+ },
+ "replacement": {
+ "label": "Vervangende tekst"
+ }
+ },
+ "expire_time": {
+ "label": "Vervaltijd in seconden",
+ "description": "Tijd in seconden waarna een niet-gezien kenteken vervalt uit de tracker (alleen voor dedicated LPR-camera's)."
+ }
+ },
+ "camera_groups": {
+ "label": "Cameragroepen",
+ "description": "Configuratie voor benoemde cameragroepen voor het organiseren van camera's in de UI.",
+ "cameras": {
+ "label": "Cameralijst",
+ "description": "Lijst met cameranamen in deze groep."
+ },
+ "icon": {
+ "label": "Groepspictogram",
+ "description": "Pictogram voor de cameragroep in de UI."
+ },
+ "order": {
+ "label": "Sorteervolgorde",
+ "description": "Numerieke volgorde voor het sorteren van cameragroepen in de UI; grotere nummers verschijnen later."
+ }
+ },
+ "active_profile": {
+ "label": "Actief profiel",
+ "description": "Naam van het momenteel actieve profiel. Alleen runtime, wordt niet opgeslagen in YAML."
+ },
+ "camera_mqtt": {
+ "label": "MQTT",
+ "description": "Instellingen voor het publiceren van MQTT-afbeeldingen.",
+ "enabled": {
+ "label": "Afbeelding versturen",
+ "description": "Het publiceren van afbeeldingssnapshots van objecten naar MQTT-topics voor deze camera inschakelen."
+ },
+ "timestamp": {
+ "label": "Tijdstempel toevoegen",
+ "description": "Een tijdstempel op naar MQTT gepubliceerde afbeeldingen weergeven."
+ },
+ "bounding_box": {
+ "label": "Detectiekader toevoegen",
+ "description": "Detectiekaders tekenen op via MQTT gepubliceerde afbeeldingen."
+ },
+ "crop": {
+ "label": "Afbeelding bijsnijden",
+ "description": "Naar MQTT gepubliceerde afbeeldingen bijsnijden tot het detectiekader van het gedetecteerde object."
+ },
+ "height": {
+ "label": "Afbeeldingshoogte",
+ "description": "Hoogte (pixels) voor het schalen van via MQTT gepubliceerde afbeeldingen."
+ },
+ "required_zones": {
+ "label": "Vereiste zones",
+ "description": "Zones die een object moet betreden voordat een MQTT-afbeelding wordt gepubliceerd."
+ },
+ "quality": {
+ "label": "JPEG-kwaliteit",
+ "description": "JPEG-kwaliteit voor naar MQTT gepubliceerde afbeeldingen (0-100)."
+ }
+ },
+ "camera_ui": {
+ "label": "Camera-UI",
+ "description": "Weergavevolgorde en zichtbaarheid van deze camera in de UI. De volgorde heeft invloed op het standaarddashboard. Gebruik cameragroepen voor fijnere controle.",
+ "order": {
+ "label": "UI-volgorde",
+ "description": "Numerieke volgorde voor het sorteren van de camera in de UI (standaarddashboard en lijsten); grotere nummers verschijnen later."
+ },
+ "dashboard": {
+ "label": "Tonen in UI",
+ "description": "Schakel de zichtbaarheid van deze camera overal in de Frigate-UI in of uit. Uitschakelen vereist handmatige aanpassing van de configuratie om de camera opnieuw te bekijken."
+ }
+ },
+ "onvif": {
+ "label": "ONVIF",
+ "description": "ONVIF-verbindings- en PTZ-autovolgingsinstellingen voor deze camera.",
+ "host": {
+ "label": "ONVIF-host",
+ "description": "Host (en optioneel schema) voor de ONVIF-dienst van deze camera."
+ },
+ "port": {
+ "label": "ONVIF-poort",
+ "description": "Poortnummer voor de ONVIF-dienst."
+ },
+ "user": {
+ "label": "ONVIF-gebruikersnaam",
+ "description": "Gebruikersnaam voor ONVIF-authenticatie; sommige apparaten vereisen de admin-gebruiker voor ONVIF."
+ },
+ "password": {
+ "label": "ONVIF-wachtwoord",
+ "description": "Wachtwoord voor ONVIF-authenticatie."
+ },
+ "tls_insecure": {
+ "label": "TLS-verificatie uitschakelen",
+ "description": "TLS-verificatie overslaan en digest-authenticatie uitschakelen voor ONVIF (onveilig; alleen in veilige netwerken)."
+ },
+ "profile": {
+ "label": "ONVIF-profiel",
+ "description": "Specifiek ONVIF-mediaprofiel voor PTZ-besturing, gekoppeld via token of naam. Indien niet ingesteld, wordt het eerste profiel met geldige PTZ-configuratie automatisch geselecteerd."
+ },
+ "autotracking": {
+ "label": "Automatisch volgen",
+ "description": "Bewegende objecten automatisch volgen en gecentreerd houden in het beeld via PTZ-camerabewegingen.",
+ "enabled": {
+ "label": "Automatisch volgen inschakelen",
+ "description": "Automatisch PTZ-camera volgen van gedetecteerde objecten in- of uitschakelen."
+ },
+ "calibrate_on_startup": {
+ "label": "Kalibreren bij opstarten",
+ "description": "PTZ-motorsnelheden meten bij opstarten voor nauwkeurigere volging. Frigate werkt de configuratie bij met movement_weights na kalibratie."
+ },
+ "zooming": {
+ "label": "Zoommodus",
+ "description": "Zoomgedrag instellen: disabled (alleen pan/tilt), absolute (meest compatibel) of relative (gelijktijdig pan/tilt/zoom)."
+ },
+ "zoom_factor": {
+ "label": "Zoomfactor",
+ "description": "Zoomniveau voor gevolgde objecten instellen. Lagere waarden tonen meer van de scène; hogere waarden zoomen verder in maar kunnen de volging verliezen. Waarden tussen 0,1 en 0,75."
+ },
+ "track": {
+ "label": "Gevolgde objecten",
+ "description": "Lijst van objecttypen die automatisch volgen activeren."
+ },
+ "required_zones": {
+ "label": "Vereiste zones",
+ "description": "Objecten moeten een van deze zones betreden voordat automatisch volgen begint."
+ },
+ "return_preset": {
+ "label": "Terugkeer-voorinstelling",
+ "description": "ONVIF-voorkeuzeinstelling in de camerafirmware om naar terug te keren na het volgen."
+ },
+ "timeout": {
+ "label": "Terugkeertimeout",
+ "description": "Dit aantal seconden wachten na het verliezen van de volging voordat de camera naar de voorkeuze-positie terugkeert."
+ },
+ "movement_weights": {
+ "label": "Bewegingsgewichten",
+ "description": "Kalibratiewaarden automatisch gegenereerd door camerakalbratie. Niet handmatig aanpassen."
+ },
+ "enabled_in_config": {
+ "label": "Originele autovolgstatus",
+ "description": "Intern veld om bij te houden of automatisch volgen was ingeschakeld in de configuratie."
+ }
+ },
+ "ignore_time_mismatch": {
+ "label": "Tijdsverschil negeren",
+ "description": "Tijdsynchronisatieverschillen tussen camera en Frigate-server negeren voor ONVIF-communicatie."
+ }
}
}
diff --git a/web/public/locales/nl/config/groups.json b/web/public/locales/nl/config/groups.json
index 6ecc7a6123..e69cd65051 100644
--- a/web/public/locales/nl/config/groups.json
+++ b/web/public/locales/nl/config/groups.json
@@ -49,7 +49,7 @@
},
"timestamp_style": {
"global": {
- "appearance": "Globaal voorkomen"
+ "appearance": "Algemeen uiterlijk"
},
"cameras": {
"appearance": "Voorkomen"
diff --git a/web/public/locales/nl/config/validation.json b/web/public/locales/nl/config/validation.json
index 6ddb7c764b..8f5391c825 100644
--- a/web/public/locales/nl/config/validation.json
+++ b/web/public/locales/nl/config/validation.json
@@ -1,6 +1,6 @@
{
"minimum": "Minimale waarde van {{limit}} vereist",
- "maximum": "Mag niet meer dan {{limit}} bedragen.",
+ "maximum": "Mag niet meer dan {{limit}} bedragen",
"exclusiveMinimum": "Waarde moet groter zijn dan {{limit}}",
"exclusiveMaximum": "Moet minder zijn dan {{limit}}",
"minLength": "Moet minstens {{limit}} karakters zijn",
@@ -28,5 +28,8 @@
"header_map": {
"roleHeaderRequired": "Rol titel is vereist wanneer rol bindingen zijn geconfigureerd."
}
+ },
+ "detect": {
+ "dimensionMustBeEven": "Het moet een even getal zijn."
}
}
diff --git a/web/public/locales/nl/objects.json b/web/public/locales/nl/objects.json
index c53f104167..d95ecb896b 100644
--- a/web/public/locales/nl/objects.json
+++ b/web/public/locales/nl/objects.json
@@ -120,5 +120,10 @@
"kangaroo": "Kangoeroe",
"skunk": "Stinkdier",
"school_bus": "Schoolbus",
- "royal_mail": "Royal Mail"
+ "royal_mail": "Royal Mail",
+ "canada_post": "Canada Post",
+ "baby": "Baby",
+ "baby_stroller": "Kinderwagen",
+ "rickshaw": "Riksja",
+ "rodent": "Knaagdier"
}
diff --git a/web/public/locales/nl/views/chat.json b/web/public/locales/nl/views/chat.json
new file mode 100644
index 0000000000..d4ffad1fb8
--- /dev/null
+++ b/web/public/locales/nl/views/chat.json
@@ -0,0 +1,17 @@
+{
+ "documentTitle": "Chat - Frigate",
+ "placeholder": "Stel een vraag...",
+ "error": "Er is iets misgegaan. Probeer opnieuw.",
+ "processing": "Verwerken...",
+ "toolsUsed": "Gebruikt: {{tools}}",
+ "hideTools": "Gereedschap verbergen",
+ "call": "Rinkel",
+ "title": "Frigate Chat",
+ "subtitle": "Jouw AI assistent voor camera beheer en inzichten",
+ "result": "Uitkomst",
+ "arguments": "Argumenten:",
+ "response": "Antwoord:",
+ "attachment_chip_remove": "Verwijder bijlage",
+ "open_in_explore": "Openen in Verken",
+ "showTools": "Gereedschap tonen"
+}
diff --git a/web/public/locales/nl/views/classificationModel.json b/web/public/locales/nl/views/classificationModel.json
index 40a947afc3..c58fc60e92 100644
--- a/web/public/locales/nl/views/classificationModel.json
+++ b/web/public/locales/nl/views/classificationModel.json
@@ -12,17 +12,18 @@
},
"toast": {
"success": {
- "deletedCategory_one": "Verwijderde klasse",
- "deletedCategory_other": "Verwijderde klassen",
- "deletedImage_one": "Verwijderde afbeelding",
- "deletedImage_other": "Verwijderde afbeeldingen",
+ "deletedCategory_one": "Verwijderd {{count}} klasse",
+ "deletedCategory_other": "Verwijderde {{count}} klassen",
+ "deletedImage_one": "Verwijderde {{count}} afbeelding",
+ "deletedImage_other": "Verwijderde {{count}} afbeeldingen",
"categorizedImage": "Succesvol geclassificeerde afbeelding",
"trainedModel": "Succesvol getraind model.",
"trainingModel": "Modeltraining succesvol gestart.",
"deletedModel_one": "{{count}} model succesvol verwijderd",
"deletedModel_other": "{{count}} modellen succesvol verwijderd",
"updatedModel": "Modelconfiguratie succesvol bijgewerkt",
- "renamedCategory": "Klasse succesvol hernoemd naar {{name}}"
+ "renamedCategory": "Klasse succesvol hernoemd naar {{name}}",
+ "reclassifiedImage": "Afbeelding succesvol opnieuw geclassificeerd"
},
"error": {
"deleteImageFailed": "Verwijderen mislukt: {{errorMessage}}",
@@ -32,7 +33,8 @@
"deleteModelFailed": "Model verwijderen mislukt: {{errorMessage}}",
"updateModelFailed": "Bijwerken van model mislukt: {{errorMessage}}",
"renameCategoryFailed": "Hernoemen van klasse mislukt: {{errorMessage}}",
- "trainingFailedToStart": "Het is niet gelukt om het model te trainen: {{errorMessage}}"
+ "trainingFailedToStart": "Het is niet gelukt om het model te trainen: {{errorMessage}}",
+ "reclassifyFailed": "Opnieuw classificeren van afbeelding mislukt: {{errorMessage}}"
}
},
"deleteCategory": {
@@ -154,8 +156,13 @@
"allImagesRequired_other": "Classificeer alle afbeeldingen. {{count}} afbeeldingen resterend.",
"modelCreated": "Model succesvol aangemaakt. Gebruik de weergave Recente classificaties om afbeeldingen voor ontbrekende statussen toe te voegen en train vervolgens het model.",
"missingStatesWarning": {
- "title": "Voorbeelden van ontbrekende staten",
- "description": "Het wordt aanbevolen om voor alle staten voorbeelden te selecteren voor het beste resultaat. Je kunt doorgaan zonder alle staten te selecteren, maar het model wordt pas getraind zodra alle staten afbeeldingen hebben. Na het doorgaan kun je in de weergave ‘Recente Classificaties’ de ontbrekende staten van afbeeldingen voorzien, en daarna het model trainen."
+ "title": "Ontbrekende klassevoorbeelden",
+ "description": "Niet alle klassen hebben voorbeelden. Probeer nieuwe voorbeelden te genereren om de ontbrekende klasse te vinden, of ga verder en gebruik de weergave 'Recente classificaties' om later afbeeldingen toe te voegen."
+ },
+ "refreshExamples": "Nieuwe voorbeelden genereren",
+ "refreshConfirm": {
+ "title": "Nieuwe voorbeelden genereren?",
+ "description": "Dit genereert een nieuwe set afbeeldingen en wist alle selecties, inclusief eerdere klassen. Je moet opnieuw voorbeelden selecteren voor alle klassen."
}
}
},
@@ -186,5 +193,7 @@
"modelNotReady": "Model is niet klaar voor training",
"noChanges": "Geen wijzigingen in de dataset sinds de laatste training."
},
- "none": "Geen overeenkomst"
+ "none": "Geen overeenkomst",
+ "reclassifyImageAs": "Afbeelding opnieuw classificeren als:",
+ "reclassifyImage": "Afbeelding opnieuw classificeren"
}
diff --git a/web/public/locales/nl/views/explore.json b/web/public/locales/nl/views/explore.json
index dcef557f02..911139074e 100644
--- a/web/public/locales/nl/views/explore.json
+++ b/web/public/locales/nl/views/explore.json
@@ -170,7 +170,8 @@
"attributes": "Classificatie-kenmerken",
"title": {
"label": "Titel"
- }
+ },
+ "scoreInfo": "Informatie over de score"
},
"itemMenu": {
"downloadVideo": {
@@ -221,6 +222,13 @@
"downloadCleanSnapshot": {
"label": "Download schone snapshot",
"aria": "Download schone snapshot"
+ },
+ "debugReplay": {
+ "label": "Debug-herhaling",
+ "aria": "Bekijk dit gevolgde object in de weergave voor het afspelen van foutopsporing"
+ },
+ "more": {
+ "aria": "Meer"
}
},
"noTrackedObjects": "Geen gevolgde objecten gevonden",
@@ -241,6 +249,9 @@
"confirmDelete": {
"title": "Bevestig Verwijderen",
"desc": "Het verwijderen van dit gevolgde object verwijdert de snapshot, alle opgeslagen embeddings en eventuele bijbehorende trackinggegevens van het object. Opgenomen videobeelden van dit object in de Geschiedenisweergave worden NIET verwijderd. Weet je zeker dat je wilt doorgaan?"
+ },
+ "toast": {
+ "error": "Fout bij het verwijderen van dit bijgehouden object: {{errorMessage}}"
}
},
"fetchingTrackedObjectsFailed": "Fout bij het ophalen van gevolgde objecten: {{errorMessage}}",
@@ -276,7 +287,10 @@
"zones": "Zones",
"ratio": "Verhouding",
"area": "Gebied",
- "score": "Score"
+ "score": "Score",
+ "computedScore": "Berekende score",
+ "topScore": "Hoogste score",
+ "toggleAdvancedScores": "Geavanceerde scores weergeven"
}
},
"annotationSettings": {
diff --git a/web/public/locales/nl/views/exports.json b/web/public/locales/nl/views/exports.json
index ffeda4a9ad..e90ebd92fa 100644
--- a/web/public/locales/nl/views/exports.json
+++ b/web/public/locales/nl/views/exports.json
@@ -4,7 +4,8 @@
"toast": {
"error": {
"renameExportFailed": "Het is niet gelukt om de export te hernoemen: {{errorMessage}}",
- "assignCaseFailed": "Kan toewijzing aan dossier niet bijwerken: {{errorMessage}}"
+ "assignCaseFailed": "Kan toewijzing aan dossier niet bijwerken: {{errorMessage}}",
+ "caseSaveFailed": "Mislukt om zaak op te slaan: {{errorMessage}}"
}
},
"editExport": {
@@ -22,7 +23,8 @@
"downloadVideo": "Download video",
"editName": "Naam bewerken",
"deleteExport": "Verwijder export",
- "assignToCase": "Toevoegen aan dossier"
+ "assignToCase": "Toevoegen aan dossier",
+ "removeFromCase": "Van zaak verwijderen"
},
"headings": {
"cases": "Gevallen",
@@ -35,5 +37,31 @@
"newCaseOption": "Nieuw dossier aanmaken",
"nameLabel": "Dossiernaam",
"descriptionLabel": "Beschrijving"
+ },
+ "deleteCase": {
+ "desc": "Weet je zeker dat je {{caseName}} wilt verwijderen?"
+ },
+ "caseCard": {
+ "emptyCase": "Nog geen exports"
+ },
+ "caseEditor": {
+ "createTitle": "Zaak aanmaken",
+ "editTitle": "Zaak wijzigen",
+ "namePlaceholder": "Zaaknaam",
+ "descriptionPlaceholder": "Voeg notities of context toe voor deze zaak"
+ },
+ "addExportDialog": {
+ "title": "Voeg export toe aan {{caseName}}",
+ "searchPlaceholder": "Zoek ongecategoriseerde exports",
+ "empty": "Geen niet-gecategoriseerde exports voldoen aan deze zoekopdracht.",
+ "addButton_one": "Voeg 1 export toe",
+ "addButton_other": "Voeg {{count}} exports toe",
+ "adding": "Toevoegen..."
+ },
+ "toolbar": {
+ "newCase": "Nieuwe zaak",
+ "addExport": "Export toevoegen",
+ "editCase": "Wijzig zaak",
+ "deleteCase": "Verwijder zaak"
}
}
diff --git a/web/public/locales/nl/views/faceLibrary.json b/web/public/locales/nl/views/faceLibrary.json
index a7fa2f6622..d314250c4d 100644
--- a/web/public/locales/nl/views/faceLibrary.json
+++ b/web/public/locales/nl/views/faceLibrary.json
@@ -21,7 +21,11 @@
"title": "Recente herkenningen",
"aria": "Selecteer recente herkenningen",
"empty": "Er zijn geen recente pogingen tot gezichtsherkenning",
- "titleShort": "Recent"
+ "titleShort": "Recent",
+ "emptyNoLibrary": {
+ "title": "Een gezicht uploaden",
+ "description": "U moet ten minste één gezicht aan de bibliotheek toevoegen om gezichtsherkenning te laten werken."
+ }
},
"selectFace": "Selecteer gezicht",
"toast": {
@@ -32,7 +36,8 @@
"updateFaceScoreFailed": "Niet gelukt om gezichtsscore bij te werken: {{errorMessage}}",
"uploadingImageFailed": "Afbeelding uploaden mislukt: {{errorMessage}}",
"trainFailed": "Trainen mislukt: {{errorMessage}}",
- "renameFaceFailed": "Het is niet gelukt om het gezicht te hernoemen: {{errorMessage}}"
+ "renameFaceFailed": "Het is niet gelukt om het gezicht te hernoemen: {{errorMessage}}",
+ "reclassifyFailed": "Opnieuw classificeren van gezicht mislukt: {{errorMessage}}"
},
"success": {
"deletedFace_one": "{{count}} gezicht is succesvol verwijderd.",
@@ -43,7 +48,8 @@
"deletedName_other": "{{count}} gezichten zijn succesvol verwijderd.",
"uploadedImage": "Afbeelding succesvol geüpload.",
"addFaceLibrary": "{{name}} is succesvol toegevoegd aan de Gezichtenbibliotheek!",
- "renamedFace": "Gezicht succesvol hernoemd naar {{name}}"
+ "renamedFace": "Gezicht succesvol hernoemd naar {{name}}",
+ "reclassifiedFace": "Gezicht succesvol geherclassificeerd."
}
},
"imageEntry": {
@@ -98,5 +104,7 @@
},
"collections": "Collecties",
"nofaces": "Geen gezichten beschikbaar",
- "pixels": "{{area}}px"
+ "pixels": "{{area}}px",
+ "reclassifyFaceAs": "Herclassificeer ‘Face’ als:",
+ "reclassifyFace": "Gezicht opnieuw classificeren"
}
diff --git a/web/public/locales/nl/views/live.json b/web/public/locales/nl/views/live.json
index a0b6cce79e..e12f191e8a 100644
--- a/web/public/locales/nl/views/live.json
+++ b/web/public/locales/nl/views/live.json
@@ -13,7 +13,8 @@
"clickMove": {
"label": "Klik in het frame om de camera te centreren",
"enable": "Klikken om te bewegen inschakelen",
- "disable": "Klikken om te bewegen uitschakelen"
+ "disable": "Klikken om te bewegen uitschakelen",
+ "enableWithZoom": "Schakel klikken om te verplaatsen / slepen om te zoomen in"
},
"right": {
"label": "Beweeg de PTZ-camera naar rechts"
@@ -53,7 +54,9 @@
},
"camera": {
"enable": "Camera inschakelen",
- "disable": "Camera uitschakelen"
+ "disable": "Camera uitschakelen",
+ "turnOn": "Camera inschakelen",
+ "turnOff": "Camera uitschakelen"
},
"muteCameras": {
"enable": "Alle camera's dempen",
@@ -107,7 +110,8 @@
},
"recording": {
"disable": "Opname uitschakelen",
- "enable": "Opname inschakelen"
+ "enable": "Opname inschakelen",
+ "disabledInConfig": "De opnamefunctie moet eerst worden ingeschakeld in de instellingen van deze camera."
},
"suspend": {
"forTime": "Onderbreken voor: "
@@ -149,7 +153,8 @@
"autotracking": "Automatisch volgen",
"snapshots": "Momentopnames",
"cameraEnabled": "Camera ingeschakeld",
- "transcription": "Audiotranscriptie"
+ "transcription": "Audiotranscriptie",
+ "camera": "Camera"
},
"history": {
"label": "Historische beelden weergeven"
diff --git a/web/public/locales/nl/views/motionSearch.json b/web/public/locales/nl/views/motionSearch.json
new file mode 100644
index 0000000000..b289113983
--- /dev/null
+++ b/web/public/locales/nl/views/motionSearch.json
@@ -0,0 +1,65 @@
+{
+ "startSearch": "Zoeken Starten",
+ "searchStarted": "Zoekopdracht gestart",
+ "searchCancelled": "Zoekopdracht geannuleerd",
+ "cancelSearch": "Annuleer",
+ "searching": "Zoekopdracht bezig.",
+ "searchComplete": "Zoekopdracht voltooid",
+ "title": "Beweging Zoeken",
+ "selectCamera": "Beweging Zoeken is aan het laden",
+ "noResultsYet": "Start een zoekactie om beweging te vinden in de geselecteerde regio",
+ "noChangesFound": "Geen pixel wijziging gedetecteerd in de geselecteerde regio",
+ "changesFound_one": "{{count}} bewegingsverandering gevonden",
+ "changesFound_other": "{{count}} bewegingsveranderingen gevonden",
+ "framesProcessed": "{{count}} frames verwerkt",
+ "jumpToTime": "Spring naar deze tijd",
+ "results": "Resultaten",
+ "documentTitle": "Beweging Zoeken - Frigate",
+ "description": "Teken een polygoon om het interessegebied te definieren en specifeer een tijdspanne voor het zoeken in dit gebied.",
+ "newSearch": "Nieuwe Zoekopdracht",
+ "clearResults": "Verwijder Resultaten",
+ "clearROI": "Verwijder Polygoon",
+ "polygonControls": {
+ "points_one": "{{count}} punt",
+ "points_other": "{{count}} punten",
+ "undo": "Verwijder het laatste punt",
+ "reset": "Herstel Polygoon"
+ },
+ "dialog": {
+ "title": "Beweging Zoeken",
+ "cameraLabel": "Camera"
+ },
+ "timeRange": {
+ "start": "Starttijd",
+ "end": "Eindtijd"
+ },
+ "settings": {
+ "title": "Zoekinstellingen",
+ "parallelMode": "Parallelle modus",
+ "parallelModeDesc": "Scan meerdere video segmenten tegelijk (sneller, maar significant meer CPU gebruik)",
+ "threshold": "Gevoeligheid drempel",
+ "thresholdDesc": "Lagere waardes detecteren eerder veranderingen (1-255)",
+ "minArea": "Minimaal wijzigings gebied",
+ "minAreaDesc": "Minimale percentage van gebied welke moet wijzigen om als significante wijziging aan te merken",
+ "frameSkip": "Frame overlaan",
+ "maxResults": "Maximaal aantal resultaten",
+ "maxResultsDesc": "Stop na dit aantal overeenkomende tijdstempels"
+ },
+ "errors": {
+ "polygonTooSmall": "De Polygoon moet minstens 3 punten bevatten",
+ "unknown": "Onbekende fout",
+ "noCamera": "Selecteer een camera",
+ "noROI": "Teken een interesse gebied a.u.b.",
+ "noTimeRange": "Selecteer een tijdsbereik a.u.b.",
+ "invalidTimeRange": "Eindtijd moet na de starttijd liggen",
+ "searchFailed": "Zoeken gefaald: {{message}}"
+ },
+ "changePercentage": "{{percentage}}% gewijzigd",
+ "metrics": {
+ "title": "Zoek Meetgegevens",
+ "segmentsScanned": "Gescande segmenten",
+ "segmentsProcessed": "Verwerkt",
+ "segmentsSkippedInactive": "Overgeslagen (geen activiteit)",
+ "segmentsSkippedHeatmap": "Overgeslagen (geen ROI overlap)"
+ }
+}
diff --git a/web/public/locales/nl/views/replay.json b/web/public/locales/nl/views/replay.json
new file mode 100644
index 0000000000..143c16ec48
--- /dev/null
+++ b/web/public/locales/nl/views/replay.json
@@ -0,0 +1,59 @@
+{
+ "websocket_messages": "Berichten",
+ "dialog": {
+ "camera": "Broncamera",
+ "preset": {
+ "1m": "Laatste 1 minuut",
+ "5m": "Laatste 5 minuten",
+ "timeline": "Vanaf tijdlijn",
+ "custom": "Aangepast"
+ },
+ "title": "Start Debug Herhaling",
+ "timeRange": "Tijdsbereik",
+ "startButton": "Start herhaling",
+ "selectFromTimeline": "Selecteer",
+ "starting": "Herhaling starten...",
+ "startLabel": "Start",
+ "endLabel": "Einde",
+ "description": "Maak een tijdelijke herhalingscamera die historische beelden in een lus afspeelt voor het debuggen van objectdetectie- en trackingproblemen. De herhalingscamera gebruikt dezelfde detectieconfiguratie als de broncamera. Kies een tijdsbereik om te beginnen.",
+ "toast": {
+ "error": "Kan debugherhaling niet starten: {{error}}",
+ "alreadyActive": "Er is al een herhalingssessie actief",
+ "stopError": "Kan debugherhaling niet stoppen: {{error}}",
+ "goToReplay": "Ga naar herhaling"
+ }
+ },
+ "title": "Debug Herhaling",
+ "description": "Herhaal camera-opnames voor foutopsporing. De objectlijst toont een vertraagde samenvatting van gedetecteerde objecten en het tabblad Berichten toont een stream van interne Frigate-berichten uit de herhaalde beelden.",
+ "page": {
+ "noSession": "Geen actieve debugherhalingssessie",
+ "noSessionDesc": "Start een debugherhaling vanuit de Geschiedenis-weergave door op de knop Acties in de werkbalk te klikken en Debug Herhaling te kiezen.",
+ "goToRecordings": "Ga naar Geschiedenis",
+ "preparingClip": "Clip voorbereiden…",
+ "preparingClipDesc": "Frigate voegt opnames samen voor het geselecteerde tijdsbereik. Dit kan bij langere bereiken even duren.",
+ "startingCamera": "Debugherhaling starten…",
+ "startError": {
+ "title": "Kan debugherhaling niet starten",
+ "back": "Terug naar Geschiedenis"
+ },
+ "sourceCamera": "Broncamera",
+ "replayCamera": "Herhalingscamera",
+ "initializingReplay": "Debugherhaling initialiseren...",
+ "stoppingReplay": "Debugherhaling stoppen...",
+ "stopReplay": "Stop herhaling",
+ "confirmStop": {
+ "title": "Debugherhaling stoppen?",
+ "description": "Dit stopt de sessie en ruimt alle tijdelijke gegevens op. Weet je het zeker?",
+ "confirm": "Stop herhaling",
+ "cancel": "Annuleren"
+ },
+ "activity": "Activiteit",
+ "objects": "Objectlijst",
+ "audioDetections": "Audiodetecties",
+ "noActivity": "Geen activiteit gedetecteerd",
+ "activeTracking": "Actieve tracking",
+ "noActiveTracking": "Geen actieve tracking",
+ "configuration": "Configuratie",
+ "configurationDesc": "Stem de instellingen voor bewegingsdetectie en objecttracking van de debugherhalingscamera nauwkeurig af. Wijzigingen worden niet opgeslagen in je Frigate-configuratiebestand."
+ }
+}
diff --git a/web/public/locales/nl/views/settings.json b/web/public/locales/nl/views/settings.json
index 1425acd22f..d1cd8293f8 100644
--- a/web/public/locales/nl/views/settings.json
+++ b/web/public/locales/nl/views/settings.json
@@ -3,7 +3,7 @@
"default": "Instellingen - Frigate",
"camera": "Camera-instellingen - Frigate",
"authentication": "Authenticatie-instellingen - Frigate",
- "motionTuner": "Motion Tuner - Frigate",
+ "motionTuner": "Beweging Tuner - Frigate",
"classification": "Classificatie-instellingen - Frigate",
"masksAndZones": "Masker- en zone-editor - Frigate",
"object": "Foutopsporing Frigate",
@@ -12,11 +12,12 @@
"notifications": "Meldingsinstellingen - Frigate",
"enrichments": "Verrijkingsinstellingen - Frigate",
"cameraManagement": "Camera's beheren - Frigate",
- "cameraReview": "Camera Review Instellingen - Frigate",
- "globalConfig": "Globale configuratie - Frigate",
+ "cameraReview": "Camera Beoordeling Instellingen - Frigate",
+ "globalConfig": "Globaale configuratie - Frigate",
"cameraConfig": "Camera-instellingen - Frigate",
"maintenance": "Onderhoud - Frigate",
- "profiles": "Profielen - Frigate"
+ "profiles": "Profielen - Frigate",
+ "detectorsAndModel": "Detectoren en model - Frigate"
},
"menu": {
"ui": "Gebruikersinterface",
@@ -34,7 +35,7 @@
"cameraManagement": "Beheer",
"cameraReview": "Beoordeel",
"general": "Algemeen",
- "globalConfig": "Globale configuratie",
+ "globalConfig": "Globaale configuratie",
"system": "Systeem",
"integrations": "Integraties",
"profileSettings": "Profielinstellingen",
@@ -76,7 +77,7 @@
"systemMqtt": "MQTT",
"systemEnvironmentVariables": "Omgevingsvariabelen",
"systemTelemetry": "Telemetrie",
- "systemBirdseye": "Overzicht",
+ "systemBirdseye": "Birdseye",
"systemFfmpeg": "FFmpeg",
"systemDetectorHardware": "Detectie hardware",
"cameraFaceRecognition": "Gezichtsherkenning",
@@ -88,7 +89,12 @@
"cameraOnvif": "ONVIF",
"cameraUi": "Camera UI",
"cameraTimestampStyle": "Tijdstempel stijl",
- "maintenance": "Onderhoud"
+ "maintenance": "Onderhoud",
+ "systemDetectorsAndModel": "Detectoren en model",
+ "cameraBirdseye": "Birdseye",
+ "cameraMqtt": "Camera MQTT",
+ "mediaSync": "Media-synchronisatie",
+ "regionGrid": "Regio-raster"
},
"dialog": {
"unsavedChanges": {
@@ -352,12 +358,27 @@
"zone": "zone",
"motion_mask": "bewegingsmasker",
"object_mask": "objectmasker"
+ },
+ "revertOverride": {
+ "title": "Terugzetten naar basisconfiguratie",
+ "desc": "Dit verwijdert de profieloverschrijving voor de {{type}} {{name}} en zet deze terug naar de basisconfiguratie."
}
},
"speed": {
"error": {
"mustBeGreaterOrEqualTo": "De snelheidsdrempel moet groter dan of gelijk zijn aan 0,1."
}
+ },
+ "id": {
+ "error": {
+ "mustNotBeEmpty": "ID mag niet leeg zijn.",
+ "alreadyExists": "Er bestaat al een masker met deze ID voor deze camera."
+ }
+ },
+ "name": {
+ "error": {
+ "mustNotBeEmpty": "Naam mag niet leeg zijn."
+ }
}
},
"zones": {
@@ -411,6 +432,10 @@
"allObjects": "Alle objecten",
"toast": {
"success": "Zone ({{zoneName}}) is opgeslagen."
+ },
+ "enabled": {
+ "title": "Ingeschakeld",
+ "description": "Of deze zone actief en ingeschakeld is in het configuratiebestand. Als deze is uitgeschakeld, kan deze niet via MQTT worden ingeschakeld. Uitgeschakelde zones worden tijdens runtime genegeerd."
}
},
"motionMasks": {
@@ -439,7 +464,13 @@
"noName": "Bewegingsmasker is opgeslagen."
}
},
- "add": "Nieuw bewegingsmasker"
+ "add": "Nieuw bewegingsmasker",
+ "defaultName": "Beweging Mask {{number}}",
+ "name": {
+ "title": "Name",
+ "description": "Een optionele vriendelijke naam voor dit bewegingsmasker.",
+ "placeholder": "Voer een naam in..."
+ }
},
"objectMasks": {
"label": "Objectmaskers",
@@ -464,11 +495,26 @@
"point_other": "{{count}} punten",
"clickDrawPolygon": "Klik om een polygoon op de afbeelding te tekenen.",
"context": "Objectfiltermaskers worden gebruikt om valse positieven uit te filteren voor een bepaald objecttype op basis van locatie.",
- "edit": "Objectmasker bewerken"
+ "edit": "Objectmasker bewerken",
+ "name": {
+ "title": "Name",
+ "description": "Een optionele vriendelijke naam voor dit objectmasker.",
+ "placeholder": "Voer een naam in..."
+ }
},
"restart_required": "Herstart vereist (maskers/zones gewijzigd)",
"motionMaskLabel": "Bewegingsmasker {{number}}",
- "objectMaskLabel": "Objectmasker {{number}} ({{label}})"
+ "objectMaskLabel": "Objectmasker {{number}}",
+ "disabledInConfig": "Item is uitgeschakeld in het configuratiebestand",
+ "addDisabledProfile": "Voeg dit eerst toe aan de basisconfiguratie en overschrijf het daarna in het profiel",
+ "profileBase": "(basis)",
+ "profileOverride": "(overschrijving)",
+ "masks": {
+ "enabled": {
+ "title": "Ingeschakeld",
+ "description": "Of dit masker is ingeschakeld in het configuratiebestand. Als het is uitgeschakeld, kan het niet via MQTT worden ingeschakeld. Uitgeschakelde maskers worden tijdens runtime genegeerd."
+ }
+ }
},
"motionDetectionTuner": {
"title": "Bewegingsdetectie-afsteller",
@@ -504,7 +550,7 @@
"desc": "Toon objectkaders rond gevolgde objecten",
"colors": {
"label": "Kleuren van objectkaders",
- "info": "Bij het opstarten wordt er een andere kleur toegewezen aan elk objectlabel. Een dunne donkerblauwe lijn geeft aan dat het object op dit moment niet wordt gedetecteerd. Een dunne grijze lijn geeft aan dat het object als stilstaand wordt herkend. Een dikke lijn geeft aan dat het object het doelwit is van automatische tracking (indien ingeschakeld). "
+ "info": "Bij het opstarten wordt er een andere kleur toegewezen aan elk objectlabel Een dunne donkerblauwe lijn geeft aan dat het object op dit moment niet wordt gedetecteerd Een dunne grijze lijn geeft aan dat het object als stilstaand wordt herkend Een dikke lijn geeft aan dat het object het doelwit is van automatische tracking (indien ingeschakeld) "
}
},
"timestamp": {
@@ -681,7 +727,7 @@
"desc": "Webpushmeldingen vereisen een veilige omgeving (https://…). Dit is een beperking van de browser. Open Frigate via een beveiligde verbinding om meldingen te kunnen ontvangen."
},
"globalSettings": {
- "title": "Globale instellingen",
+ "title": "Globaale instellingen",
"desc": "Meldingen voor specifieke camera's op alle geregistreerde apparaten tijdelijk uitschakelen."
},
"email": {
@@ -760,6 +806,14 @@
"plusModelType": {
"baseModel": "Basismodel",
"userModel": "Verfijnd"
+ },
+ "noModelLoaded": "Er is momenteel geen Frigate+-model geladen.",
+ "selectModel": "Selecteren a model",
+ "noModelsAvailable": "Geen modellen beschikbaar",
+ "filter": {
+ "ariaLabel": "Modellen filteren op type",
+ "baseModels": "Basismodellen",
+ "fineTunedModels": "Verfijnde modellen"
}
},
"toast": {
@@ -767,7 +821,15 @@
"error": "Configuratiewijzigingen konden niet worden opgeslagen: {{errorMessage}}"
},
"restart_required": "Herstart vereist (Frigate+ model gewijzigd)",
- "unsavedChanges": "Niet-opgeslagen wijzigingen in Frigate+ instellingen"
+ "unsavedChanges": "Niet-opgeslagen wijzigingen in Frigate+ instellingen",
+ "description": "Frigate+ is een abonnementsdienst die toegang biedt tot extra functies en mogelijkheden voor je Frigate-installatie, waaronder het gebruik van aangepaste objectdetectiemodellen die op je eigen gegevens zijn getraind. Je kunt je Frigate+-modelinstellingen hier beheren.",
+ "cardTitles": {
+ "api": "API",
+ "currentModel": "Huidig model",
+ "otherModels": "Andere modellen",
+ "configuration": "Configuratie"
+ },
+ "changeInDetectorsAndModel": "Van model wisselen"
},
"enrichments": {
"semanticSearch": {
@@ -1291,7 +1353,8 @@
},
"hikvision": {
"substreamWarning": "Substream 1 is beperkt tot een lage resolutie. Veel Hikvision-camera’s ondersteunen extra substreams die in de instellingen van de camera ingeschakeld moeten worden. Het wordt aanbevolen deze streams te controleren en te gebruiken indien beschikbaar."
- }
+ },
+ "resolutionUnknown": "De resolutie van deze stream kon niet worden uitgelezen. Stel de detectieresolutie handmatig in via Instellingen of in je configuratie."
}
}
},
@@ -1303,7 +1366,18 @@
"backToSettings": "Terug naar camera-instellingen",
"streams": {
"title": "Camera's in-/uitschakelen",
- "desc": "Schakel een camera tijdelijk uit totdat Frigate opnieuw wordt gestart. Het uitschakelen van een camera stopt de verwerking van de streams van deze camera volledig door Frigate. Detectie, opname en foutopsporing zijn dan niet beschikbaar. Let op: dit schakelt go2rtc-restreams niet uit. "
+ "desc": "Schakel een camera tijdelijk uit totdat Frigate opnieuw wordt gestart. Het uitschakelen van een camera stopt de verwerking van de streams van deze camera volledig door Frigate. Detectie, opname en foutopsporing zijn dan niet beschikbaar. Let op: dit schakelt go2rtc-restreams niet uit. ",
+ "enableLabel": "Ingeschakeld cameras",
+ "enableDesc": "Schakel een ingeschakelde camera tijdelijk uit totdat Frigate opnieuw wordt gestart. Het uitschakelen van een camera stopt de verwerking van de streams van deze camera volledig. Detectie, opname en foutopsporing zijn dan niet beschikbaar. Let op: dit schakelt go2rtc-restreams niet uit. ",
+ "disableLabel": "Uitgeschakeld cameras",
+ "disableDesc": "Schakel een camera in die momenteel niet zichtbaar is in de UI en is uitgeschakeld in de configuratie. Na het inschakelen is een herstart van Frigate vereist.",
+ "enableSuccess": "{{cameraName}} ingeschakeld in de configuratie. Herstart Frigate om de wijzigingen toe te passen.",
+ "friendlyName": {
+ "edit": "Cameranaam bewerken",
+ "title": "Weergavenaam bewerken",
+ "description": "Stel de vriendelijke naam in die voor deze camera in de Frigate-UI wordt weergegeven. Laat leeg om de camera-ID te gebruiken.",
+ "rename": "Hernoemen"
+ }
},
"cameraConfig": {
"add": "Camera toevoegen",
@@ -1333,6 +1407,35 @@
"toast": {
"success": "Camera {{cameraName}} is succesvol opgeslagen"
}
+ },
+ "description": "Voeg camera's toe, bewerk of verwijder ze, bepaal welke camera's zijn ingeschakeld en configureer overschrijvingen per profiel en cameratype. Kies voor streams, detectie, beweging en andere cameraspecifieke instellingen de betreffende sectie onder Cameraconfiguratie.",
+ "deleteCamera": "Verwijderen Camera",
+ "deleteCameraDialog": {
+ "title": "Verwijderen Camera",
+ "description": "Het verwijderen van een camera verwijdert permanent alle opnames, gevolgde objecten en configuratie voor die camera. Eventuele go2rtc-streams die aan deze camera zijn gekoppeld, moeten mogelijk nog handmatig worden verwijderd.",
+ "selectPlaceholder": "Kies camera...",
+ "confirmTitle": "Weet je het zeker?",
+ "confirmWarning": "Het verwijderen van {{cameraName}} kan niet ongedaan worden gemaakt.",
+ "deleteExports": "Verwijder ook exports voor deze camera",
+ "confirmButton": "Verwijderen Permanently",
+ "success": "Camera {{cameraName}} is succesvol verwijderd",
+ "error": "Kan camera {{cameraName}} niet verwijderen"
+ },
+ "profiles": {
+ "title": "Profiel Camera Overrides",
+ "selectLabel": "Selecteren profile",
+ "description": "Configureer welke camera's zijn ingeschakeld of uitgeschakeld wanneer een profiel wordt geactiveerd. Camera's die op \"Overnemen\" staan, behouden hun basisstatus.",
+ "inherit": "Overnemen",
+ "enabled": "Ingeschakeld",
+ "disabled": "Uitgeschakeld"
+ },
+ "cameraType": {
+ "title": "Cameratype",
+ "label": "Cameratype",
+ "description": "Stel het type voor elke camera in. Speciale LPR-camera's zijn camera's met één doel en krachtige optische zoom om kentekens van voertuigen op afstand vast te leggen. De meeste camera's moeten het normale cameratype gebruiken, tenzij de camera specifiek voor LPR is bedoeld en een nauwkeurig gericht beeld op kentekens heeft.",
+ "normal": "Normal",
+ "dedicatedLpr": "Speciale LPR",
+ "saveSuccess": "Cameratype voor {{cameraName}} bijgewerkt. Herstart Frigate om de wijzigingen toe te passen."
}
},
"cameraReview": {
@@ -1376,6 +1479,562 @@
"overriddenGlobal": "Overschreven (globaal)",
"overriddenGlobalTooltip": "Deze camera heeft voorrang op de algemene configuratie-instellingen in dit gedeelte",
"overriddenBaseConfig": "Overschreven (basis configuratie)",
- "overriddenBaseConfigTooltip": "Het profiel {{profile}} heeft voorrang op de configuratie-instellingen in dit gedeelte"
+ "overriddenBaseConfigTooltip": "Het profiel {{profile}} heeft voorrang op de configuratie-instellingen in dit gedeelte",
+ "overriddenGlobalHeading_one": "Deze camera overschrijft {{count}} veld uit de globale configuratie:",
+ "overriddenGlobalHeading_other": "Deze camera overschrijft {{count}} velden uit de globale configuratie:",
+ "overriddenGlobalNoDeltas": "Deze camera overschrijft de globale configuratie, maar er zijn geen afwijkende veldwaarden.",
+ "overriddenBaseConfigHeading_one": "Het profiel {{profile}} overschrijft {{count}} veld uit de basisconfiguratie:",
+ "overriddenBaseConfigHeading_other": "Het profiel {{profile}} overschrijft {{count}} velden uit de basisconfiguratie:",
+ "overriddenBaseConfigNoDeltas": "Het profiel {{profile}} overschrijft deze sectie, maar er zijn geen afwijkende veldwaarden ten opzichte van de basisconfiguratie.",
+ "overriddenInCameras": {
+ "label_one": "Overschreven in {{count}} camera",
+ "label_other": "Overschreven in {{count}} camera's",
+ "tooltip_one": "{{count}} camera overschrijft waarden in deze sectie. Klik om details te bekijken.",
+ "tooltip_other": "{{count}} camera's overschrijven waarden in deze sectie. Klik om details te bekijken.",
+ "heading_one": "Deze globale sectie bevat velden die in {{count}} camera worden overschreven.",
+ "heading_other": "Deze globale sectie bevat velden die in {{count}} camera's worden overschreven.",
+ "othersField_one": "{{count}} andere",
+ "othersField_other": "{{count}} andere",
+ "profilePrefix": "{{profile}}-profiel: {{fields}}"
+ }
+ },
+ "saveAllPreview": {
+ "title": "Wijzigingen die worden opgeslagen",
+ "triggerLabel": "Beoordeling pending changes",
+ "empty": "Geen openstaande wijzigingen.",
+ "scope": {
+ "label": "Bereik",
+ "global": "Globaal",
+ "camera": "Camera: {{cameraName}}"
+ },
+ "profile": {
+ "label": "Profiel"
+ },
+ "field": {
+ "label": "Veld"
+ },
+ "value": {
+ "label": "Nieuwe waarde",
+ "reset": "Resetten"
+ }
+ },
+ "timestampPosition": {
+ "tl": "Linksboven",
+ "tr": "Rechtsboven",
+ "bl": "Linksonder",
+ "br": "Rechtsonder"
+ },
+ "detectorsAndModel": {
+ "title": "Detectoren en model",
+ "description": "Configureer de detector-backend die objectdetectie uitvoert en het model dat daarbij wordt gebruikt. Wijzigingen worden samen opgeslagen zodat de detector en het model gesynchroniseerd blijven.",
+ "cardTitles": {
+ "detector": "Detector-hardware",
+ "model": "Detectie Model"
+ },
+ "tabs": {
+ "plus": "Frigate+",
+ "custom": "Aangepast Model"
+ },
+ "mismatch": {
+ "warning": "Het huidige Frigate+-model \"{{model}}\" vereist de {{required}}-detector. Kies hieronder een compatibel model of schakel over naar Aangepast model voordat je opslaat."
+ },
+ "plusModel": {
+ "requiresDetector": "Vereist: {{detector}}",
+ "noModelSelected": "Selecteren a Frigate+ model"
+ },
+ "toast": {
+ "saveSuccess": "Detector- en modelinstellingen zijn opgeslagen. Herstart Frigate om de wijzigingen toe te passen.",
+ "saveError": "Kan detector- en modelinstellingen niet opslaan"
+ },
+ "unsavedChanges": "Niet-opgeslagen wijzigingen aan detector en model",
+ "restartRequired": "Herstart vereist (detector of model gewijzigd)"
+ },
+ "maintenance": {
+ "title": "Onderhoud",
+ "sync": {
+ "title": "Media synchroniseren",
+ "desc": "Frigate ruimt media periodiek op volgens je retentieconfiguratie. Het is normaal dat er tijdens het gebruik van Frigate enkele verweesde bestanden ontstaan. Gebruik deze functie om verweesde mediabestanden van de schijf te verwijderen die niet langer in de database worden gebruikt.",
+ "started": "De mediasynchronisatie is gestart.",
+ "alreadyRunning": "Er wordt al een synchronisatietaak uitgevoerd",
+ "error": "Kan synchronisatie niet starten",
+ "currentStatus": "Status",
+ "jobId": "Verwerkingsnummer",
+ "startTime": "Starttijd",
+ "endTime": "Eindtijd",
+ "statusLabel": "Status",
+ "results": "Resultaten",
+ "errorLabel": "Fout",
+ "mediaTypes": "Mediatypen",
+ "allMedia": "Alle media",
+ "dryRun": "Proefdraaien",
+ "dryRunEnabled": "Er worden geen bestanden verwijderd",
+ "dryRunDisabled": "Bestanden worden verwijderd",
+ "force": "Gedwongen",
+ "forceDesc": "Negeer de veiligheidsdrempel en voltooi de synchronisatie, zelfs als meer dan 50% van de bestanden zou worden verwijderd.",
+ "verbose": "Uitgebreid",
+ "verboseDesc": "Schrijf een volledige lijst van verweesde bestanden naar de schijf ter controle.",
+ "running": "Synchroniseren bezig...",
+ "start": "Synchronisatie starten",
+ "inProgress": "Synchronisatie is bezig. Deze pagina is uitgeschakeld.",
+ "status": {
+ "queued": "In de wachtrij",
+ "running": "Bezig",
+ "completed": "Voltooid",
+ "failed": "Mislukt",
+ "notRunning": "Niet actief"
+ },
+ "resultsFields": {
+ "filesChecked": "Gecontroleerde bestanden",
+ "orphansFound": "Wezen gevonden",
+ "orphansDeleted": "Orphans Verwijderend",
+ "aborted": "Afgebroken. Het verwijderen zou de veiligheidsdrempel overschrijden.",
+ "error": "Fout",
+ "totals": "Totalen"
+ },
+ "event_snapshots": "Snapshots van gevolgde objecten",
+ "event_thumbnails": "Thumbnails van gevolgde objecten",
+ "review_thumbnails": "Beoordeling Thumbnails",
+ "previews": "Vooruitblikken",
+ "exports": "Exports",
+ "recordings": "Opnames"
+ },
+ "regionGrid": {
+ "title": "Regio-raster",
+ "desc": "Het region grid is een optimalisatie die leert waar objecten van verschillende groottes meestal verschijnen in het gezichtsveld van elke camera. Frigate gebruikt deze gegevens om detectieregio's efficiënt te schalen. Het grid wordt na verloop van tijd automatisch opgebouwd uit gegevens van gevolgde objecten.",
+ "clear": "Raster van de regio wissen",
+ "clearConfirmTitle": "Raster van de regio wissen",
+ "clearConfirmDesc": "Het wissen van het region grid wordt niet aanbevolen, tenzij je onlangs de modelgrootte van je detector hebt gewijzigd of de fysieke positie van je camera hebt aangepast en problemen hebt met objecttracking. Het grid wordt na verloop van tijd automatisch opnieuw opgebouwd terwijl objecten worden gevolgd. Een herstart van Frigate is vereist om de wijzigingen toe te passen.",
+ "clearSuccess": "Het raster van de regio is succesvol gewist",
+ "clearError": "Kan region grid niet wissen",
+ "restartRequired": "Herstart vereist om wijzigingen aan het region grid toe te passen"
+ }
+ },
+ "configForm": {
+ "global": {
+ "title": "Globaal Instellingen",
+ "description": "Deze instellingen gelden voor alle camera's, tenzij ze worden overschreven in de cameraspecifieke instellingen."
+ },
+ "camera": {
+ "title": "Camera Instellingen",
+ "description": "Deze instellingen gelden alleen voor deze camera en overschrijven de globale instellingen.",
+ "noCameras": "Geen camera's beschikbaar"
+ },
+ "advancedSettingsCount": "Advanced Instellingen ({{count}})",
+ "advancedCount": "Geavanceerd ({{count}})",
+ "showAdvanced": "Show Advanced Instellingen",
+ "tabs": {
+ "sharedDefaults": "Shared Standaards",
+ "system": "System",
+ "integrations": "Integraties"
+ },
+ "additionalProperties": {
+ "keyLabel": "Sleutel",
+ "valueLabel": "Waarde",
+ "keyPlaceholder": "Nieuwe sleutel",
+ "remove": "Verwijderen"
+ },
+ "knownPlates": {
+ "namePlaceholder": "bijv. de auto van mijn vrouw",
+ "platePlaceholder": "Kenteken of reguliere expressie"
+ },
+ "timezone": {
+ "defaultOption": "Tijdzone van browser gebruiken"
+ },
+ "roleMap": {
+ "empty": "Geen rolkoppelingen",
+ "roleLabel": "Role",
+ "groupsLabel": "Groepen",
+ "addMapping": "Rolkoppeling toevoegen",
+ "remove": "Verwijderen"
+ },
+ "ffmpegArgs": {
+ "preset": "Voorinstelling",
+ "manual": "Handmatige argumenten",
+ "inherit": "Overnemen van camera-instelling",
+ "none": "Geen",
+ "useGlobalSetting": "Overnemen uit algemene instelling",
+ "selectPreset": "Selecteren preset",
+ "manualPlaceholder": "Voer FFmpeg-argumenten in",
+ "presetLabels": {
+ "preset-rpi-64-h264": "Raspberry Pi (H.264)",
+ "preset-rpi-64-h265": "Raspberry Pi (H.265)",
+ "preset-vaapi": "VAAPI (Intel/AMD GPU)",
+ "preset-intel-qsv-h264": "Intel QuickSync (H.264)",
+ "preset-intel-qsv-h265": "Intel QuickSync (H.265)",
+ "preset-nvidia": "NVIDIA GPU",
+ "preset-jetson-h264": "NVIDIA Jetson (H.264)",
+ "preset-jetson-h265": "NVIDIA Jetson (H.265)",
+ "preset-rkmpp": "Rockchip RKMPP",
+ "preset-http-jpeg-generic": "HTTP JPEG (Generiek)",
+ "preset-http-mjpeg-generic": "HTTP MJPEG (Generiek)",
+ "preset-http-reolink": "HTTP - Reolink Camera's",
+ "preset-rtmp-generic": "RTMP (Generiek)",
+ "preset-rtsp-generic": "RTSP (Generiek)",
+ "preset-rtsp-restream": "RTSP - her-stream van go2rtc",
+ "preset-rtsp-restream-low-latency": "RTSP - her-stream van go2rtc (Lage latentie)",
+ "preset-rtsp-udp": "RTSP - UDP",
+ "preset-rtsp-blue-iris": "RTSP - Blue Iris",
+ "preset-record-generic": "Opnemen (generiek, geen audio)",
+ "preset-record-generic-audio-copy": "Opnemen (Generiek + Audio kopiëren)",
+ "preset-record-generic-audio-aac": "Opnemen (generiek + audio naar AAC)",
+ "preset-record-mjpeg": "Record - MJPEG Camera's",
+ "preset-record-jpeg": "Record - JPEG Camera's",
+ "preset-record-ubiquiti": "Record - Ubiquiti Camera's"
+ }
+ },
+ "cameraInputs": {
+ "itemTitle": "Stream {{index}}"
+ },
+ "restartRequiredField": "Herstart vereist",
+ "restartRequiredFooter": "Configuratie changed - Restart required",
+ "sections": {
+ "detect": "Detectie",
+ "record": "Opname",
+ "snapshots": "Snapshots",
+ "motion": "Beweging",
+ "objects": "Objecten",
+ "review": "Beoordeling",
+ "audio": "Audio",
+ "notifications": "Meldingen",
+ "live": "Live weergaven",
+ "timestamp_style": "Tijdstempels",
+ "mqtt": "MQTT",
+ "database": "Database",
+ "telemetry": "Telemetrie",
+ "auth": "Authenticatie",
+ "tls": "TLS",
+ "proxy": "Proxy",
+ "go2rtc": "go2rtc",
+ "ffmpeg": "FFmpeg",
+ "detectors": "Detectoren",
+ "model": "Model",
+ "semantic_search": "Semantic Zoeken",
+ "genai": "GenAI",
+ "face_recognition": "Gezichtsherkenning",
+ "lpr": "Kentekenherkenning",
+ "birdseye": "Birdseye",
+ "masksAndZones": "Maskers / Zones"
+ },
+ "detect": {
+ "title": "Detectie Instellingen"
+ },
+ "detectors": {
+ "title": "Detector Instellingen",
+ "singleType": "Er is slechts één {{type}}-detector toegestaan.",
+ "keyRequired": "Detectornaam is vereist.",
+ "keyDuplicate": "De naam van de detector bestaat al.",
+ "noSchema": "Geen detectorschema's beschikbaar.",
+ "none": "Geen detectorinstanties geconfigureerd.",
+ "add": "Detector toevoegen",
+ "addCustomKey": "Aangepaste sleutel toevoegen"
+ },
+ "record": {
+ "title": "Opname Instellingen"
+ },
+ "snapshots": {
+ "title": "Snapshot Instellingen"
+ },
+ "motion": {
+ "title": "Beweging Instellingen"
+ },
+ "objects": {
+ "title": "Object Instellingen"
+ },
+ "audioLabels": {
+ "summary": "{{count}} audiolabels geselecteerd",
+ "empty": "Geen audiolabels beschikbaar"
+ },
+ "objectLabels": {
+ "summary": "{{count}} objecttypen geselecteerd",
+ "empty": "Geen objectlabels beschikbaar"
+ },
+ "reviewLabels": {
+ "summary": "{{count}} labels geselecteerd",
+ "empty": "Geen labels beschikbaar"
+ },
+ "filters": {
+ "objectFieldLabel": "{{field}} voor {{label}}"
+ },
+ "zoneNames": {
+ "summary": "{{count}} geselecteerd",
+ "empty": "Geen zones beschikbaar"
+ },
+ "inputRoles": {
+ "summary": "{{count}} rollen geselecteerd",
+ "empty": "Geen rollen beschikbaar",
+ "options": {
+ "detect": "Detecteren",
+ "record": "Opnemen",
+ "audio": "Audio"
+ }
+ },
+ "genaiRoles": {
+ "options": {
+ "embeddings": "Embedding",
+ "descriptions": "Beschrijvingen",
+ "chat": "Chat"
+ }
+ },
+ "semanticSearchModel": {
+ "placeholder": "Selecteren model…",
+ "builtIn": "Ingebouwde modellen",
+ "genaiProviders": "Aanbieders van generatieve AI"
+ },
+ "review": {
+ "title": "Beoordeling Instellingen"
+ },
+ "audio": {
+ "title": "Audio Instellingen"
+ },
+ "notifications": {
+ "title": "Melding Instellingen"
+ },
+ "live": {
+ "title": "Live View Instellingen"
+ },
+ "timestamp_style": {
+ "title": "Timestamp Instellingen"
+ },
+ "searchPlaceholder": "Zoeken...",
+ "addCustomLabel": "Aangepast label toevoegen...",
+ "genaiModel": {
+ "placeholder": "Selecteren model…",
+ "search": "Zoeken models…",
+ "noModels": "Geen modellen beschikbaar"
+ }
+ },
+ "globalConfig": {
+ "title": "Globaal Configuratie",
+ "description": "Configureer globale instellingen die op alle camera's van toepassing zijn, tenzij ze worden overschreven.",
+ "toast": {
+ "success": "Globaal settings saved successfully",
+ "error": "Kan globale instellingen niet opslaan",
+ "validationError": "Validatie is mislukt"
+ }
+ },
+ "cameraConfig": {
+ "title": "Camera Configuratie",
+ "description": "Configure settings for individual cameras. Instellingen override global defaults.",
+ "overriddenBadge": "Overschreven",
+ "resetToGlobal": "Resetten to Globaal",
+ "toast": {
+ "success": "Camera-instellingen zijn succesvol opgeslagen",
+ "error": "Kan camera-instellingen niet opslaan"
+ }
+ },
+ "toast": {
+ "success": "Instellingen saved successfully",
+ "applied": "Instellingen applied successfully",
+ "successRestartRequired": "Instellingen saved successfully. Restart Frigate to apply your changes.",
+ "error": "Kan instellingen niet opslaan",
+ "validationError": "Validatie mislukt: {{message}}",
+ "resetSuccess": "Resetten to global defaults",
+ "resetError": "Kan instellingen niet resetten",
+ "saveAllSuccess_one": "Opslaand {{count}} section successfully.",
+ "saveAllSuccess_other": "Alle {{count}} secties zijn succesvol opgeslagen.",
+ "saveAllPartial_one": "{{successCount}} van {{totalCount}} sectie opgeslagen. {{failCount}} mislukt.",
+ "saveAllPartial_other": "{{successCount}} van {{totalCount}} secties opgeslagen. {{failCount}} mislukt.",
+ "saveAllFailure": "Kan niet alle secties opslaan."
+ },
+ "profiles": {
+ "title": "Profielen",
+ "activeProfile": "Active Profiel",
+ "noActiveProfile": "Geen actief profiel",
+ "active": "Active",
+ "activated": "Profiel '{{profile}}' activated",
+ "activateFailed": "Kan profiel niet instellen",
+ "deactivated": "Profiel deactivated",
+ "noProfiles": "Geen profielen gedefinieerd.",
+ "noOverrides": "Geen overschrijvingen",
+ "cameraCount_one": "{{count}} camera",
+ "cameraCount_other": "{{count}} cameras",
+ "columnCamera": "Camera",
+ "columnOverrides": "Profiel Overrides",
+ "baseConfig": "Basisconfiguratie",
+ "addProfile": "Toevoegen Profiel",
+ "newProfile": "New Profiel",
+ "profileNamePlaceholder": "bijv. Ingeschakeld, Afwezig, Nachtmodus",
+ "friendlyNameLabel": "Profiel Name",
+ "profileIdLabel": "Profiel ID",
+ "profileIdDescription": "Interne identificatie die wordt gebruikt in configuratie en automatiseringen",
+ "nameInvalid": "Alleen kleine letters, cijfers en onderstrepingstekens zijn toegestaan",
+ "nameDuplicate": "Er bestaat al een profiel met deze naam",
+ "error": {
+ "mustBeAtLeastTwoCharacters": "Moet minimaal 2 tekens bevatten",
+ "mustNotContainPeriod": "Mag geen punten bevatten",
+ "alreadyExists": "Er bestaat al een profiel met deze ID"
+ },
+ "renameProfile": "Rename Profiel",
+ "renameSuccess": "Profiel renamed to '{{profile}}'",
+ "deleteProfile": "Verwijderen Profiel",
+ "deleteProfileConfirm": "Profiel \"{{profile}}\" van alle camera's verwijderen? Dit kan niet ongedaan worden gemaakt.",
+ "deleteSuccess": "Profiel '{{profile}}' deleted",
+ "createSuccess": "Profiel '{{profile}}' created",
+ "removeOverride": "Verwijderen Profiel Override",
+ "deleteSection": "Verwijderen Section Overrides",
+ "deleteSectionConfirm": "De {{section}}-overschrijvingen voor profiel {{profile}} op {{camera}} verwijderen?",
+ "deleteSectionSuccess": "{{section}}-overschrijvingen voor {{profile}} verwijderd",
+ "enableSwitch": "Enable Profielen",
+ "enabledDescription": "Profielen zijn ingeschakeld. Maak hieronder een nieuw profiel aan, ga naar een cameraconfiguratiesectie om je wijzigingen aan te brengen en sla op om de wijzigingen toe te passen.",
+ "disabledDescription": "Met profielen kun je benoemde sets van cameraconfiguratie-overschrijvingen definiëren (bijv. ingeschakeld, afwezig, nacht) die op verzoek kunnen worden geactiveerd."
+ },
+ "unsavedChanges": "Er zijn wijzigingen die nog niet zijn opgeslagen",
+ "confirmReset": "Confirm Resetten",
+ "resetToDefaultDescription": "Dit zet alle instellingen in deze sectie terug naar hun standaardwaarden. Deze actie kan niet ongedaan worden gemaakt.",
+ "resetToGlobalDescription": "Dit zet de instellingen in deze sectie terug naar de globale standaardwaarden. Deze actie kan niet ongedaan worden gemaakt.",
+ "go2rtcStreams": {
+ "title": "go2rtc Streams",
+ "description": "Beheer go2rtc-streamconfiguraties voor het restreamen van camera's. Elke stream heeft een naam en één of meer bron-URL's.",
+ "addStream": "Stream toevoegen",
+ "addStreamDesc": "Voer een naam in voor de nieuwe stream. Deze naam wordt gebruikt om naar de stream te verwijzen in je cameraconfiguratie.",
+ "addUrl": "URL toevoegen",
+ "streamName": "Stream naam",
+ "streamNamePlaceholder": "bijv. voor_deur",
+ "streamUrlPlaceholder": "bijv, rtsp://user:pass@192.168.1.100/stream",
+ "deleteStream": "Verwijderen stream",
+ "deleteStreamConfirm": "Weet je zeker dat je de stream \"{{streamName}}\" wilt verwijderen? Camera's die naar deze stream verwijzen, werken mogelijk niet meer.",
+ "noStreams": "Geen go2rtc-streams geconfigureerd. Voeg een stream toe om te beginnen.",
+ "validation": {
+ "nameRequired": "Streamnaam is vereist",
+ "nameDuplicate": "Er bestaat al een stream met deze naam",
+ "nameInvalid": "Streamnaam mag alleen letters, cijfers, onderstrepingstekens en koppeltekens bevatten",
+ "urlRequired": "Er is minimaal één URL vereist"
+ },
+ "renameStream": "Stream hernoemen",
+ "renameStreamDesc": "Voer een nieuwe naam in voor deze stream. Het hernoemen van een stream kan camera's of andere streams die er op naam naar verwijzen verstoren.",
+ "newStreamName": "Nieuwe stream naam",
+ "ffmpeg": {
+ "useFfmpegModule": "Compatibiliteitsmodus gebruiken (ffmpeg)",
+ "video": "Video",
+ "audio": "Audio",
+ "hardware": "Hardware-versnelling",
+ "videoCopy": "Kopiëren",
+ "videoH264": "Transcoderen naar H.264",
+ "videoH265": "Transcoderen naar H.265",
+ "videoExclude": "Uitsluiten",
+ "audioCopy": "Kopiëren",
+ "audioAac": "Transcoderen naar AAC",
+ "audioOpus": "Transcoderen naar Opus",
+ "audioPcmu": "Transcoderen naar PCM μ-law",
+ "audioPcma": "Transcoderen naar PCM A-law",
+ "audioPcm": "Transcoderen naar PCM",
+ "audioMp3": "Transcoderen naar MP3",
+ "audioExclude": "Uitsluiten",
+ "hardwareNone": "Geen hardwareversnelling",
+ "hardwareAuto": "Automatische hardware-versnelling"
+ }
+ },
+ "birdseye": {
+ "trackingMode": {
+ "objects": "Objecten",
+ "motion": "Beweging",
+ "continuous": "Doorlopend"
+ }
+ },
+ "retainMode": {
+ "all": "Alle",
+ "motion": "Beweging",
+ "active_objects": "Active Objecten"
+ },
+ "previewQuality": {
+ "very_high": "Zeer hoog",
+ "high": "High",
+ "medium": "Medium",
+ "low": "Low",
+ "very_low": "Zeer laag"
+ },
+ "ui": {
+ "timeFormat": {
+ "browser": "Browser",
+ "12hour": "12 uur",
+ "24hour": "24 uur"
+ },
+ "TimeOrDateStyle": {
+ "full": "Full",
+ "long": "Lang",
+ "medium": "Medium",
+ "short": "Kort"
+ },
+ "unitSystem": {
+ "metric": "Metrisch",
+ "imperial": "Imperial"
+ }
+ },
+ "review": {
+ "imageSource": {
+ "recordings": "Opnames",
+ "previews": "Voorbeelden"
+ }
+ },
+ "logger": {
+ "logLevel": {
+ "debug": "Foutopsporing",
+ "info": "Info",
+ "warning": "Waarschuwing",
+ "error": "Fout",
+ "critical": "Kritisch"
+ }
+ },
+ "onvif": {
+ "profileAuto": "Auto",
+ "profileLoading": "Profielen laden...",
+ "autotracking": {
+ "zooming": {
+ "disabled": "Uitgeschakeld",
+ "absolute": "Absoluut",
+ "relative": "Relatief"
+ }
+ }
+ },
+ "modelSize": {
+ "small": "Klein",
+ "large": "Large"
+ },
+ "configMessages": {
+ "review": {
+ "recordDisabled": "Opname is disabled, review items will not be generated.",
+ "detectDisabled": "Object detection is disabled. Beoordeling items require detected objects to categorize alerts and detections.",
+ "allNonAlertDetections": "Alle activiteit die geen melding is, wordt opgenomen als detecties.",
+ "genaiImageSourceRecordingsRecordDisabled": "De afbeeldingsbron is ingesteld op 'recordings', maar opnemen is uitgeschakeld. Frigate valt terug op voorbeeldafbeeldingen."
+ },
+ "audio": {
+ "noAudioRole": "Er zijn geen streams met de audiorol gedefinieerd. Je moet de audiorol inschakelen om audiodetectie te laten werken."
+ },
+ "audioTranscription": {
+ "audioDetectionDisabled": "Audiodetectie is niet ingeschakeld voor deze camera. Audiotranscriptie vereist dat audiodetectie actief is."
+ },
+ "detect": {
+ "fpsGreaterThanFive": "Het instellen van de detectie-FPS hoger dan 5 wordt niet aanbevolen. Hogere waarden kunnen prestatieproblemen veroorzaken en leveren geen voordeel op.",
+ "disabled": "Objectdetectie is uitgeschakeld. Snapshots, beoordelingsitems en verrijkingen zoals gezichtsherkenning, kentekenherkenning en generatieve AI werken dan niet."
+ },
+ "objects": {
+ "genaiNoDescriptionsProvider": "Je moet een GenAI-provider configureren met de rol 'descriptions' om beschrijvingen te kunnen genereren."
+ },
+ "faceRecognition": {
+ "globalDisabled": "De verrijking voor gezichtsherkenning moet zijn ingeschakeld om gezichtsherkenningsfuncties op deze camera te laten werken.",
+ "personNotTracked": "Gezichtsherkenning vereist dat het object 'person' wordt gevolgd. Schakel 'person' in bij Objecten voor deze camera.",
+ "modelSizeLarge": "Het 'large'-model vereist een GPU of NPU voor redelijke prestaties. Gebruik 'small' op systemen met alleen een CPU."
+ },
+ "lpr": {
+ "globalDisabled": "De verrijking voor kentekenherkenning moet zijn ingeschakeld om LPR-functies op deze camera te laten werken.",
+ "vehicleNotTracked": "Kentekenherkenning vereist dat 'car' of 'motorcycle' wordt gevolgd. Schakel 'car' of 'motorcycle' in bij Objecten voor deze camera.",
+ "modelSizeLarge": "Het 'large'-model is geoptimaliseerd voor kentekenplaten met meerdere regels. Het 'small'-model presteert beter dan 'large' en moet worden gebruikt tenzij jouw regio kentekenformaten met meerdere regels gebruikt."
+ },
+ "record": {
+ "noRecordRole": "Er zijn geen streams met de opnamerol gedefinieerd. Opnemen werkt dan niet."
+ },
+ "birdseye": {
+ "objectsModeDetectDisabled": "Birdseye staat ingesteld op de modus 'objects', maar objectdetectie is uitgeschakeld voor deze camera. De camera wordt niet weergegeven in Birdseye."
+ },
+ "snapshots": {
+ "detectDisabled": "Objectdetectie is uitgeschakeld. Snapshots worden gegenereerd uit gevolgde objecten en worden daarom niet aangemaakt."
+ },
+ "detectors": {
+ "mixedTypes": "Alle detectoren moeten hetzelfde type gebruiken. Verwijder bestaande detectoren om een ander type te gebruiken.",
+ "mixedTypesSuggestion": "Alle detectoren moeten hetzelfde type gebruiken. Verwijder bestaande detectoren of selecteer {{type}}."
+ },
+ "semanticSearch": {
+ "jinav2SmallModelSize": "De 'small'-grootte met het Jina V2-model heeft hoge RAM- en inferentiekosten. Het 'large'-model met een aparte GPU wordt aanbevolen."
+ }
}
}
diff --git a/web/public/locales/pl/audio.json b/web/public/locales/pl/audio.json
index 6d5350572b..9a58b982ea 100644
--- a/web/public/locales/pl/audio.json
+++ b/web/public/locales/pl/audio.json
@@ -2,7 +2,7 @@
"speech": "Mowa",
"babbling": "Gaworzenie",
"yell": "Krzyk",
- "bellow": "Ryk",
+ "bellow": "Poniżej",
"whoop": "Okrzyk",
"whispering": "Szept",
"laughter": "Śmiech",
@@ -426,7 +426,7 @@
"sanding": "Szlifowanie",
"clock": "Zegar",
"tick": "Tykanie",
- "sodeling": "Sodeling",
+ "sodeling": "Jodłowanie",
"liquid": "Płyn",
"splash": "Plusk",
"slosh": "Rozchlapywanie",
@@ -444,8 +444,8 @@
"clicking": "Klikanie",
"inside": "Wewnątrz",
"outside": "Na zewnątrz",
- "chird": "Child",
- "change_ringing": "Zmienny dzwonek",
+ "chird": "Akord",
+ "change_ringing": "Karylion",
"shofar": "Szofar",
"trickle": "Spływanie",
"gush": "Wylew",
@@ -479,12 +479,12 @@
"rustle": "Szelest",
"whir": "Świst",
"clatter": "Stukot",
- "sizzle": "Sizzle",
+ "sizzle": "Skwierczenie",
"clickety_clack": "Klik-klak",
"rumble": "Grzmot",
- "plop": "Plop",
+ "plop": "Pluśnięcie",
"hum": "Szum",
- "zing": "Zing",
+ "zing": "Błysk",
"boing": "Odbicie",
"crunch": "Chrupnięcie",
"sine_wave": "Sinusoida",
diff --git a/web/public/locales/pl/common.json b/web/public/locales/pl/common.json
index e6fea5b424..f9238b4b72 100644
--- a/web/public/locales/pl/common.json
+++ b/web/public/locales/pl/common.json
@@ -156,7 +156,19 @@
"off": "WYŁĄCZ",
"edit": "Edytuj",
"copyCoordinates": "Kopiuj współrzędne",
- "continue": "Kontynuuj"
+ "continue": "Kontynuuj",
+ "add": "Dodaj",
+ "applying": "Zastosowywanie…",
+ "undo": "Cofnij",
+ "copiedToClipboard": "Skopiowano do schowka",
+ "modified": "Zmodyfikowane",
+ "overridden": "Nadpisany",
+ "resetToDefault": "Przywróć do domyślnych",
+ "saveAll": "Zapisz wszystkie",
+ "savingAll": "Zapisywanie wszystkich…",
+ "undoAll": "Cofnij wszystko",
+ "retry": "Powtórz",
+ "resetToGlobal": "Przywróć ustawienie globalne"
},
"menu": {
"system": "System",
@@ -207,7 +219,9 @@
"gl": "Galego (Galicyjski)",
"id": "Bahasa Indonesia (Indonezyjski)",
"ur": "اردو (Urdu)",
- "hr": "Hrvatski (Chorwacki)"
+ "hr": "Hrvatski (Chorwacki)",
+ "zhHant": "繁體中文 (Chiński Tradycyjny)",
+ "bs": "Bosanski (Bośniacki)"
},
"appearance": "Wygląd",
"darkMode": {
@@ -260,7 +274,11 @@
"help": "Pomoc",
"settings": "Ustawienia",
"export": "Eksportuj",
- "classification": "Klasyfikacja"
+ "classification": "Klasyfikacja",
+ "profiles": "Profile",
+ "actions": "Akcje",
+ "features": "Funkcje",
+ "chat": "Czat"
},
"role": {
"viewer": "Przeglądający",
@@ -298,7 +316,8 @@
"title": "Nie udało się zapisać zmian konfiguracji: {{errorMessage}}",
"noMessage": "Nie udało się zapisać zmian konfiguracji"
},
- "title": "Zapisz"
+ "title": "Zapisz",
+ "success": "Konfiguracja zapisana."
}
},
"readTheDocumentation": "Przeczytaj dokumentację",
@@ -313,5 +332,10 @@
"field": {
"optional": "Opcjonalny",
"internalID": "Wewnętrzny identyfikator używany przez Frigate w konfiguracji i bazie danych"
+ },
+ "no_items": "Brak elementów",
+ "validation_errors": "Błędy walidacji",
+ "credentialField": {
+ "savedPlaceholder": "Zapisano — pozostaw puste, aby zachować bieżącą wartość"
}
}
diff --git a/web/public/locales/pl/components/camera.json b/web/public/locales/pl/components/camera.json
index ada44e296b..dfbd82300c 100644
--- a/web/public/locales/pl/components/camera.json
+++ b/web/public/locales/pl/components/camera.json
@@ -68,7 +68,10 @@
"stream": "Strumień"
},
"birdseye": "Widok z lotu ptaka"
- }
+ },
+ "showAll": "Pokaż wszystkie grupy kamer",
+ "showLess": "Pokaż mniej",
+ "editGroups": "Edytuj grupy kamer"
},
"debug": {
"options": {
diff --git a/web/public/locales/pl/components/dialog.json b/web/public/locales/pl/components/dialog.json
index 994aeb53be..c2cde61599 100644
--- a/web/public/locales/pl/components/dialog.json
+++ b/web/public/locales/pl/components/dialog.json
@@ -68,15 +68,87 @@
"toast": {
"success": "Pomyślnie rozpoczęto eksport. Zobacz plik na stronie eksportów.",
"error": {
- "failed": "Nie udało się rozpocząć eksportu: {{error}}",
+ "failed": "Nie udało się zakolejkować eksportu: {{error}}",
"endTimeMustAfterStartTime": "Czas zakończenia musi być późniejszy niż czas rozpoczęcia",
"noVaildTimeSelected": "Nie wybrano prawidłowego zakresu czasu"
},
- "view": "Widok"
+ "view": "Widok",
+ "queued": "Eksport dodany do kolejki. Postęp można sprawdzić na stronie eksportów.",
+ "batchSuccess_one": "Rozpoczęto 1 eksport. Otwieranie przypadku.",
+ "batchSuccess_few": "Rozpoczęto {{count}} eksporty Otwieranie przypadku.",
+ "batchSuccess_many": "Rozpoczęto {{count}} eksportów. Otwieranie przypadku.",
+ "batchPartial": "Rozpoczęto {{successful}} z {{total}} eksportów. Nieudane kamery: {{failedCameras}}",
+ "batchFailed": "Nie udało się uruchomić {{total}} eksportów. Nieudane kamery: {{failedCameras}}",
+ "batchQueuedSuccess_one": "Eksport dodany do kolejki. Otwieranie przypadku.",
+ "batchQueuedSuccess_few": "Dodano {{count}} eksportydo kolejki. Otwieranie przypadku.",
+ "batchQueuedSuccess_many": "Dodano {{count}} eksportów do kolejki. Otwieranie przypadku.",
+ "batchQueuedPartial": "Dodano do kolejki {{successful}} z {{total}} eksportów. Nieudane kamery: {{failedCameras}}",
+ "batchQueueFailed": "Nie udało się dodać do kolejki {{total}} eksportów. Nieudane kamery: {{failedCameras}}"
},
"fromTimeline": {
"saveExport": "Zapisz Eksport",
- "previewExport": "Podgląd Eksportu"
+ "previewExport": "Podgląd Eksportu",
+ "queueingExport": "Kolejkowanie eksportu...",
+ "useThisRange": "Użyj tego zakresu"
+ },
+ "multiCamera": {
+ "timeRange": "Zakres czasu",
+ "selectFromTimeline": "Wybierz z osi czasu",
+ "cameraSelection": "Kamery",
+ "cameraSelectionHelp": "Kamery ze śledzonymi obiektami w tym przedziale czasowym są wstępnie wybierane",
+ "checkingActivity": "Sprawdzanie aktywności kamery...",
+ "noCameras": "Brak dostępnych kamer",
+ "detectionCount_one": "{{count}} śledzony obiekt",
+ "detectionCount_few": "{{count}} śledzone obiekty",
+ "detectionCount_many": "{{count}} śledzonych obiektów",
+ "nameLabel": "Wyeksportuj nazwę",
+ "namePlaceholder": "Opcjonalna nazwa bazowa eksportów",
+ "queueingButton": "Kolejkowanie eksportów...",
+ "exportButton_one": "Eksport 1 kamery",
+ "exportButton_few": "Eksport {{count}} kamer",
+ "exportButton_many": "Eksport {{count}} kamer",
+ "searchOrSelectGroup": "Szukaj lub wybierz grupę kamer ...",
+ "selectAll": "Wybierz wszystkie kamery",
+ "clearSelection": "Wyczyść wybór",
+ "selectWithActivity": "Kamery z obiektami śledzonymi",
+ "selectGroup": "Wybierz grupę",
+ "noMatchingCameras": "Nie ma kamer spełniających Twoje wyszukiwanie",
+ "selectedCount": "{{selected}} / {{total}} wybrane"
+ },
+ "case": {
+ "newCaseOption": "Utwórz nowy przypadek",
+ "newCaseNamePlaceholder": "Nazwa nowego przypadku",
+ "newCaseDescriptionPlaceholder": "Opis przypadku",
+ "label": "Przypadek",
+ "nonAdminHelp": "Dla tych eksportów zostanie utworzony nowy przypadek.",
+ "placeholder": "Wybierz przypadek"
+ },
+ "queueing": "Dodawanie eksportu do kolejki...",
+ "tabs": {
+ "export": "Pojedyncza kamera",
+ "multiCamera": "Wiele kamer"
+ },
+ "multi": {
+ "title_one": "Eksport 1 recenzji",
+ "title_few": "Eksport {{count}} recenzji",
+ "title_many": "Eksport {{count}} recenzji",
+ "description": "Eksportuj każdą wybraną recenzję. Wszystkie eksporty zostaną pogrupowane w jednym przypadku.",
+ "descriptionNoCase": "Eksportuj każdą wybraną recenzję.",
+ "caseNamePlaceholder": "Eksport recenzji – {{date}}",
+ "exportButton_one": "Eksport 1 recenzji",
+ "exportButton_few": "Eksport {{count}} recenzji",
+ "exportButton_many": "Eksport {{count}} recenzji",
+ "exportingButton": "Eksportowanie...",
+ "toast": {
+ "started_one": "Rozpoczęto 1 eksport. Otwieranie przypadku.",
+ "started_few": "Rozpoczęto {{count}} eksportów. Otwieranie przypadku.",
+ "started_many": "Rozpoczęto {{count}} eksportów. Otwieranie przypadku.",
+ "startedNoCase_one": "Rozpoczęto 1 eksport.",
+ "startedNoCase_few": "Rozpoczęto {{count}} eksportów.",
+ "startedNoCase_many": "Rozpoczęto {{count}} eksportów.",
+ "partial": "Rozpoczęto {{successful}} z {{total}} eksportów. Niepowodzenia: {{failedItems}}",
+ "failed": "Nie udało się uruchomić {{total}} eksportów. Niepowodzenia: {{failedItems}}"
+ }
}
},
"recording": {
@@ -95,6 +167,14 @@
"success": "Nagrania wideo powiązane z wybranymi elementami przeglądu zostały pomyślnie usunięte.",
"error": "Nie udało się usunąć: {{error}}"
}
+ },
+ "shareTimestamp": {
+ "label": "Udostępnij znacznik czasu",
+ "title": "Udostępnij znacznik czasu",
+ "description": "Udostępnij adres URL z aktualnym znacznikiem czasu pozycji odtwarzacza lub wybierz niestandardowy znacznik czasu. Należy pamiętać, że nie jest to publiczny link udostępniania i jest dostępny wyłącznie dla użytkowników mających dostęp do Frigate i tej kamery.",
+ "custom": "Własny znacznik czasu",
+ "button": "Udostępnij adres URL ze znacznikiem czasu",
+ "shareTitle": "Znacznik czasu przeglądu Frigate: {{camera}}"
}
},
"streaming": {
diff --git a/web/public/locales/pl/components/player.json b/web/public/locales/pl/components/player.json
index 227813f9c9..3cd8520438 100644
--- a/web/public/locales/pl/components/player.json
+++ b/web/public/locales/pl/components/player.json
@@ -33,7 +33,8 @@
"noPreviewFoundFor": "Nie znaleziono podglądu dla {{cameraName}}",
"submitFrigatePlus": {
"title": "Wyślij tę klatkę do Frigate+?",
- "submit": "Wyślij"
+ "submit": "Wyślij",
+ "previewError": "Nie udało się załadować podglądu nagrania. Nagranie może być niedostępne w podanym czasie."
},
"livePlayerRequiredIOSVersion": "Wymagana wersja iOS 17.1 lub nowsza dla tego typu transmisji na żywo.",
"streamOffline": {
@@ -47,5 +48,6 @@
"error": {
"submitFrigatePlusFailed": "Nie udało się wysłać klatki do Frigate+"
}
- }
+ },
+ "cameraOff": "Kamera jest wyłączona"
}
diff --git a/web/public/locales/pl/config/cameras.json b/web/public/locales/pl/config/cameras.json
index 9943f13328..f890558aa7 100644
--- a/web/public/locales/pl/config/cameras.json
+++ b/web/public/locales/pl/config/cameras.json
@@ -176,7 +176,7 @@
"description": "Włączone"
},
"audio": {
- "label": "Zdarzenia audio",
+ "label": "Wykrywanie dźwięku",
"description": "Ustawienia detekcji zdarzeń audio dla tej kamery.",
"enabled": {
"label": "Włącz detekcję audio",
@@ -196,7 +196,11 @@
},
"filters": {
"label": "Filtry audio",
- "description": "Ustawienia filtrów dla poszczególnych typów audio, takie jak progi pewności, używane do redukcji fałszywych alarmów."
+ "description": "Ustawienia filtrów dla poszczególnych typów audio, takie jak progi pewności, używane do redukcji fałszywych alarmów.",
+ "threshold": {
+ "label": "Minimalna pewność wykrycia dźwięku",
+ "description": "Minimalny próg pewności, aby zdarzenie audio zostało zliczone."
+ }
},
"enabled_in_config": {
"label": "Pierwotny stan audio",
@@ -221,5 +225,116 @@
"label": "Transkrypcja na żywo",
"description": "Włącz transkrypcję strumieniową audio na żywo w momencie jego odbierania."
}
+ },
+ "birdseye": {
+ "mode": {
+ "label": "Tryb śledzenia",
+ "description": "Tryb uwzględniania kamer w widoku z lotu ptaka: „obiekty”, „ruch” lub „ciągły”."
+ },
+ "order": {
+ "label": "Pozycja",
+ "description": "Pozycja numeryczna określająca kolejność kamery w układzie widoku z lotu ptaka."
+ },
+ "label": "Widok z lotu ptaka",
+ "description": "Ustawienia widoku z lotu ptaka „Birdseye”, który łączy obraz z wielu kamer w jeden układ.",
+ "enabled": {
+ "label": "Włącz widok z lotu ptaka",
+ "description": "Włącz lub wyłącz funkcję widoku z lotu ptaka."
+ }
+ },
+ "detect": {
+ "enabled": {
+ "label": "Włącz wykrywanie obiektów",
+ "description": "Włącza lub wyłącza wykrywanie obiektów dla tej kamery."
+ },
+ "stationary": {
+ "label": "Konfiguracja obiektów stacjonarnych",
+ "description": "Ustawienia wykrywania i obsługi obiektów statycznych przez określony czas.",
+ "interval": {
+ "label": "Okres bezruchu",
+ "description": "Częstotliwość (w klatkach) sprawdzania detekcji dla obiektów nieruchomych."
+ },
+ "threshold": {
+ "label": "Próg bezruchu",
+ "description": "Liczba klatek bez zmiany pozycji potrzebna do uznania obiektu za nieruchomy."
+ },
+ "max_frames": {
+ "label": "Maksymalna liczba klatek",
+ "description": "Ogranicza czas śledzenia nieruchomych obiektów przed ich usunięciem.",
+ "default": {
+ "label": "Domyślna maks. liczba klatek",
+ "description": "Domyślna maksymalna liczba klatek śledzenia nieruchomego obiektu przed zatrzymaniem."
+ },
+ "objects": {
+ "label": "Maksymalna liczba klatek obiektu",
+ "description": "Nadpisania dla poszczególnych obiektów określające maksymalną liczbę klatek śledzenia nieruchomych obiektów."
+ }
+ },
+ "classifier": {
+ "label": "Włącz klasyfikator wizualny",
+ "description": "Użyj klasyfikatora wizualnego do wykrywania rzeczywiście nieruchomych obiektów, nawet gdy ramki ograniczające drżą."
+ }
+ },
+ "label": "Wykrywanie obiektów",
+ "description": "Ustawienia roli „detection/detect” używanej do wykrywania obiektów i inicjalizacji trackerów.",
+ "height": {
+ "label": "Wysokość wykrywania",
+ "description": "Wysokość (w pikselach) klatek używanych w strumieniu detekcji; pozostaw puste, aby użyć natywnej rozdzielczości strumienia."
+ },
+ "width": {
+ "label": "Szerokość wykrywania",
+ "description": "Szerokość (w pikselach) klatek używanych w strumieniu detekcji; pozostaw puste, aby użyć natywnej rozdzielczości strumienia."
+ },
+ "fps": {
+ "label": "FPS wykrywania",
+ "description": "Docelowa liczba klatek na sekundę używana do detekcji; niższe wartości zmniejszają użycie CPU (zalecana wartość to 5, ustaw wyższą — maksymalnie 10 — tylko jeśli śledzisz bardzo szybko poruszające się obiekty)."
+ },
+ "min_initialized": {
+ "label": "Minimalna liczba klatek inicjalizacyjnych",
+ "description": "Liczba kolejnych trafień detekcji wymagana do utworzenia śledzonego obiektu. Zwiększenie wartości zmniejsza liczbę fałszywych inicjalizacji. Wartość domyślna to fps podzielone przez 2."
+ },
+ "max_disappeared": {
+ "label": "Maksymalna liczba pominiętych klatek",
+ "description": "Liczba klatek bez detekcji, po której obiekt śledzony jest uznawany za utracony."
+ },
+ "annotation_offset": {
+ "label": "Przesunięcie oznaczenia",
+ "description": "Liczba milisekund przesunięcia adnotacji detekcji w celu lepszego dopasowania ramek ograniczających na osi czasu do nagrań; wartość może być dodatnia lub ujemna."
+ }
+ },
+ "face_recognition": {
+ "label": "Rozpoznawanie twarzy",
+ "description": "Ustawienia wykrywania i rozpoznawania twarzy dla tej kamery.",
+ "enabled": {
+ "label": "Włącz rozpoznawanie twarzy",
+ "description": "Włącz lub wyłącz rozpoznawanie twarzy."
+ },
+ "min_area": {
+ "label": "Minimalny obszar twarzy",
+ "description": "Minimalny obszar twarzy (w pikselach) wymagany do podjęcia próby rozpoznania."
+ }
+ },
+ "ffmpeg": {
+ "label": "Strumienie (FFmpeg)",
+ "output_args": {
+ "detect": {
+ "description": "Domyślne argumenty wyjściowe dla strumieni z rolą detekcji."
+ },
+ "record": {
+ "description": "Domyślne argumenty wyjściowe dla strumieni z rolą zapisu."
+ }
+ },
+ "retry_interval": {
+ "label": "Czas ponownej próby FFmpeg",
+ "description": "Czas (w sekundach) przed ponownym uruchomieniem utraconego strumienia kamery. Domyślnie: 10."
+ },
+ "apple_compatibility": {
+ "label": "Kompatybilność z Apple",
+ "description": "Włącz tagowanie HEVC, aby zapewnić lepszą zgodność z odtwarzaczami Apple podczas nagrywania w formacie H.265."
+ },
+ "gpu": {
+ "label": "Indeks GPU",
+ "description": "Domyślny indeks karty graficznej używany do akceleracji sprzętowej, jeśli jest dostępna."
+ }
}
}
diff --git a/web/public/locales/pl/config/global.json b/web/public/locales/pl/config/global.json
index ed12af3c70..59d2232a06 100644
--- a/web/public/locales/pl/config/global.json
+++ b/web/public/locales/pl/config/global.json
@@ -1,6 +1,6 @@
{
"audio": {
- "label": "Zdarzenia audio",
+ "label": "Wykrywanie dźwięku",
"enabled": {
"label": "Włącz detekcję audio"
},
@@ -18,7 +18,11 @@
},
"filters": {
"label": "Filtry audio",
- "description": "Ustawienia filtrów dla poszczególnych typów audio, takie jak progi pewności, używane do redukcji fałszywych alarmów."
+ "description": "Ustawienia filtrów dla poszczególnych typów audio, takie jak progi pewności, używane do redukcji fałszywych alarmów.",
+ "threshold": {
+ "label": "Minimalna pewność wykrycia dźwięku",
+ "description": "Minimalny próg pewności, aby zdarzenie audio zostało zliczone."
+ }
},
"enabled_in_config": {
"label": "Pierwotny stan audio",
@@ -38,6 +42,142 @@
}
},
"version": {
- "label": "Aktualna wersja"
+ "label": "Bieżąca wersja konfiguracji",
+ "description": "Liczbowa lub znakowa wersja aktywnej konfiguracji w celu wykrywania migracji lub zmiany formatu."
+ },
+ "safe_mode": {
+ "label": "Tryb bezpieczny",
+ "description": "Po włączeniu Frigate uruchomi się w trybie awaryjnym z ograniczonymi funkcjami, co ułatwi rozwiązywanie problemów."
+ },
+ "environment_vars": {
+ "label": "Zmienne środowiskowe",
+ "description": "Pary klucz-wartość zmiennych środowiskowych, które należy ustawić dla procesu Frigate w systemie Home Assistant. Użytkownicy niekorzystający z Home Assistant powinni zamiast tego skorzystać z konfiguracji zmiennych środowiskowych w Dockerze."
+ },
+ "logger": {
+ "label": "Rejestrowanie",
+ "description": "Konfiguracja domyślnej szczegółowości logów oraz indywidualnych ustawień dla komponentów.",
+ "default": {
+ "label": "Poziom szczegółowości logów",
+ "description": "Domyślny poziom szczegółowości logów globalnych (debug, info, warning, error)."
+ },
+ "logs": {
+ "label": "Poziom logowania dla poszczególnych procesów",
+ "description": "Zmiana poziomu logowania dla poszczególnych komponentów w celu zwiększenia lub zmniejszenia szczegółowości logów dla określonych modułów."
+ }
+ },
+ "auth": {
+ "label": "Uwierzytelnianie",
+ "description": "Ustawienia związane z uwierzytelnianiem i sesjami, w tym opcje dotyczące plików cookie i ograniczeń częstotliwości.",
+ "enabled": {
+ "label": "Włącz uwierzytelnianie",
+ "description": "Włącz natywne uwierzytelnianie dla interfejsu Frigate."
+ }
+ },
+ "birdseye": {
+ "mode": {
+ "label": "Tryb śledzenia",
+ "description": "Tryb uwzględniania kamer w widoku z lotu ptaka: „obiekty”, „ruch” lub „ciągły”."
+ },
+ "order": {
+ "label": "Pozycja",
+ "description": "Pozycja numeryczna określająca kolejność kamery w układzie widoku z lotu ptaka."
+ },
+ "label": "Widok z lotu ptaka",
+ "description": "Ustawienia widoku z lotu ptaka „Birdseye”, który łączy obraz z wielu kamer w jeden układ.",
+ "enabled": {
+ "label": "Włącz widok z lotu ptaka",
+ "description": "Włącz lub wyłącz funkcję widoku z lotu ptaka."
+ }
+ },
+ "detect": {
+ "enabled": {
+ "label": "Włącz wykrywanie obiektów"
+ },
+ "stationary": {
+ "label": "Konfiguracja obiektów stacjonarnych",
+ "description": "Ustawienia wykrywania i obsługi obiektów statycznych przez określony czas.",
+ "interval": {
+ "label": "Okres bezruchu",
+ "description": "Częstotliwość (w klatkach) sprawdzania detekcji dla obiektów nieruchomych."
+ },
+ "threshold": {
+ "label": "Próg bezruchu",
+ "description": "Liczba klatek bez zmiany pozycji potrzebna do uznania obiektu za nieruchomy."
+ },
+ "max_frames": {
+ "label": "Maksymalna liczba klatek",
+ "description": "Ogranicza czas śledzenia nieruchomych obiektów przed ich usunięciem.",
+ "default": {
+ "label": "Domyślna maks. liczba klatek",
+ "description": "Domyślna maksymalna liczba klatek śledzenia nieruchomego obiektu przed zatrzymaniem."
+ },
+ "objects": {
+ "label": "Maksymalna liczba klatek obiektu",
+ "description": "Nadpisania dla poszczególnych obiektów określające maksymalną liczbę klatek śledzenia nieruchomych obiektów."
+ }
+ },
+ "classifier": {
+ "label": "Włącz klasyfikator wizualny",
+ "description": "Użyj klasyfikatora wizualnego do wykrywania rzeczywiście nieruchomych obiektów, nawet gdy ramki ograniczające drżą."
+ }
+ },
+ "label": "Wykrywanie obiektów",
+ "description": "Ustawienia roli „detection/detect” używanej do wykrywania obiektów i inicjalizacji trackerów.",
+ "height": {
+ "label": "Wysokość wykrywania",
+ "description": "Wysokość (w pikselach) klatek używanych w strumieniu detekcji; pozostaw puste, aby użyć natywnej rozdzielczości strumienia."
+ },
+ "width": {
+ "label": "Szerokość wykrywania",
+ "description": "Szerokość (w pikselach) klatek używanych w strumieniu detekcji; pozostaw puste, aby użyć natywnej rozdzielczości strumienia."
+ },
+ "fps": {
+ "label": "FPS wykrywania",
+ "description": "Docelowa liczba klatek na sekundę używana do detekcji; niższe wartości zmniejszają użycie CPU (zalecana wartość to 5, ustaw wyższą — maksymalnie 10 — tylko jeśli śledzisz bardzo szybko poruszające się obiekty)."
+ },
+ "min_initialized": {
+ "label": "Minimalna liczba klatek inicjalizacyjnych",
+ "description": "Liczba kolejnych trafień detekcji wymagana do utworzenia śledzonego obiektu. Zwiększenie wartości zmniejsza liczbę fałszywych inicjalizacji. Wartość domyślna to fps podzielone przez 2."
+ },
+ "max_disappeared": {
+ "label": "Maksymalna liczba pominiętych klatek",
+ "description": "Liczba klatek bez detekcji, po której obiekt śledzony jest uznawany za utracony."
+ },
+ "annotation_offset": {
+ "label": "Przesunięcie oznaczenia",
+ "description": "Liczba milisekund przesunięcia adnotacji detekcji w celu lepszego dopasowania ramek ograniczających na osi czasu do nagrań; wartość może być dodatnia lub ujemna."
+ }
+ },
+ "face_recognition": {
+ "label": "Rozpoznawanie twarzy",
+ "enabled": {
+ "label": "Włącz rozpoznawanie twarzy"
+ },
+ "min_area": {
+ "label": "Minimalny obszar twarzy",
+ "description": "Minimalny obszar twarzy (w pikselach) wymagany do podjęcia próby rozpoznania."
+ }
+ },
+ "ffmpeg": {
+ "output_args": {
+ "detect": {
+ "description": "Domyślne argumenty wyjściowe dla strumieni z rolą detekcji."
+ },
+ "record": {
+ "description": "Domyślne argumenty wyjściowe dla strumieni z rolą zapisu."
+ }
+ },
+ "retry_interval": {
+ "label": "Czas ponownej próby FFmpeg",
+ "description": "Czas (w sekundach) przed ponownym uruchomieniem utraconego strumienia kamery. Domyślnie: 10."
+ },
+ "apple_compatibility": {
+ "label": "Kompatybilność z Apple",
+ "description": "Włącz tagowanie HEVC, aby zapewnić lepszą zgodność z odtwarzaczami Apple podczas nagrywania w formacie H.265."
+ },
+ "gpu": {
+ "label": "Indeks GPU",
+ "description": "Domyślny indeks karty graficznej używany do akceleracji sprzętowej, jeśli jest dostępna."
+ }
}
}
diff --git a/web/public/locales/pl/config/groups.json b/web/public/locales/pl/config/groups.json
index 0967ef424b..1d6d94b34e 100644
--- a/web/public/locales/pl/config/groups.json
+++ b/web/public/locales/pl/config/groups.json
@@ -1 +1,73 @@
-{}
+{
+ "audio": {
+ "global": {
+ "detection": "Ogólne Wykrywanie",
+ "sensitivity": "Ogólna Czułość"
+ },
+ "cameras": {
+ "detection": "Wykrywanie",
+ "sensitivity": "Czułość"
+ }
+ },
+ "timestamp_style": {
+ "global": {
+ "appearance": "Ogólny Wygląd"
+ },
+ "cameras": {
+ "appearance": "Wygląd"
+ }
+ },
+ "motion": {
+ "global": {
+ "sensitivity": "Ogólna Czułość",
+ "algorithm": "Ogólny Algorytm"
+ },
+ "cameras": {
+ "sensitivity": "Czułość",
+ "algorithm": "Algorytm"
+ }
+ },
+ "snapshots": {
+ "global": {
+ "display": "Ogólne Ustawienia Wyświetlania"
+ },
+ "cameras": {
+ "display": "Wyświetlanie"
+ }
+ },
+ "detect": {
+ "global": {
+ "resolution": "Ogólna Rozdzielczość",
+ "tracking": "Ogólne Śledzenie"
+ },
+ "cameras": {
+ "resolution": "Rozdzielczość",
+ "tracking": "Śledzenie"
+ }
+ },
+ "objects": {
+ "global": {
+ "tracking": "Ogólne Śledzenie",
+ "filtering": "Ogólne Filtrowanie"
+ },
+ "cameras": {
+ "tracking": "Śledzenie",
+ "filtering": "Filtrowanie"
+ }
+ },
+ "record": {
+ "global": {
+ "retention": "Ogólne Przechowywanie",
+ "events": "Ogólne Zdarzenia"
+ },
+ "cameras": {
+ "retention": "Przechowywanie",
+ "events": "Zdarzenia"
+ }
+ },
+ "ffmpeg": {
+ "cameras": {
+ "cameraFfmpeg": "Argumenty FFmpeg dotyczące konkretnej kamery"
+ }
+ }
+}
diff --git a/web/public/locales/pl/config/validation.json b/web/public/locales/pl/config/validation.json
index 0967ef424b..cded1fbb3d 100644
--- a/web/public/locales/pl/config/validation.json
+++ b/web/public/locales/pl/config/validation.json
@@ -1 +1,18 @@
-{}
+{
+ "minimum": "Musi wynosić przynajmniej {{limit}}",
+ "maximum": "Może wynosić najwyżej {{limit}}",
+ "exclusiveMinimum": "Musi być większe niż {{limit}}",
+ "exclusiveMaximum": "Musi być mniejsze niż {{limit}}",
+ "minLength": "Musi być co najmniej {{limit}} znaków",
+ "maxLength": "Maksymalnie może być {{limit}} znaków",
+ "minItems": "Musi zawierać co najmniej {{limit}} pozycji",
+ "maxItems": "Maksymalnie {{limit}} pozycji",
+ "pattern": "Nieprawidłowy format",
+ "required": "To pole jest obowiązkowe",
+ "type": "Nieprawidłowy typ wartości",
+ "enum": "Musi to być jedna z dozwolonych wartości",
+ "const": "Wartość niezgodna z oczekiwaną stałą",
+ "uniqueItems": "Wszystkie pozycje muszą być unikalne",
+ "format": "Nieprawidłowy format",
+ "additionalProperties": "Nieznana właściwość jest niedozwolona"
+}
diff --git a/web/public/locales/pl/views/chat.json b/web/public/locales/pl/views/chat.json
new file mode 100644
index 0000000000..f16ccc2c32
--- /dev/null
+++ b/web/public/locales/pl/views/chat.json
@@ -0,0 +1,4 @@
+{
+ "documentTitle": "Czat - Frigate",
+ "title": "Frigate Czat"
+}
diff --git a/web/public/locales/pl/views/classificationModel.json b/web/public/locales/pl/views/classificationModel.json
index bb29f4598a..b5a048882e 100644
--- a/web/public/locales/pl/views/classificationModel.json
+++ b/web/public/locales/pl/views/classificationModel.json
@@ -17,12 +17,12 @@
},
"toast": {
"success": {
- "deletedCategory_one": "Usunięte klasy",
- "deletedCategory_few": "",
- "deletedCategory_many": "",
- "deletedImage_one": "Usunięte obrazy",
- "deletedImage_few": "",
- "deletedImage_many": "",
+ "deletedCategory_one": "Usunięto {{count}} klasę",
+ "deletedCategory_few": "Usunięto {{count}} klasy",
+ "deletedCategory_many": "Usunięto {{count}} klas",
+ "deletedImage_one": "Usunięto {{count}} obraz",
+ "deletedImage_few": "Usunięto {{count}} obrazy",
+ "deletedImage_many": "Usunięto {{count}} obrazów",
"deletedModel_one": "Pomyślenie usunięto {{count}} model",
"deletedModel_few": "Pomyślenie usunięto {{count}} modele",
"deletedModel_many": "Pomyślenie usunięto {{count}} modeli",
@@ -30,7 +30,8 @@
"trainedModel": "Model pomyślnie wytrenowany.",
"trainingModel": "Pomyślnie uruchomiono trenowanie modelu.",
"updatedModel": "Pomyślnie zaktualizowane ustawienia modelu",
- "renamedCategory": "Pomyślnie zmieniono nazwę klasy na {{name}}"
+ "renamedCategory": "Pomyślnie zmieniono nazwę klasy na {{name}}",
+ "reclassifiedImage": "Pomyślne przeklasyfikowanie obrazu"
},
"error": {
"deleteImageFailed": "Nie udało się usunąć: {{errorMessage}}",
@@ -40,7 +41,8 @@
"trainingFailed": "Trening modelu zakończył się niepowodzeniem. Sprawdź logi Frigate aby uzyskać więcej informacji.",
"updateModelFailed": "Nie udało się zaktualizować modelu: {{errorMessage}}",
"trainingFailedToStart": "Nie udało się rozpocząć trenowania modelu: {{errorMessage}}",
- "renameCategoryFailed": "Nie udało się zmienić nazwy klasy: {{errorMessage}}"
+ "renameCategoryFailed": "Nie udało się zmienić nazwy klasy: {{errorMessage}}",
+ "reclassifyFailed": "Niepowodzenie zmiany klasyfikacji obrazu: {{errorMessage}}"
}
},
"deleteCategory": {
@@ -191,7 +193,14 @@
"missingStatesWarning": {
"title": "Przykłady brakujących stanów",
"description": "Aby uzyskać najlepsze wyniki, zaleca się wybranie przykładów dla wszystkich stanów. Można kontynuować bez wybierania wszystkich stanów, ale model nie zostanie wytrenowany, dopóki wszystkie stany nie będą miały obrazów. Po kontynuowaniu należy użyć widoku Ostatnie klasyfikacje, aby sklasyfikować obrazy dla brakujących stanów, a następnie wytrenować model."
+ },
+ "refreshExamples": "Generuj nowe przykłady",
+ "refreshConfirm": {
+ "title": "Czy generować nowe przykłady?",
+ "description": "Spowoduje to wygenerowanie nowego zestawu obrazów i wyczyszczenie wszystkich zaznaczeń, w tym dotychczasowych klas. Konieczne będzie ponowne wybranie przykładów dla wszystkich klas."
}
}
- }
+ },
+ "reclassifyImageAs": "Klasyfikuj jako:",
+ "reclassifyImage": "Zmiana klasyfikacji obrazu"
}
diff --git a/web/public/locales/pl/views/events.json b/web/public/locales/pl/views/events.json
index 0ffc5419f3..1e85b72650 100644
--- a/web/public/locales/pl/views/events.json
+++ b/web/public/locales/pl/views/events.json
@@ -16,7 +16,9 @@
"description": "Elementy przeglądu można tworzyć dla kamery tylko wtedy, gdy dla tej kamery włączono nagrywanie."
}
},
- "timeline": "Oś czasu",
+ "timeline": {
+ "label": "Oś czasu"
+ },
"timeline.aria": "Wybierz oś czasu",
"events": {
"label": "Zdarzenia",
diff --git a/web/public/locales/pl/views/explore.json b/web/public/locales/pl/views/explore.json
index d18d065b85..3dfe9f509e 100644
--- a/web/public/locales/pl/views/explore.json
+++ b/web/public/locales/pl/views/explore.json
@@ -85,7 +85,8 @@
"attributes": "Atrybuty klasyfikacji",
"title": {
"label": "Tytuł"
- }
+ },
+ "scoreInfo": "Informacje o wyniku"
},
"objectLifecycle": {
"annotationSettings": {
@@ -222,6 +223,13 @@
},
"hideObjectDetails": {
"label": "Ukryj ścieżkę obiektu"
+ },
+ "debugReplay": {
+ "label": "Odtwarzanie debugowania",
+ "aria": "Wyświetl śledzony obiekt w widoku odtwarzania debugowania"
+ },
+ "more": {
+ "aria": "Więcej"
}
},
"trackedObjectsCount_one": "{{count}} śledzony obiekt ",
@@ -232,6 +240,9 @@
"confirmDelete": {
"desc": "Usunięcie tego śledzonego obiektu usuwa zrzut ekranu, wszelkie zapisane osadzenia i wszystkie powiązane wpisy śledzenia obiektu. Nagrany materiał tego śledzonego obiektu w widoku Historii NIE zostanie usunięty. Czy na pewno chcesz kontynuować?",
"title": "Potwierdź usunięcie"
+ },
+ "toast": {
+ "error": "Błąd kasowania śledzonego obiektu: {{errorMessage}}"
}
},
"fetchingTrackedObjectsFailed": "Błąd pobierania śledzonych obiektów: {{errorMessage}}",
@@ -276,7 +287,10 @@
"zones": "Strefy",
"area": "Powierzchnia",
"score": "Wynik",
- "ratio": "Proporcje"
+ "ratio": "Proporcje",
+ "toggleAdvancedScores": "Przełącz widok wyników zaawansowanych",
+ "computedScore": "Obliczony wynik",
+ "topScore": "Najwyższy wynik"
},
"heard": "{{label}} słyszałem"
},
diff --git a/web/public/locales/pl/views/exports.json b/web/public/locales/pl/views/exports.json
index b0d41bbc38..82e6857fc1 100644
--- a/web/public/locales/pl/views/exports.json
+++ b/web/public/locales/pl/views/exports.json
@@ -2,7 +2,9 @@
"search": "Szukaj",
"documentTitle": "Eksportuj - Frigate",
"noExports": "Nie znaleziono eksportów",
- "deleteExport": "Usuń eksport",
+ "deleteExport": {
+ "label": "Usuń eksport"
+ },
"deleteExport.desc": "Czy na pewno chcesz usunąć {{exportName}}?",
"editExport": {
"title": "Zmień nazwę eksportu",
@@ -18,6 +20,11 @@
"shareExport": "Udostępnij eksport",
"downloadVideo": "Pobierz wideo",
"editName": "Edytuj nazwę",
- "deleteExport": "Usuń eksport"
+ "deleteExport": "Usuń eksport",
+ "assignToCase": "Dodaj do sprawy"
+ },
+ "headings": {
+ "cases": "Przypadki",
+ "uncategorizedExports": "Bez kategorii Eksport"
}
}
diff --git a/web/public/locales/pl/views/live.json b/web/public/locales/pl/views/live.json
index b4ab24def8..77f1a4229e 100644
--- a/web/public/locales/pl/views/live.json
+++ b/web/public/locales/pl/views/live.json
@@ -1,5 +1,7 @@
{
- "documentTitle": "Na żywo - Frigate",
+ "documentTitle": {
+ "default": "Transmisja - Frigate"
+ },
"documentTitle.withCamera": "{{camera}}- Na żywo - Frigate",
"lowBandwidthMode": "Tryb niskiej przepustowości",
"twoWayTalk": {
@@ -15,7 +17,8 @@
"clickMove": {
"label": "Kliknij w ramce, aby wyśrodkować kamerę",
"enable": "Włącz kliknięcie do przesuwania",
- "disable": "Wyłącz kliknięcie do przesuwania"
+ "disable": "Wyłącz kliknięcie do przesuwania",
+ "enableWithZoom": "Włącz kliknij, aby przesunąć / przeciągnij, aby przybliżyć"
},
"left": {
"label": "Przesuń kamerę PTZ w lewo"
diff --git a/web/public/locales/pl/views/motionSearch.json b/web/public/locales/pl/views/motionSearch.json
new file mode 100644
index 0000000000..6406859bb3
--- /dev/null
+++ b/web/public/locales/pl/views/motionSearch.json
@@ -0,0 +1,58 @@
+{
+ "documentTitle": "Wyszukiwanie zdarzeń ruchu - Frigate",
+ "title": "Wyszukiwanie zdarzeń ruchu",
+ "description": "Narysuj wielokąt aby wyznaczyć obszar zainteresowania, a następnie określ przedział czasowy w którym chcesz wyszukać zmiany ruchu w tym obszarze.",
+ "selectCamera": "Ładowanie wyszukiwania zdarzeń ruchu",
+ "startSearch": "Start wyszukiwania",
+ "searchStarted": "Uruchomiono wyszukiwanie",
+ "searchCancelled": "Zatrzymano wyszukiwanie",
+ "cancelSearch": "Anuluj",
+ "searching": "Wyszukiwanie w trakcie.",
+ "searchComplete": "Wyszukiwanie zakończono",
+ "noResultsYet": "Przeprowadź wyszukiwanie aby znaleźć zmiany ruchu w zaznaczonym obszarze",
+ "noChangesFound": "W wybranym obszarze nie wykryto żadnych zmian w pikselach",
+ "framesProcessed": "{{count}} przetworzonych klatek obrazu",
+ "jumpToTime": "Przejdź do tego momentu",
+ "results": "Wyniki",
+ "showSegmentHeatmap": "Mapa cieplna",
+ "newSearch": "Nowe wyszukiwanie",
+ "clearResults": "Wyczyść wyniki",
+ "clearROI": "Wyczyść strefę",
+ "polygonControls": {
+ "undo": "Cofnij ostatnią zmianę",
+ "reset": "Reset strefy"
+ },
+ "motionHeatmapLabel": "Mapa aktywności ruchu",
+ "dialog": {
+ "title": "Wyszukiwanie według ruchu",
+ "cameraLabel": "Kamera",
+ "previewAlt": "Podgląd z kamery {{camera}}"
+ },
+ "timeRange": {
+ "title": "Zakres wyszukiwania",
+ "start": "Czas rozpoczęcia",
+ "end": "Czas zakończenia"
+ },
+ "settings": {
+ "title": "Ustawienia wyszukiwania",
+ "parallelMode": "Tryb równoległy",
+ "parallelModeDesc": "Przeskanuj wiele fragmentów nagrania jednocześnie (szybsze, ale znacznie bardziej obciążające procesor)",
+ "threshold": "Próg czułości",
+ "thresholdDesc": "Niższe wartości pozwalają wykrywać mniejsze zmiany (1–255)",
+ "minArea": "Obszar minimalnej zmiany",
+ "minAreaDesc": "Minimalny procent obszaru zainteresowania który musi ulec zmianie aby uznano to za istotne",
+ "frameSkip": "Pominięcie klatki",
+ "frameSkipDesc": "Przetwarza co N klatkę. Ustaw wartość na liczbę klatek na sekundę swojej kamery, aby przetwarzać jedną klatkę na sekundę (np. 5 dla kamery o 5 klatkach na sekundę, 30 dla kamery o 30 klatkach na sekundę). Wyższe wartości zapewniają większą szybkość ale mogą powodować pominięcie krótkich zdarzeń ruchu.",
+ "maxResults": "Maksymalne wyniki",
+ "maxResultsDesc": "Zatrzymaj się po osiągnięciu określonej liczby pasujących znaczników czasu"
+ },
+ "errors": {
+ "noCamera": "Wybierz kamerę",
+ "noROI": "Zaznacz obszar zainteresowania",
+ "noTimeRange": "Ustaw przedział czasowy",
+ "invalidTimeRange": "Czas zakończenia musi być późniejszy niż czas rozpoczęcia",
+ "searchFailed": "Wyszukiwanie nie powiodło się: {{message}}",
+ "polygonTooSmall": "Obszar musi mieć co najmniej 3 punkty",
+ "unknown": "Nieznany błąd"
+ }
+}
diff --git a/web/public/locales/pl/views/replay.json b/web/public/locales/pl/views/replay.json
new file mode 100644
index 0000000000..bfc3c4fe3a
--- /dev/null
+++ b/web/public/locales/pl/views/replay.json
@@ -0,0 +1,6 @@
+{
+ "page": {
+ "preparingClip": "Przygotuje urywek…"
+ },
+ "title": "Debugowanie nagrań"
+}
diff --git a/web/public/locales/pl/views/settings.json b/web/public/locales/pl/views/settings.json
index f7440b0468..6167a9d0e9 100644
--- a/web/public/locales/pl/views/settings.json
+++ b/web/public/locales/pl/views/settings.json
@@ -7,13 +7,68 @@
"cameras": "Ustawienia Kamery",
"frigateplus": "Frigate+",
"masksAndZones": "Maski / Strefy",
- "motionTuner": "Konfigurator Ruchu",
+ "motionTuner": "Konfigurator ruchu",
"debug": "Debugowanie",
"enrichments": "Wzbogacenia",
"triggers": "Wyzwalacze",
"roles": "Role",
"cameraManagement": "Zarządzanie",
- "cameraReview": "Przegląd"
+ "cameraReview": "Przegląd",
+ "integrations": "Integracje",
+ "uiSettings": "Ustawienia interfejsu użytkownika",
+ "profiles": "Profile",
+ "globalDetect": "Detekcja obiektów",
+ "globalRecording": "Nagrywanie",
+ "globalSnapshots": "Snapshoty",
+ "globalFfmpeg": "FFmpeg",
+ "globalMotion": "Detekcja ruchu",
+ "globalObjects": "Obiekty",
+ "globalReview": "Recenzja",
+ "globalAudioEvents": "Detekcja dźwięku",
+ "globalLivePlayback": "Podgląd na żywo",
+ "globalTimestampStyle": "Styl znacznika czasu",
+ "systemDatabase": "Baza danych",
+ "systemTls": "TLS",
+ "systemAuthentication": "Autentykacja",
+ "systemNetworking": "Sieć",
+ "systemProxy": "Proxy",
+ "systemUi": "UI",
+ "systemLogging": "Logowanie",
+ "systemEnvironmentVariables": "Zmienne środowiskowe",
+ "systemTelemetry": "Telemetria",
+ "systemBirdseye": "Podgląd obrazu",
+ "systemFfmpeg": "FFmpeg",
+ "systemDetectorsAndModel": "Detektory i model",
+ "systemMqtt": "MQTT",
+ "systemGo2rtcStreams": "strumienie go2rtc",
+ "integrationSemanticSearch": "Wyszukiwanie semantyczne",
+ "integrationGenerativeAi": "Generatywna sztuczna inteligencja",
+ "integrationFaceRecognition": "Rozpoznawanie twarzy",
+ "integrationLpr": "Rozpoznawanie tablic rejestracyjnych",
+ "integrationObjectClassification": "Klasyfikacja obiektów",
+ "integrationAudioTranscription": "Transkrypcja dźwięku",
+ "cameraDetect": "Detekcja obiektów",
+ "cameraFfmpeg": "FFmpeg",
+ "cameraRecording": "Nagrywanie",
+ "cameraBirdseye": "Podgląd obrazu",
+ "cameraFaceRecognition": "Rozpoznawanie twarzy",
+ "cameraLpr": "Rozpoznawanie tablic rejestracyjnych",
+ "cameraMqttConfig": "MQTT",
+ "cameraOnvif": "ONVIF",
+ "cameraAudioTranscription": "Transkrypcja dźwięku",
+ "cameraNotifications": "Powiadomienia",
+ "cameraLivePlayback": "Podgląd na żywo",
+ "cameraSnapshots": "Snapshoty",
+ "cameraMotion": "Detekcja ruchu",
+ "cameraObjects": "Obiekty",
+ "cameraConfigReview": "Recenzja",
+ "cameraAudioEvents": "Detekcja dźwięku",
+ "cameraUi": "Interfejs użytkownika kamery",
+ "cameraTimestampStyle": "Styl znacznika czasu",
+ "cameraMqtt": "MQTT kamery",
+ "maintenance": "Utrzymanie",
+ "mediaSync": "Synchronizacja mediów",
+ "regionGrid": "Siatka regionalna"
},
"dialog": {
"unsavedChanges": {
@@ -96,7 +151,12 @@
"notifications": "Ustawienia powiadomień - Frigate",
"enrichments": "Ustawienia wzbogacania - Frigate",
"cameraManagement": "Zarządzanie kamerami – Frigate",
- "cameraReview": "Ustawienia przeglądu kamer - Frigate"
+ "cameraReview": "Ustawienia przeglądu kamer - Frigate",
+ "globalConfig": "Konfiguracja globalna - Frigate",
+ "cameraConfig": "Konfiguracja kamery - Frigate",
+ "maintenance": "Konserwacja – Frigate",
+ "profiles": "Profile - Frigate",
+ "detectorsAndModel": "Detektory i model"
},
"classification": {
"title": "Ustawienia Klasyfikacji",
@@ -410,7 +470,7 @@
},
"restart_required": "Wymagane ponowne uruchomienie (maski/strefy zmienione)",
"motionMaskLabel": "Maska Ruchu {{number}}",
- "objectMaskLabel": "Maska Obiektu {{number}} ({{label}})"
+ "objectMaskLabel": "Maska Obiektu {{number}}"
},
"debug": {
"objectList": "Lista Obiektów",
@@ -684,7 +744,7 @@
"cleanCopySnapshots": "Zrzuty ekranu clean_copy",
"camera": "Kamera"
},
- "cleanCopyWarning": "Niektóre kamery mają włączone zrzuty ekranu, ale mają wyłączoną funkcję czystej kopii. Musisz włączyć clean_copy w konfiguracji zrzutów ekranu, aby móc przesyłać obrazy z tych kamer do Frigate+."
+ "cleanCopyWarning": "Niektóre kamery mają wyłączone migawki"
},
"modelInfo": {
"title": "Informacje o modelu",
@@ -1227,5 +1287,47 @@
"title": "Opisy generatywnej sztucznej inteligencji",
"desc": "Tymczasowo włącz/wyłącz generatywne opisy AI dla tej kamery. Po wyłączeniu opisy generowane przez AI nie będą wymagane dla elementów przeglądu w tej kamerze."
}
+ },
+ "button": {
+ "overriddenGlobal": "Nadpisane (globalnie)",
+ "overriddenGlobalTooltip": "Ta kamera nadpisuje globalną konfigurację w tej sekcji",
+ "overriddenGlobalHeading_one": "Ta kamera nadpisuje pole {{count}} z globalnej konfiguracji:",
+ "overriddenGlobalHeading_few": "Ta kamera nadpisuje pola {{count}} z globalnej konfiguracji:",
+ "overriddenGlobalHeading_many": "Ta kamera nadpisuje pola {{count}} z globalnej konfiguracji:",
+ "overriddenGlobalNoDeltas": "Ta kamera nadpisuje ustawienia globalne, ale żadne wartości pól się nie różnią.",
+ "overriddenBaseConfig": "Nadpisane (bazowa konfiguracja)",
+ "overriddenBaseConfigTooltip": "Profil {{profile}} zastępuje ustawienia konfiguracyjne w tej sekcji",
+ "overriddenBaseConfigHeading_one": "Profil {{profile}} zastępuje pole {{count}} z konfiguracji podstawowej:",
+ "overriddenBaseConfigHeading_few": "Profile {{profile}} zastępują pola {{count}} z konfiguracji podstawowej:",
+ "overriddenBaseConfigHeading_many": "Profile {{profile}} zastępują pola {{count}} z konfiguracji podstawowej:",
+ "overriddenBaseConfigNoDeltas": "Profil {{profile}} zastępuje tę sekcję, ale żadne wartości pól nie różnią się od konfiguracji podstawowej.",
+ "overriddenInCameras": {
+ "label_one": "Zastąpiono w kamerze {{count}}",
+ "label_few": "Zastąpiono w kamerach {{count}}",
+ "label_many": "Zastąpiono w kamerach {{count}}",
+ "tooltip_one": "Kamera {{count}} zastępuje wartości w tej sekcji. Kliknij, aby wyświetlić szczegóły.",
+ "tooltip_few": "Kamery {{count}} zastępują wartości w tej sekcji. Kliknij, aby wyświetlić szczegóły.",
+ "tooltip_many": "Kamery {{count}} zastępują wartości w tej sekcji. Kliknij, aby wyświetlić szczegóły."
+ }
+ },
+ "saveAllPreview": {
+ "scope": {
+ "label": "Zakres",
+ "global": "Globalne",
+ "camera": "Kamera: {{cameraName}}"
+ },
+ "profile": {
+ "label": "Profil"
+ },
+ "field": {
+ "label": "Pole"
+ },
+ "value": {
+ "label": "Nowa wartość",
+ "reset": "Reset"
+ },
+ "title": "Zmiany do zapisania",
+ "triggerLabel": "Przejrzyj oczekujące zmiany",
+ "empty": "Brak oczekujących zmian."
}
}
diff --git a/web/public/locales/pl/views/system.json b/web/public/locales/pl/views/system.json
index ebcc114633..cc5851067f 100644
--- a/web/public/locales/pl/views/system.json
+++ b/web/public/locales/pl/views/system.json
@@ -7,7 +7,8 @@
"logs": {
"frigate": "Logi Frigate - Frigate",
"go2rtc": "Logi Go2RTC - Frigate",
- "nginx": "Logi Nginx - Frigate"
+ "nginx": "Logi Nginx - Frigate",
+ "websocket": "Logi Websocket - Frigate"
}
},
"general": {
@@ -163,6 +164,16 @@
"fetchingLogsFailed": "Błąd pobierania logów: {{errorMessage}}",
"whileStreamingLogs": "Błąd podczas strumieniowania logów: {{errorMessage}}"
}
+ },
+ "websocket": {
+ "label": "Wiadomości",
+ "pause": "Pauza",
+ "resume": "Wznów",
+ "clear": "Wyczyść",
+ "filter": {
+ "all": "Wszystkie tematy",
+ "topics": "Tematy"
+ }
}
},
"title": "System",
diff --git a/web/public/locales/pt-BR/audio.json b/web/public/locales/pt-BR/audio.json
index b36f099021..1444d4d6a4 100644
--- a/web/public/locales/pt-BR/audio.json
+++ b/web/public/locales/pt-BR/audio.json
@@ -429,5 +429,74 @@
"noise": "Ruído",
"distortion": "Distorção",
"cacophony": "Cacofonia",
- "vibration": "Vibração"
+ "vibration": "Vibração",
+ "change_ringing": "Mudar Toque",
+ "shofar": "Berrante",
+ "liquid": "Líquido",
+ "splash": "Respingar",
+ "slosh": "Respingo",
+ "squish": "Esmagar",
+ "drip": "Pingar",
+ "pour": "Derramar",
+ "trickle": "Gotejar",
+ "gush": "Jorrar",
+ "fill": "Preencher",
+ "spray": "Borrifar",
+ "pump": "Bombear",
+ "stir": "Mexer",
+ "boiling": "Fervendo",
+ "sonar": "Sonar",
+ "arrow": "Flecha",
+ "whoosh": "Uau",
+ "thump": "Baque",
+ "thunk": "Tombo",
+ "electronic_tuner": "Afinador Eletrônico",
+ "effects_unit": "Unidade de Efeitos",
+ "chorus_effect": "Efeito Coro",
+ "basketball_bounce": "Quique da bola",
+ "bang": "Batida",
+ "slap": "Tapa",
+ "whack": "Bater",
+ "smash": "Esmagar",
+ "breaking": "Quebrando",
+ "bouncing": "Quicando",
+ "whip": "Chicote",
+ "flap": "Aba",
+ "scratch": "Arranhão",
+ "scrape": "Raspagem",
+ "rub": "Esfregar",
+ "roll": "Rolar",
+ "crushing": "Esmagamento",
+ "crumpling": "Amarrotar",
+ "tearing": "Rasgando",
+ "beep": "Bip",
+ "ping": "Pingo",
+ "ding": "Campainha",
+ "clang": "Estridente",
+ "squeal": "Guincho",
+ "creak": "Ranger",
+ "rustle": "Farfalhar",
+ "whir": "Zumbir",
+ "clatter": "Barulho",
+ "sizzle": "Chiado",
+ "clicking": "Clicando",
+ "clickety_clack": "Tique-taque",
+ "rumble": "Estrondo",
+ "plop": "Ploft",
+ "hum": "Zumbir",
+ "zing": "Zangando",
+ "boing": "Poin",
+ "crunch": "Mastigar",
+ "sine_wave": "Onda Senoidal",
+ "harmonic": "Harmonica",
+ "chirp_tone": "Som Agudo",
+ "pulse": "Pulso",
+ "inside": "Dentro",
+ "outside": "Fora",
+ "reverberation": "Reverberação",
+ "echo": "Eco",
+ "mains_hum": "Zumbido Elétrico",
+ "sidetone": "Retorno de Voz",
+ "throbbing": "Latejante",
+ "sodeling": "Soldagem"
}
diff --git a/web/public/locales/pt-BR/common.json b/web/public/locales/pt-BR/common.json
index 632155ddd1..3ce687cc73 100644
--- a/web/public/locales/pt-BR/common.json
+++ b/web/public/locales/pt-BR/common.json
@@ -158,7 +158,9 @@
"resetToDefault": "Redefinir para o Padrão",
"saveAll": "Salvar Tudo",
"savingAll": "Salvando Tudo…",
- "undoAll": "Desfazer Tudo"
+ "undoAll": "Desfazer Tudo",
+ "applying": "Aplicando…",
+ "retry": "Tente novamente"
},
"menu": {
"system": "Sistema",
@@ -207,7 +209,9 @@
"gl": "Galego (Galego)",
"id": "Bahasa Indonesia (Indonésio)",
"ur": "اردو (Urdu)",
- "hr": "Hrvatski (Croata)"
+ "hr": "Hrvatski (Croata)",
+ "bs": "Bosanski (Bósnio)",
+ "zhHant": "Chinês Tradicional"
},
"systemLogs": "Logs de sistema",
"settings": "Configurações",
@@ -262,7 +266,10 @@
"setPassword": "Definir Senha"
},
"classification": "Classificação",
- "chat": "Chat"
+ "chat": "Chat",
+ "profiles": "Perfis",
+ "actions": "Ações",
+ "features": "Funcionalidades"
},
"toast": {
"copyUrlToClipboard": "URL copiada para a área de transferência.",
@@ -271,7 +278,8 @@
"error": {
"title": "Falha ao salvar as alterações de configuração: {{errorMessage}}",
"noMessage": "Falha ao salvar as alterações de configuração"
- }
+ },
+ "success": "Alterações salvas com sucesso."
}
},
"role": {
@@ -314,5 +322,10 @@
"field": {
"optional": "Opcional",
"internalID": "O ID interno que o Frigate usa na configuração e banco de dados"
+ },
+ "no_items": "Sem itens",
+ "validation_errors": "Erros de validação",
+ "credentialField": {
+ "savedPlaceholder": "Salvo - deixar em branco para manter a atual"
}
}
diff --git a/web/public/locales/pt-BR/components/camera.json b/web/public/locales/pt-BR/components/camera.json
index 03ee52b587..a746960980 100644
--- a/web/public/locales/pt-BR/components/camera.json
+++ b/web/public/locales/pt-BR/components/camera.json
@@ -68,7 +68,10 @@
}
},
"birdseye": "Visão Panorâmica"
- }
+ },
+ "showAll": "Exibir todos os grupos de câmeras",
+ "showLess": "Mostrar menos",
+ "editGroups": "Editar grupos de câmeras"
},
"debug": {
"options": {
@@ -82,6 +85,7 @@
"motion": "Movimento",
"regions": "Regiões",
"boundingBox": "Caixa Delimitadora",
- "timestamp": "Timestamp"
+ "timestamp": "Timestamp",
+ "paths": "Caminhos"
}
}
diff --git a/web/public/locales/pt-BR/components/dialog.json b/web/public/locales/pt-BR/components/dialog.json
index 5ce4c631bd..7f877adadb 100644
--- a/web/public/locales/pt-BR/components/dialog.json
+++ b/web/public/locales/pt-BR/components/dialog.json
@@ -24,6 +24,9 @@
},
"state": {
"submitted": "Enviado"
+ },
+ "toast": {
+ "error": "Falha ao enviar para o Frigate+. Verifique sua conexão de rede e tente novamente."
}
}
},
@@ -56,19 +59,81 @@
"toast": {
"success": "Exportação iniciada com sucesso. Veja o arquivo na tela exportar.",
"error": {
- "failed": "Falha em iniciar exportação: {{error}}",
+ "failed": "Falha ao iniciar exportação: {{error}}",
"endTimeMustAfterStartTime": "Tempo de finalização deve ser após tempo de início",
"noVaildTimeSelected": "Nenhuma faixa de tempo válida selecionada"
},
- "view": "Ver"
+ "view": "Ver",
+ "batchQueuedSuccess_one": "1 exportação na fila. Abrindo o caso agora.",
+ "batchQueuedSuccess_many": "{{count}} exportações colocadas na fila. Abrindo o caso agora.",
+ "batchQueuedSuccess_other": "",
+ "batchQueuedPartial": "{{successful}} de {{total}} exportações na fila. Câmeras com falha: {{failedCameras}}",
+ "batchQueueFailed": "Falha ao enfileirar {{total}} exportações. Câmeras com falha: {{failedCameras}}",
+ "batchPartial": "Iniciadas {{successful}} de {{total}} exportações. Câmeras com falha: {{failedCameras}}",
+ "batchFailed": "Falha ao iniciar {{total}} exportações. Câmeras com falha: {{failedCameras}}",
+ "queued": "Exportação na fila. Acompanhe o progresso na página de exportações."
},
"fromTimeline": {
"saveExport": "Salvar Exportação",
- "previewExport": "Pré-Visualizar Exportação"
+ "previewExport": "Pré-Visualizar Exportação",
+ "queueingExport": "Enfileirando exportação...",
+ "useThisRange": "Use esta faixa"
},
"case": {
"label": "Caso",
- "placeholder": "Selecione um caso"
+ "placeholder": "Selecione um caso",
+ "newCaseOption": "Criar novo caso de uso",
+ "newCaseNamePlaceholder": "Novo caso de uso",
+ "newCaseDescriptionPlaceholder": "Descrição do caso de uso",
+ "nonAdminHelp": "Um novo caso de uso será criado para estas exportações."
+ },
+ "queueing": "Exportação na fila...",
+ "tabs": {
+ "export": "Câmera única",
+ "multiCamera": "Multi-Câmera"
+ },
+ "multiCamera": {
+ "timeRange": "Intervalo de tempo",
+ "selectFromTimeline": "Selecione do intervale de tempo",
+ "cameraSelection": "Câmeras",
+ "cameraSelectionHelp": "Câmeras com objetos localizados neste intervalo de tempo estão pré-selecionados",
+ "checkingActivity": "Verificando se a câmera está ativa...",
+ "noCameras": "Sem câmeras disponíveis",
+ "detectionCount_one": "Objeto localizado",
+ "detectionCount_many": "{{count}} objetos localizados",
+ "detectionCount_other": "{{count}} objetos localizados",
+ "nameLabel": "Nome do arquivo exportado",
+ "namePlaceholder": "Padrão de nome para exportação",
+ "queueingButton": "Exportações na fila...",
+ "exportButton_one": "Exportar câmera",
+ "exportButton_many": "Exportar {{count}} câmeras",
+ "exportButton_other": "Exportar {{count}} câmeras",
+ "searchOrSelectGroup": "Procurar, ou selecionar um grupo de câmeras...",
+ "selectAll": "Selecionar todas as câmeras",
+ "clearSelection": "Limpar seleção",
+ "selectWithActivity": "Câmeras com rastreamento de objetos",
+ "selectGroup": "Selecione grupo",
+ "noMatchingCameras": "Nenhuma câmera corresponde à sua pesquisa",
+ "selectedCount": "{{selected}} / {{total}} selecionados"
+ },
+ "multi": {
+ "title_one": "Exportar análise",
+ "title_many": "Exportar {{count}} análises",
+ "title_other": "Exportar {{count}} análises",
+ "description": "Exportar cada análise selecionada. Todas as exportações serão agrupadas em um único caso.",
+ "descriptionNoCase": "Exportar cada análise selecionada.",
+ "caseNamePlaceholder": "Exportar análise - {{date}}",
+ "exportButton_one": "Exportar análise",
+ "exportButton_many": "Exportar {{count}} análises",
+ "exportButton_other": "Exportar {{count}} análises",
+ "exportingButton": "Exportando...",
+ "toast": {
+ "started_one": "Uma exportação iniciada. Abrindo o caso agora.",
+ "started_many": "Iniciadas {{count}} exportações. Abrindo o caso agora.",
+ "started_other": "",
+ "failed": "Falha ao iniciar {{total}} exportações. Falhas: {{failedItems}}",
+ "partial": "Iniciadas {{successful}} de {{total}} exportações. Falhas: {{failedItems}}"
+ }
}
},
"streaming": {
@@ -116,6 +181,14 @@
"export": "Exportar",
"deleteNow": "Deletar Agora",
"markAsUnreviewed": "Marcar como não revisado"
+ },
+ "shareTimestamp": {
+ "label": "Compartilhar marca de tempo",
+ "title": "Compartilhar marca de tempo",
+ "description": "Compartilhe uma URL com marca de tempo da posição atual da reprodução ou escolha uma marca de tempo personalizada. Observe que esta não é uma URL de compartilhamento público e só pode ser acessada por usuários que tenham acesso ao Frigate e a esta câmera.",
+ "custom": "Marcação de data/hora personalizado",
+ "button": "Compartilhar URL com marcação de tempo",
+ "shareTitle": "Marcação de data/hora da análise do Frigate: {{camera}}"
}
},
"imagePicker": {
diff --git a/web/public/locales/pt-BR/components/player.json b/web/public/locales/pt-BR/components/player.json
index 370565b633..e783725e8b 100644
--- a/web/public/locales/pt-BR/components/player.json
+++ b/web/public/locales/pt-BR/components/player.json
@@ -4,7 +4,8 @@
"noPreviewFoundFor": "Nenhuma Pré-Visualização Encontrada em {{cameraName}}",
"submitFrigatePlus": {
"title": "Enviar esse frame para Frigate+?",
- "submit": "Enviar"
+ "submit": "Enviar",
+ "previewError": "Não foi possível carregar a prévia do instantâneo. A gravação pode não estar disponível no momento."
},
"livePlayerRequiredIOSVersion": "iOS 17.1 ou superior é necessário para esse tipo de transmissão ao vivo.",
"streamOffline": {
@@ -47,5 +48,6 @@
"error": {
"submitFrigatePlusFailed": "Falha em submeter quadro ao Frigate+"
}
- }
+ },
+ "cameraOff": "A câmera está desligada"
}
diff --git a/web/public/locales/pt-BR/config/cameras.json b/web/public/locales/pt-BR/config/cameras.json
index b065dbb258..4be948c526 100644
--- a/web/public/locales/pt-BR/config/cameras.json
+++ b/web/public/locales/pt-BR/config/cameras.json
@@ -12,7 +12,7 @@
"description": "Habilitado"
},
"audio": {
- "label": "Eventos de áudio",
+ "label": "Detecção de áudio",
"description": "Configurações para detecção de eventos baseados em áudio para esta câmera.",
"enabled": {
"label": "Habilitar detecção de áudio",
@@ -28,11 +28,15 @@
},
"listen": {
"label": "Tipos de escuta",
- "description": "Lista de tipos de eventos de áudio a serem detectados (por exemplo: latido, alarme de incêndio, grito, fala, berro)."
+ "description": "Lista de tipos de eventos de áudio a serem detectados (por exemplo: latido, alarme de incêndio, fala, grito)."
},
"filters": {
"label": "Filtros de áudio",
- "description": "Configurações de filtro por tipo de áudio, como limites de confiança, usadas para reduzir falsos positivos."
+ "description": "Configurações de filtro por tipo de áudio, como limites de confiança, usadas para reduzir falsos positivos.",
+ "threshold": {
+ "label": "Confiança mínima do áudio",
+ "description": "Limite mínimo de confiança para que o evento de áudio seja contabilizado."
+ }
},
"enabled_in_config": {
"label": "Estado de áudio original",
@@ -45,6 +49,497 @@
},
"label": "Configuração da Câmera",
"audio_transcription": {
- "label": "Transcrição de áudio"
+ "label": "Transcrição de áudio",
+ "enabled": {
+ "label": "Habilitar transcrição",
+ "description": "Ative ou desative a transcrição de eventos de áudio acionada manualmente."
+ },
+ "live_enabled": {
+ "label": "Transcrição em tempo real",
+ "description": "Ative a transcrição ao vivo por streaming para o áudio à medida que ele é recebido."
+ },
+ "description": "Configurações de transcrição de áudio e voz ao vivo para eventos e legendas em tempo real.",
+ "enabled_in_config": {
+ "label": "Estado original da transcrição"
+ }
+ },
+ "detect": {
+ "enabled": {
+ "label": "Ativar detecção de objetos",
+ "description": "Ative ou desative a detecção de objetos para esta câmera."
+ },
+ "height": {
+ "label": "Detectar altura",
+ "description": "Altura (em pixels) dos quadros usados para o fluxo de detecção; deixe em branco para usar a resolução nativa do fluxo."
+ },
+ "width": {
+ "label": "Detectar largura",
+ "description": "Largura (em pixels) dos quadros usados para o fluxo de detecção; deixe em branco para usar a resolução nativa do fluxo."
+ },
+ "fps": {
+ "label": "Detectar FPS",
+ "description": "Taxa de quadros por segundo desejada para a execução da detecção; valores mais baixos reduzem o uso da CPU (o valor recomendado é 5; defina um valor mais alto — no máximo 10 — apenas se estiver rastreando objetos que se movem extremamente rápido)."
+ },
+ "min_initialized": {
+ "label": "Quadros de inicialização mínimos",
+ "description": "Número de detecções consecutivas necessárias antes de criar um objeto rastreado. Aumente esse valor para reduzir inicializações falsas. O valor padrão é o fps dividido por 2."
+ },
+ "max_disappeared": {
+ "label": "Máximo de quadros perdidos",
+ "description": "Número de quadros sem detecção antes que um objeto rastreado seja considerado desaparecido."
+ },
+ "stationary": {
+ "label": "Configuração de objetos estáticos",
+ "description": "Configurações para detectar e gerenciar objetos que permanecem parados por um período de tempo.",
+ "interval": {
+ "label": "Intervalo estacionário",
+ "description": "Com que frequência (em quadros) realizar uma verificação de detecção para confirmar um objeto estacionário."
+ },
+ "threshold": {
+ "label": "Limiar estacionário",
+ "description": "Número de quadros sem alteração de posição necessários para marcar um objeto como estacionário."
+ },
+ "max_frames": {
+ "label": "Quadros máximos",
+ "description": "Limita por quanto tempo objetos estacionários são rastreados antes de serem descartados.",
+ "default": {
+ "label": "Máximo de quadros padrão",
+ "description": "Número máximo padrão de quadros para rastrear um objeto estacionário antes de parar."
+ },
+ "objects": {
+ "label": "Quadros máximos do objeto",
+ "description": "Substituições por objeto para o número máximo de quadros de rastreamento de objetos estacionários."
+ }
+ },
+ "classifier": {
+ "label": "Ativar classificador visual",
+ "description": "Use um classificador visual para detectar objetos verdadeiramente estacionários, mesmo quando as caixas delimitadoras oscilam."
+ }
+ },
+ "annotation_offset": {
+ "label": "Deslocamento da anotação",
+ "description": "Milissegundos para deslocar as anotações de detecção, a fim de alinhar melhor as caixas delimitadoras da linha do tempo com as gravações; pode ser um valor positivo ou negativo."
+ },
+ "label": "Detecção de Objetos",
+ "description": "Configurações para a função de detecção (*detect*) usada para executar a detecção de objetos e inicializar rastreadores."
+ },
+ "face_recognition": {
+ "label": "Reconhecimento facial",
+ "description": "Configurações de detecção e reconhecimento facial para esta câmera.",
+ "enabled": {
+ "label": "Ativar reconhecimento facial",
+ "description": "Ative ou desative o reconhecimento facial."
+ },
+ "min_area": {
+ "label": "Área mínima do rosto",
+ "description": "Área mínima (em pixels) da caixa delimitadora de um rosto detectado necessária para tentar o reconhecimento."
+ }
+ },
+ "ffmpeg": {
+ "label": "Fluxos (FFmpeg)",
+ "description": "Entradas de fluxo de câmera e opções do FFmpeg, incluindo caminho do binário, argumentos, aceleração de hardware (hwaccel) e argumentos de saída por função.",
+ "path": {
+ "label": "Caminho do FFmpeg",
+ "description": "Caminho para o binário do FFmpeg a ser utilizado ou um alias de versão (\"7.0\" ou \"8.0\")."
+ },
+ "global_args": {
+ "label": "Argumentos globais do FFmpeg",
+ "description": "Argumentos globais passados para processos do FFmpeg."
+ },
+ "hwaccel_args": {
+ "label": "Argumentos de aceleração de hardware",
+ "description": "Argumentos de aceleração de hardware para o FFmpeg. Recomenda-se o uso de predefinições específicas do provedor."
+ },
+ "input_args": {
+ "label": "Argumentos de entrada",
+ "description": "Argumentos de entrada aplicados aos fluxos de entrada do FFmpeg."
+ },
+ "output_args": {
+ "label": "Argumentos de saída",
+ "description": "Argumentos de saída padrão utilizados para diferentes funções do FFmpeg, como detecção e gravação.",
+ "detect": {
+ "label": "Detectar argumentos de saída",
+ "description": "Argumentos de saída padrão para fluxos de detecção de função."
+ },
+ "record": {
+ "label": "Registrar argumentos de saída",
+ "description": "Argumentos de saída padrão para fluxos de função de registro."
+ }
+ },
+ "retry_interval": {
+ "label": "Tempo de nova tentativa do FFmpeg",
+ "description": "Segundos de espera antes de tentar reconectar um fluxo de câmera após uma falha. O padrão é 10."
+ },
+ "apple_compatibility": {
+ "label": "Compatibilidade com a Apple",
+ "description": "Ative a marcação HEVC para melhor compatibilidade com reprodutores da Apple ao gravar em H.265."
+ },
+ "gpu": {
+ "label": "Índice da GPU",
+ "description": "Índice da GPU padrão usado para aceleração de hardware, se disponível."
+ },
+ "inputs": {
+ "label": "Entradas de câmera",
+ "description": "Lista de definições de fluxo de entrada (caminhos e funções) para esta câmera.",
+ "path": {
+ "label": "Caminho de entrada",
+ "description": "URL ou caminho do fluxo de entrada da câmera."
+ },
+ "roles": {
+ "label": "Funções de entrada",
+ "description": "Funções para este fluxo de entrada."
+ },
+ "global_args": {
+ "label": "Argumentos globais do FFmpeg",
+ "description": "Argumentos globais do FFmpeg para este fluxo de entrada."
+ },
+ "hwaccel_args": {
+ "label": "Argumentos de aceleração de hardware",
+ "description": "Argumentos de aceleração de hardware para este fluxo de entrada."
+ },
+ "input_args": {
+ "label": "Argumentos de entrada",
+ "description": "Argumentos de entrada específicos para este fluxo."
+ }
+ }
+ },
+ "birdseye": {
+ "mode": {
+ "label": "Modo de rastreamento",
+ "description": "Modo de inclusão de câmeras no Birdseye: 'objetos', 'movimento' ou 'contínuo'."
+ },
+ "order": {
+ "label": "Posição",
+ "description": "Posição numérica que controla a ordem da câmera no layout de vista aérea (Birdseye)."
+ },
+ "label": "Birdseye",
+ "description": "Configurações para a visualização composta \"Birdseye\", que combina múltiplas transmissões de câmera em um único layout.",
+ "enabled": {
+ "label": "Ativar Birdseye"
+ }
+ },
+ "live": {
+ "label": "Reprodução ao vivo",
+ "description": "Configurações utilizadas pela interface web para controlar a seleção, a resolução e a qualidade da transmissão ao vivo.",
+ "streams": {
+ "label": "Nomes de transmissões ao vivo",
+ "description": "Mapeamento de nomes de fluxos configurados para nomes de restream/go2rtc usados na reprodução ao vivo."
+ },
+ "height": {
+ "description": "Altura (em pixels) para renderizar a transmissão ao vivo jsmpeg na interface web; deve ser menor ou igual à altura da transmissão de detecção."
+ },
+ "quality": {
+ "description": "Qualidade de codificação para o fluxo jsmpeg (1 para a mais alta, 31 para a mais baixa)."
+ }
+ },
+ "motion": {
+ "enabled": {
+ "label": "Ativar detecção de movimento",
+ "description": "Ative ou desative a detecção de movimento para esta câmera."
+ },
+ "threshold": {
+ "label": "Limiar de movimento",
+ "description": "Limiar de diferença de pixels utilizado pelo detector de movimento; valores mais altos reduzem a sensibilidade (faixa de 1 a 255)."
+ },
+ "lightning_threshold": {
+ "label": "Limiar de relâmpago",
+ "description": "Limite para detectar e ignorar picos breves de luminosidade (valores mais baixos indicam maior sensibilidade; valores entre 0,3 e 1,0). Isso não impede totalmente a detecção de movimento; apenas faz com que o detector pare de analisar quadros adicionais assim que o limite é excedido. Gravações baseadas em movimento continuam sendo geradas durante esses eventos."
+ },
+ "skip_motion_threshold": {
+ "label": "Limite de movimento para pular",
+ "description": "Se definido para um valor entre 0,0 e 1,0, e se uma proporção da imagem superior a esse valor sofrer alterações em um único quadro, o detector não retornará caixas de detecção de movimento e será recalibrado imediatamente. Isso pode economizar recursos da CPU e reduzir falsos positivos causados por relâmpagos, tempestades, etc., mas pode deixar passar eventos reais, como o rastreamento automático de um objeto por uma câmera PTZ. A escolha envolve um equilíbrio entre descartar alguns megabytes de gravações e analisar alguns clipes curtos. Deixe a opção não definida (None) para desativar esse recurso."
+ },
+ "improve_contrast": {
+ "label": "Melhorar contraste",
+ "description": "Aplique melhoria de contraste aos quadros antes da análise de movimento para auxiliar na detecção."
+ },
+ "contour_area": {
+ "label": "Área de contorno",
+ "description": "Área mínima do contorno, em pixels, necessária para que um contorno de movimento seja contabilizado."
+ },
+ "delta_alpha": {
+ "label": "Delta Alfa",
+ "description": "Fator de *alpha blending* utilizado na diferenciação de quadros para o cálculo de movimento."
+ },
+ "frame_alpha": {
+ "label": "Quadro alfa",
+ "description": "Valor alfa utilizado ao combinar quadros para o pré-processamento de movimento."
+ },
+ "frame_height": {
+ "label": "Altura da quadro",
+ "description": "Altura em pixels para a qual os quadros são redimensionados ao calcular o movimento."
+ },
+ "mask": {
+ "label": "Coordenadas da máscara",
+ "description": "Coordenadas x,y ordenadas que definem o polígono da máscara de movimento utilizado para incluir ou excluir áreas."
+ },
+ "mqtt_off_delay": {
+ "label": "Atraso de desligamento MQTT",
+ "description": "Segundos de espera após o último movimento antes de publicar um estado \"off\" via MQTT."
+ },
+ "enabled_in_config": {
+ "label": "Estado de movimento original",
+ "description": "Indica se a detecção de movimento estava habilitada na configuração estática original."
+ },
+ "raw_mask": {
+ "label": "Máscara Bruta"
+ },
+ "label": "Detecção de movimento",
+ "description": "Configurações padrão de detecção de movimento para esta câmera."
+ },
+ "objects": {
+ "label": "Objetos",
+ "description": "Configurações padrão de rastreamento de objetos, incluindo quais rótulos rastrear e filtros por objeto.",
+ "track": {
+ "label": "Objetos a rastrear",
+ "description": "Lista de rótulos de objetos a serem rastreados para esta câmera."
+ },
+ "filters": {
+ "label": "Filtros de objeto",
+ "description": "Filtros aplicados aos objetos detectados para reduzir falsos positivos (área, proporção, confiança).",
+ "min_area": {
+ "label": "Área mínima do objeto",
+ "description": "Área mínima da caixa delimitadora (em pixels ou porcentagem) necessária para este tipo de objeto. Pode ser especificada em pixels (inteiro) ou porcentagem (valor de ponto flutuante entre 0,000001 e 0,99)."
+ },
+ "max_area": {
+ "label": "Área máxima do objeto",
+ "description": "Área máxima da caixa delimitadora (pixels ou porcentagem) permitida para este tipo de objeto. Pode ser pixels (int) ou porcentagem (flutuar entre 0,000001 e 0,99)."
+ },
+ "min_ratio": {
+ "label": "Razão de aspecto mínimo",
+ "description": "Razão mínima entre largura e altura necessária para que a caixa delimitadora se qualifique."
+ },
+ "max_ratio": {
+ "label": "Razão de aspecto máxima",
+ "description": "Razão máxima entre largura e altura permitida para que a caixa delimitadora qualifique."
+ },
+ "threshold": {
+ "label": "Limiar de confiança",
+ "description": "Limiar médio de confiança de detecção necessário para que o objeto seja considerado um verdadeiro positivo."
+ },
+ "min_score": {
+ "label": "Confiança mínima",
+ "description": "Confiança mínima de detecção em um único quadro necessária para que o objeto seja contabilizado."
+ },
+ "mask": {
+ "label": "Máscara com filtro",
+ "description": "Coordenadas do polígono que definem onde este filtro é aplicado dentro do quadro."
+ },
+ "raw_mask": {
+ "label": "Máscara Bruta"
+ }
+ },
+ "mask": {
+ "label": "Máscara de objeto",
+ "description": "Polígono de máscara usado para impedir a detecção de objetos em áreas específicas."
+ },
+ "raw_mask": {
+ "label": "Máscara Bruta"
+ },
+ "genai": {
+ "label": "Configuração de objeto da GenAI",
+ "description": "Opções de GenIA para descrever objetos rastreados e enviar quadros para geração.",
+ "enabled": {
+ "label": "Ativar GenAI",
+ "description": "Habilitar por padrão a geração de descrições para objetos rastreados usando GenAI."
+ },
+ "use_snapshot": {
+ "label": "Usar instantâneos",
+ "description": "Use instantâneos de objetos em vez de miniaturas para a geração de descrições pela GenAI."
+ },
+ "prompt": {
+ "label": "Sugestão de legenda",
+ "description": "Modelo de prompt padrão usado ao gerar descrições com GenAI."
+ },
+ "object_prompts": {
+ "label": "Prompts de objeto",
+ "description": "Prompts por objeto para personalizar as saídas de GenAI para rótulos específicos."
+ },
+ "objects": {
+ "label": "Objetos de GenAI",
+ "description": "Lista de rótulos de objetos a serem enviados à GenAI por padrão."
+ },
+ "required_zones": {
+ "label": "Zonas obrigatórias",
+ "description": "Zonas que devem ser acessadas para que os objetos se qualifiquem para a geração de descrições pela GenAI."
+ },
+ "debug_save_thumbnails": {
+ "label": "Salvar miniaturas",
+ "description": "Salve as miniaturas enviadas à GenAI para depuração e revisão."
+ },
+ "send_triggers": {
+ "label": "Gatilhos de GenAI",
+ "description": "Define quando os quadros devem ser enviados para a GenAI (ao final, após atualizações, etc.).",
+ "tracked_object_end": {
+ "label": "Enviar no fim",
+ "description": "Envie uma solicitação à GenAI quando o objeto rastreado terminar."
+ },
+ "after_significant_updates": {
+ "label": "Acionamento antecipado da GenAI",
+ "description": "Envie uma solicitação à GenAI após um número especificado de atualizações significativas do objeto monitorado."
+ }
+ },
+ "enabled_in_config": {
+ "label": "Estado original da GenAI",
+ "description": "Indica se a GenAI foi habilitada na configuração estática original."
+ }
+ }
+ },
+ "lpr": {
+ "enabled": {
+ "label": "Ativar LPR",
+ "description": "Ative ou desative o LPR nesta câmera."
+ },
+ "expire_time": {
+ "label": "Segundos para expirar",
+ "description": "Tempo, em segundos, após o qual uma placa não detectada é removida do rastreador (apenas para câmeras LPR dedicadas)."
+ },
+ "min_area": {
+ "label": "Área mínima da placa",
+ "description": "Área mínima da placa (em pixels) necessária para tentar o reconhecimento."
+ },
+ "enhancement": {
+ "label": "Nível de aprimoramento",
+ "description": "Nível de aprimoramento (0-10) a ser aplicado aos recortes da placa antes do OCR; valores mais altos nem sempre melhoram os resultados, e níveis acima de 5 podem funcionar apenas com placas noturnas, devendo ser utilizados com cautela."
+ }
+ },
+ "record": {
+ "label": "Gravação",
+ "description": "Configurações de gravação e retenção para esta câmera.",
+ "enabled": {
+ "label": "Ativar gravação",
+ "description": "Ative ou desative a gravação para esta câmera."
+ },
+ "expire_interval": {
+ "label": "Intervalo de limpeza de gravação",
+ "description": "Minutos entre as execuções de limpeza que removem segmentos de gravação expirados."
+ },
+ "continuous": {
+ "label": "Retenção contínua",
+ "description": "Número de dias para manter as gravações, independentemente de objetos rastreados ou movimento. Defina como 0 se quiser manter apenas gravações de alertas e detecções.",
+ "days": {
+ "label": "Dias de retenção",
+ "description": "Dias para manter as gravações."
+ }
+ },
+ "motion": {
+ "label": "Retenção de movimento",
+ "description": "Número de dias para manter gravações acionadas por movimento, independentemente dos objetos rastreados. Defina como 0 se quiser manter apenas gravações de alertas e detecções.",
+ "days": {
+ "label": "Dias de retenção",
+ "description": "Dias para manter as gravações."
+ }
+ },
+ "detections": {
+ "label": "Retenção de detecção",
+ "description": "Configurações de retenção de gravação para eventos de detecção, incluindo durações de pré e pós-captura.",
+ "pre_capture": {
+ "label": "Segundos de pré-captura",
+ "description": "Número de segundos anteriores ao evento de detecção a serem incluídos na gravação."
+ },
+ "post_capture": {
+ "label": "Segundos após a captura",
+ "description": "Número de segundos após o evento de detecção a serem incluídos na gravação."
+ },
+ "retain": {
+ "label": "Retenção de eventos",
+ "description": "Configurações de retenção para gravações de eventos de detecção.",
+ "days": {
+ "label": "Dias de retenção",
+ "description": "Número de dias para manter as gravações de eventos de detecção."
+ },
+ "mode": {
+ "label": "Modo de retenção",
+ "description": "Modo de retenção: all (salva todos os segmentos), motion (salva segmentos com movimento) ou active_objects (salva segmentos com objetos ativos)."
+ }
+ }
+ },
+ "alerts": {
+ "label": "Retenção de alertas",
+ "description": "Configurações de retenção de gravações para eventos de alerta, incluindo durações de pré e pós-captura.",
+ "pre_capture": {
+ "label": "Segundos de pré-captura",
+ "description": "Número de segundos anteriores ao evento de detecção a serem incluídos na gravação."
+ },
+ "post_capture": {
+ "label": "Segundos após a captura",
+ "description": "Número de segundos após o evento de detecção a serem incluídos na gravação."
+ },
+ "retain": {
+ "label": "Retenção de eventos",
+ "description": "Configurações de retenção para gravações de eventos de detecção.",
+ "days": {
+ "label": "Dias de retenção",
+ "description": "Número de dias para manter as gravações de eventos de detecção."
+ },
+ "mode": {
+ "label": "Modo de retenção",
+ "description": "Modo de retenção: all (salvar todos os segmentos), motion (salvar segmentos com movimento) ou active_objects (salvar segmentos com objetos ativos)."
+ }
+ }
+ },
+ "export": {
+ "label": "Exportar configuração",
+ "description": "Configurações utilizadas ao exportar gravações, como timelapse e aceleração de hardware.",
+ "hwaccel_args": {
+ "label": "Exportar argumentos de hwaccel",
+ "description": "Argumentos de aceleração de hardware a serem usados para operações de exportação/transcodificação."
+ },
+ "max_concurrent": {
+ "label": "Máximo de exportações simultâneas",
+ "description": "Número máximo de tarefas de exportação a serem processadas simultaneamente."
+ },
+ "chapters": {
+ "label": "Metadados de capítulos para incorporar nas gravações exportadas"
+ }
+ },
+ "preview": {
+ "label": "Pré-visualizar configuração",
+ "description": "Configurações que controlam a qualidade das prévias de gravação exibidas na interface de usuário.",
+ "quality": {
+ "label": "Qualidade da pré-visualização",
+ "description": "Nível de qualidade da pré-visualização (muito_baixa, baixa, média, alta, muito_alta)."
+ }
+ },
+ "enabled_in_config": {
+ "label": "Estado original da gravação",
+ "description": "Indica se a gravação foi habilitada na configuração estática original."
+ }
+ },
+ "review": {
+ "label": "Rever",
+ "description": "Configurações que controlam alertas, detecções e resumos de revisão por GenAI utilizados pela interface do usuário e pelo armazenamento desta câmera.",
+ "alerts": {
+ "label": "Configuração de alertas",
+ "description": "Configurações sobre quais objetos monitorados geram alertas e como os alertas são retidos.",
+ "enabled": {
+ "label": "Ativar alertas",
+ "description": "Ative ou desative a geração de alertas para esta câmera."
+ },
+ "labels": {
+ "label": "Etiquetas de alerta",
+ "description": "Lista de rótulos de objetos que se qualificam como alertas (por exemplo: carro, pessoa)."
+ },
+ "required_zones": {
+ "label": "Zonas obrigatórias",
+ "description": "Zonas nas quais um objeto deve entrar para ser considerado um alerta; deixe em branco para permitir qualquer zona."
+ },
+ "enabled_in_config": {
+ "label": "Estado dos alertas originais",
+ "description": "Monitora se os alertas estavam originalmente habilitados na configuração estática."
+ },
+ "cutoff_time": {
+ "label": "Horário limite dos alertas",
+ "description": "Segundos de espera após a ausência de atividade que gere alerta antes de encerrar o alerta."
+ }
+ },
+ "detections": {
+ "label": "Configuração de detecções",
+ "description": "Configurações que definem para quais objetos rastreados são geradas detecções (sem gerar alerta) e como essas detecções são retidas.",
+ "enabled": {
+ "label": "Ativar detecções",
+ "description": "Ative ou desative eventos de detecção para esta câmera."
+ }
+ }
}
}
diff --git a/web/public/locales/pt-BR/config/global.json b/web/public/locales/pt-BR/config/global.json
index a9cbd3f9c6..1cf4a289ef 100644
--- a/web/public/locales/pt-BR/config/global.json
+++ b/web/public/locales/pt-BR/config/global.json
@@ -34,11 +34,15 @@
},
"listen": {
"label": "Tipos de escuta",
- "description": "Lista de tipos de eventos de áudio a serem detectados (por exemplo: latido, alarme de incêndio, grito, fala, berro)."
+ "description": "Lista de tipos de eventos de áudio a serem detectados (por exemplo: latido, alarme de incêndio, fala, grito)."
},
"filters": {
"label": "Filtros de áudio",
- "description": "Configurações de filtro por tipo de áudio, como limites de confiança, usadas para reduzir falsos positivos."
+ "description": "Configurações de filtro por tipo de áudio, como limites de confiança, usadas para reduzir falsos positivos.",
+ "threshold": {
+ "label": "Confiança mínima do áudio",
+ "description": "Limite mínimo de confiança para que o evento de áudio seja contabilizado."
+ }
},
"enabled_in_config": {
"label": "Estado de áudio original",
@@ -47,7 +51,8 @@
"num_threads": {
"label": "Threads de detecção",
"description": "Número de threads a serem usadas para o processamento de detecção de áudio."
- }
+ },
+ "label": "Detecção de áudio"
},
"auth": {
"label": "Autenticação",
@@ -71,9 +76,484 @@
"session_length": {
"label": "Duração da sessão",
"description": "Duração da sessão em segundos para sessões baseadas em JWT."
+ },
+ "failed_login_rate_limit": {
+ "label": "Limites de falha de login"
+ },
+ "refresh_time": {
+ "label": "Janela de atualização da sessão"
}
},
"audio_transcription": {
- "label": "Transcrição de áudio"
+ "label": "Transcrição de áudio",
+ "live_enabled": {
+ "label": "Transcrição em tempo real",
+ "description": "Ative a transcrição ao vivo por streaming para o áudio à medida que ele é recebido."
+ },
+ "description": "Configurações de transcrição de áudio e voz ao vivo para eventos e legendas em tempo real."
+ },
+ "detect": {
+ "enabled": {
+ "label": "Ativar detecção de objetos"
+ },
+ "height": {
+ "label": "Detectar altura",
+ "description": "Altura (em pixels) dos quadros usados para o fluxo de detecção; deixe em branco para usar a resolução nativa do fluxo."
+ },
+ "width": {
+ "label": "Detectar largura",
+ "description": "Largura (em pixels) dos quadros usados para o fluxo de detecção; deixe em branco para usar a resolução nativa do fluxo."
+ },
+ "fps": {
+ "label": "Detectar FPS",
+ "description": "Taxa de quadros por segundo desejada para a execução da detecção; valores mais baixos reduzem o uso da CPU (o valor recomendado é 5; defina um valor mais alto — no máximo 10 — apenas se estiver rastreando objetos que se movem extremamente rápido)."
+ },
+ "min_initialized": {
+ "label": "Quadros de inicialização mínimos",
+ "description": "Número de detecções consecutivas necessárias antes de criar um objeto rastreado. Aumente esse valor para reduzir inicializações falsas. O valor padrão é o fps dividido por 2."
+ },
+ "max_disappeared": {
+ "label": "Máximo de quadros perdidos",
+ "description": "Número de quadros sem detecção antes que um objeto rastreado seja considerado desaparecido."
+ },
+ "stationary": {
+ "label": "Configuração de objetos estáticos",
+ "description": "Configurações para detectar e gerenciar objetos que permanecem parados por um período de tempo.",
+ "interval": {
+ "label": "Intervalo estacionário",
+ "description": "Com que frequência (em quadros) realizar uma verificação de detecção para confirmar um objeto estacionário."
+ },
+ "threshold": {
+ "label": "Limiar estacionário",
+ "description": "Número de quadros sem alteração de posição necessários para marcar um objeto como estacionário."
+ },
+ "max_frames": {
+ "label": "Quadros máximos",
+ "description": "Limita por quanto tempo objetos estacionários são rastreados antes de serem descartados.",
+ "default": {
+ "label": "Máximo de quadros padrão",
+ "description": "Número máximo padrão de quadros para rastrear um objeto estacionário antes de parar."
+ },
+ "objects": {
+ "label": "Quadros máximos do objeto",
+ "description": "Substituições por objeto para o número máximo de quadros de rastreamento de objetos estacionários."
+ }
+ },
+ "classifier": {
+ "label": "Ativar classificador visual",
+ "description": "Use um classificador visual para detectar objetos verdadeiramente estacionários, mesmo quando as caixas delimitadoras oscilam."
+ }
+ },
+ "annotation_offset": {
+ "label": "Deslocamento da anotação",
+ "description": "Milissegundos para deslocar as anotações de detecção, a fim de alinhar melhor as caixas delimitadoras da linha do tempo com as gravações; pode ser um valor positivo ou negativo."
+ },
+ "label": "Detecção de Objetos",
+ "description": "Configurações para a função de detecção (*detect*) usada para executar a detecção de objetos e inicializar rastreadores."
+ },
+ "face_recognition": {
+ "label": "Reconhecimento facial",
+ "enabled": {
+ "label": "Ativar reconhecimento facial"
+ },
+ "min_area": {
+ "label": "Área mínima do rosto",
+ "description": "Área mínima (em pixels) da caixa delimitadora de um rosto detectado necessária para tentar o reconhecimento."
+ }
+ },
+ "ffmpeg": {
+ "path": {
+ "label": "Caminho do FFmpeg",
+ "description": "Caminho para o binário do FFmpeg a ser utilizado ou um alias de versão (\"7.0\" ou \"8.0\")."
+ },
+ "global_args": {
+ "label": "Argumentos globais do FFmpeg",
+ "description": "Argumentos globais passados para processos do FFmpeg."
+ },
+ "hwaccel_args": {
+ "label": "Argumentos de aceleração de hardware",
+ "description": "Argumentos de aceleração de hardware para o FFmpeg. Recomenda-se o uso de predefinições específicas do provedor."
+ },
+ "input_args": {
+ "label": "Argumentos de entrada",
+ "description": "Argumentos de entrada aplicados aos fluxos de entrada do FFmpeg."
+ },
+ "output_args": {
+ "label": "Argumentos de saída",
+ "description": "Argumentos de saída padrão utilizados para diferentes funções do FFmpeg, como detecção e gravação.",
+ "detect": {
+ "label": "Detectar argumentos de saída",
+ "description": "Argumentos de saída padrão para fluxos de detecção de função."
+ },
+ "record": {
+ "label": "Registrar argumentos de saída",
+ "description": "Argumentos de saída padrão para fluxos de função de registro."
+ }
+ },
+ "retry_interval": {
+ "label": "Tempo de nova tentativa do FFmpeg",
+ "description": "Segundos de espera antes de tentar reconectar um fluxo de câmera após uma falha. O padrão é 10."
+ },
+ "apple_compatibility": {
+ "label": "Compatibilidade com a Apple",
+ "description": "Ative a marcação HEVC para melhor compatibilidade com reprodutores da Apple ao gravar em H.265."
+ },
+ "gpu": {
+ "label": "Índice da GPU",
+ "description": "Índice da GPU padrão usado para aceleração de hardware, se disponível."
+ },
+ "inputs": {
+ "label": "Entradas de câmera",
+ "description": "Lista de definições de fluxo de entrada (caminhos e funções) para esta câmera.",
+ "path": {
+ "label": "Caminho de entrada",
+ "description": "URL ou caminho do fluxo de entrada da câmera."
+ },
+ "roles": {
+ "label": "Funções de entrada",
+ "description": "Funções para este fluxo de entrada."
+ },
+ "global_args": {
+ "label": "Argumentos globais do FFmpeg",
+ "description": "Argumentos globais do FFmpeg para este fluxo de entrada."
+ },
+ "hwaccel_args": {
+ "label": "Argumentos de aceleração de hardware",
+ "description": "Argumentos de aceleração de hardware para este fluxo de entrada."
+ },
+ "input_args": {
+ "label": "Argumentos de entrada",
+ "description": "Argumentos de entrada específicos para este fluxo."
+ }
+ }
+ },
+ "birdseye": {
+ "mode": {
+ "label": "Modo de rastreamento",
+ "description": "Modo de inclusão de câmeras no Birdseye: 'objetos', 'movimento' ou 'contínuo'."
+ },
+ "order": {
+ "label": "Posição",
+ "description": "Posição numérica que controla a ordem da câmera no layout de vista aérea (Birdseye)."
+ },
+ "label": "Birdseye",
+ "description": "Configurações para a visualização composta \"Birdseye\", que combina múltiplas transmissões de câmera em um único layout.",
+ "enabled": {
+ "label": "Ativar Birdseye"
+ }
+ },
+ "live": {
+ "label": "Reprodução ao vivo",
+ "streams": {
+ "label": "Nomes de transmissões ao vivo",
+ "description": "Mapeamento de nomes de fluxos configurados para nomes de restream/go2rtc usados na reprodução ao vivo."
+ },
+ "height": {
+ "description": "Altura (em pixels) para renderizar a transmissão ao vivo jsmpeg na interface web; deve ser menor ou igual à altura da transmissão de detecção."
+ },
+ "quality": {
+ "description": "Qualidade de codificação para o fluxo jsmpeg (1 para a mais alta, 31 para a mais baixa)."
+ }
+ },
+ "motion": {
+ "enabled": {
+ "label": "Ativar detecção de movimento"
+ },
+ "threshold": {
+ "label": "Limiar de movimento",
+ "description": "Limiar de diferença de pixels utilizado pelo detector de movimento; valores mais altos reduzem a sensibilidade (faixa de 1 a 255)."
+ },
+ "lightning_threshold": {
+ "label": "Limiar de relâmpago",
+ "description": "Limite para detectar e ignorar picos breves de luminosidade (valores mais baixos indicam maior sensibilidade; valores entre 0,3 e 1,0). Isso não impede totalmente a detecção de movimento; apenas faz com que o detector pare de analisar quadros adicionais assim que o limite é excedido. Gravações baseadas em movimento continuam sendo geradas durante esses eventos."
+ },
+ "skip_motion_threshold": {
+ "label": "Limite de movimento para pular",
+ "description": "Se definido para um valor entre 0,0 e 1,0, e se uma proporção da imagem superior a esse valor sofrer alterações em um único quadro, o detector não retornará caixas de detecção de movimento e será recalibrado imediatamente. Isso pode economizar recursos da CPU e reduzir falsos positivos causados por relâmpagos, tempestades, etc., mas pode deixar passar eventos reais, como o rastreamento automático de um objeto por uma câmera PTZ. A escolha envolve um equilíbrio entre descartar alguns megabytes de gravações e analisar alguns clipes curtos. Deixe a opção não definida (None) para desativar esse recurso."
+ },
+ "improve_contrast": {
+ "label": "Melhorar contraste",
+ "description": "Aplique melhoria de contraste aos quadros antes da análise de movimento para auxiliar na detecção."
+ },
+ "contour_area": {
+ "label": "Área de contorno",
+ "description": "Área mínima do contorno, em pixels, necessária para que um contorno de movimento seja contabilizado."
+ },
+ "delta_alpha": {
+ "label": "Delta Alfa",
+ "description": "Fator de *alpha blending* utilizado na diferenciação de quadros para o cálculo de movimento."
+ },
+ "frame_alpha": {
+ "label": "Quadro alfa",
+ "description": "Valor alfa utilizado ao combinar quadros para o pré-processamento de movimento."
+ },
+ "frame_height": {
+ "label": "Altura da quadro",
+ "description": "Altura em pixels para a qual os quadros são redimensionados ao calcular o movimento."
+ },
+ "mask": {
+ "label": "Coordenadas da máscara",
+ "description": "Coordenadas x,y ordenadas que definem o polígono da máscara de movimento utilizado para incluir ou excluir áreas."
+ },
+ "mqtt_off_delay": {
+ "label": "Atraso de desligamento MQTT",
+ "description": "Segundos de espera após o último movimento antes de publicar um estado \"off\" via MQTT."
+ },
+ "enabled_in_config": {
+ "label": "Estado de movimento original",
+ "description": "Indica se a detecção de movimento estava habilitada na configuração estática original."
+ },
+ "raw_mask": {
+ "label": "Máscara Bruta"
+ },
+ "label": "Detecção de movimento"
+ },
+ "objects": {
+ "label": "Objetos",
+ "description": "Configurações padrão de rastreamento de objetos, incluindo quais rótulos rastrear e filtros por objeto.",
+ "track": {
+ "label": "Objetos a rastrear"
+ },
+ "filters": {
+ "label": "Filtros de objeto",
+ "description": "Filtros aplicados aos objetos detectados para reduzir falsos positivos (área, proporção, confiança).",
+ "min_area": {
+ "label": "Área mínima do objeto",
+ "description": "Área mínima da caixa delimitadora (em pixels ou porcentagem) necessária para este tipo de objeto. Pode ser especificada em pixels (inteiro) ou porcentagem (valor de ponto flutuante entre 0,000001 e 0,99)."
+ },
+ "max_area": {
+ "label": "Área máxima do objeto",
+ "description": "Área máxima da caixa delimitadora (pixels ou porcentagem) permitida para este tipo de objeto. Pode ser pixels (int) ou porcentagem (flutuar entre 0,000001 e 0,99)."
+ },
+ "min_ratio": {
+ "label": "Razão de aspecto mínimo",
+ "description": "Razão mínima entre largura e altura necessária para que a caixa delimitadora se qualifique."
+ },
+ "max_ratio": {
+ "label": "Razão de aspecto máxima",
+ "description": "Razão máxima entre largura e altura permitida para que a caixa delimitadora qualifique."
+ },
+ "threshold": {
+ "label": "Limiar de confiança",
+ "description": "Limiar médio de confiança de detecção necessário para que o objeto seja considerado um verdadeiro positivo."
+ },
+ "min_score": {
+ "label": "Confiança mínima",
+ "description": "Confiança mínima de detecção em um único quadro necessária para que o objeto seja contabilizado."
+ },
+ "mask": {
+ "label": "Máscara com filtro",
+ "description": "Coordenadas do polígono que definem onde este filtro é aplicado dentro do quadro."
+ },
+ "raw_mask": {
+ "label": "Máscara Bruta"
+ }
+ },
+ "mask": {
+ "label": "Máscara de objeto",
+ "description": "Polígono de máscara usado para impedir a detecção de objetos em áreas específicas."
+ },
+ "raw_mask": {
+ "label": "Máscara Bruta"
+ },
+ "genai": {
+ "label": "Configuração de objeto da GenAI",
+ "description": "Opções de GenIA para descrever objetos rastreados e enviar quadros para geração.",
+ "enabled": {
+ "label": "Ativar GenAI",
+ "description": "Habilitar por padrão a geração de descrições para objetos rastreados usando GenAI."
+ },
+ "use_snapshot": {
+ "label": "Usar instantâneos",
+ "description": "Use instantâneos de objetos em vez de miniaturas para a geração de descrições pela GenAI."
+ },
+ "prompt": {
+ "label": "Sugestão de legenda",
+ "description": "Modelo de prompt padrão usado ao gerar descrições com GenAI."
+ },
+ "object_prompts": {
+ "label": "Prompts de objeto",
+ "description": "Prompts por objeto para personalizar as saídas de GenAI para rótulos específicos."
+ },
+ "objects": {
+ "label": "Objetos de GenAI",
+ "description": "Lista de rótulos de objetos a serem enviados à GenAI por padrão."
+ },
+ "required_zones": {
+ "label": "Zonas obrigatórias",
+ "description": "Zonas que devem ser acessadas para que os objetos se qualifiquem para a geração de descrições pela GenAI."
+ },
+ "debug_save_thumbnails": {
+ "label": "Salvar miniaturas",
+ "description": "Salve as miniaturas enviadas à GenAI para depuração e revisão."
+ },
+ "send_triggers": {
+ "label": "Gatilhos de GenAI",
+ "description": "Define quando os quadros devem ser enviados para a GenAI (ao final, após atualizações, etc.).",
+ "tracked_object_end": {
+ "label": "Enviar no fim",
+ "description": "Envie uma solicitação à GenAI quando o objeto rastreado terminar."
+ },
+ "after_significant_updates": {
+ "label": "Acionamento antecipado da GenAI",
+ "description": "Envie uma solicitação à GenAI após um número especificado de atualizações significativas do objeto monitorado."
+ }
+ },
+ "enabled_in_config": {
+ "label": "Estado original da GenAI",
+ "description": "Indica se a GenAI foi habilitada na configuração estática original."
+ }
+ }
+ },
+ "lpr": {
+ "enabled": {
+ "label": "Ativar LPR"
+ },
+ "expire_time": {
+ "label": "Segundos para expirar",
+ "description": "Tempo, em segundos, após o qual uma placa não detectada é removida do rastreador (apenas para câmeras LPR dedicadas)."
+ },
+ "min_area": {
+ "label": "Área mínima da placa",
+ "description": "Área mínima da placa (em pixels) necessária para tentar o reconhecimento."
+ },
+ "enhancement": {
+ "label": "Nível de aprimoramento",
+ "description": "Nível de aprimoramento (0-10) a ser aplicado aos recortes da placa antes do OCR; valores mais altos nem sempre melhoram os resultados, e níveis acima de 5 podem funcionar apenas com placas noturnas, devendo ser utilizados com cautela."
+ }
+ },
+ "record": {
+ "label": "Gravação",
+ "enabled": {
+ "label": "Ativar gravação"
+ },
+ "expire_interval": {
+ "label": "Intervalo de limpeza de gravação",
+ "description": "Minutos entre as execuções de limpeza que removem segmentos de gravação expirados."
+ },
+ "continuous": {
+ "label": "Retenção contínua",
+ "description": "Número de dias para manter as gravações, independentemente de objetos rastreados ou movimento. Defina como 0 se quiser manter apenas gravações de alertas e detecções.",
+ "days": {
+ "label": "Dias de retenção",
+ "description": "Dias para manter as gravações."
+ }
+ },
+ "motion": {
+ "label": "Retenção de movimento",
+ "description": "Número de dias para manter gravações acionadas por movimento, independentemente dos objetos rastreados. Defina como 0 se quiser manter apenas gravações de alertas e detecções.",
+ "days": {
+ "label": "Dias de retenção",
+ "description": "Dias para manter as gravações."
+ }
+ },
+ "detections": {
+ "label": "Retenção de detecção",
+ "description": "Configurações de retenção de gravação para eventos de detecção, incluindo durações de pré e pós-captura.",
+ "pre_capture": {
+ "label": "Segundos de pré-captura",
+ "description": "Número de segundos anteriores ao evento de detecção a serem incluídos na gravação."
+ },
+ "post_capture": {
+ "label": "Segundos após a captura",
+ "description": "Número de segundos após o evento de detecção a serem incluídos na gravação."
+ },
+ "retain": {
+ "label": "Retenção de eventos",
+ "description": "Configurações de retenção para gravações de eventos de detecção.",
+ "days": {
+ "label": "Dias de retenção",
+ "description": "Número de dias para manter as gravações de eventos de detecção."
+ },
+ "mode": {
+ "label": "Modo de retenção",
+ "description": "Modo de retenção: all (salva todos os segmentos), motion (salva segmentos com movimento) ou active_objects (salva segmentos com objetos ativos)."
+ }
+ }
+ },
+ "alerts": {
+ "label": "Retenção de alertas",
+ "description": "Configurações de retenção de gravações para eventos de alerta, incluindo durações de pré e pós-captura.",
+ "pre_capture": {
+ "label": "Segundos de pré-captura",
+ "description": "Número de segundos anteriores ao evento de detecção a serem incluídos na gravação."
+ },
+ "post_capture": {
+ "label": "Segundos após a captura",
+ "description": "Número de segundos após o evento de detecção a serem incluídos na gravação."
+ },
+ "retain": {
+ "label": "Retenção de eventos",
+ "description": "Configurações de retenção para gravações de eventos de detecção.",
+ "days": {
+ "label": "Dias de retenção",
+ "description": "Número de dias para manter as gravações de eventos de detecção."
+ },
+ "mode": {
+ "label": "Modo de retenção",
+ "description": "Modo de retenção: all (salvar todos os segmentos), motion (salvar segmentos com movimento) ou active_objects (salvar segmentos com objetos ativos)."
+ }
+ }
+ },
+ "export": {
+ "label": "Exportar configuração",
+ "description": "Configurações utilizadas ao exportar gravações, como timelapse e aceleração de hardware.",
+ "hwaccel_args": {
+ "label": "Exportar argumentos de hwaccel",
+ "description": "Argumentos de aceleração de hardware a serem usados para operações de exportação/transcodificação."
+ },
+ "max_concurrent": {
+ "label": "Máximo de exportações simultâneas",
+ "description": "Número máximo de tarefas de exportação a serem processadas simultaneamente."
+ },
+ "chapters": {
+ "label": "Metadados de capítulos para incorporar nas gravações exportadas"
+ }
+ },
+ "preview": {
+ "label": "Pré-visualizar configuração",
+ "description": "Configurações que controlam a qualidade das prévias de gravação exibidas na interface de usuário.",
+ "quality": {
+ "label": "Qualidade da pré-visualização",
+ "description": "Nível de qualidade da pré-visualização (muito_baixa, baixa, média, alta, muito_alta)."
+ }
+ },
+ "enabled_in_config": {
+ "label": "Estado original da gravação",
+ "description": "Indica se a gravação foi habilitada na configuração estática original."
+ }
+ },
+ "review": {
+ "label": "Rever",
+ "alerts": {
+ "label": "Configuração de alertas",
+ "description": "Configurações sobre quais objetos monitorados geram alertas e como os alertas são retidos.",
+ "enabled": {
+ "label": "Ativar alertas"
+ },
+ "labels": {
+ "label": "Etiquetas de alerta",
+ "description": "Lista de rótulos de objetos que se qualificam como alertas (por exemplo: carro, pessoa)."
+ },
+ "required_zones": {
+ "label": "Zonas obrigatórias",
+ "description": "Zonas nas quais um objeto deve entrar para ser considerado um alerta; deixe em branco para permitir qualquer zona."
+ },
+ "enabled_in_config": {
+ "label": "Estado dos alertas originais",
+ "description": "Monitora se os alertas estavam originalmente habilitados na configuração estática."
+ },
+ "cutoff_time": {
+ "label": "Horário limite dos alertas",
+ "description": "Segundos de espera após a ausência de atividade que gere alerta antes de encerrar o alerta."
+ }
+ },
+ "detections": {
+ "label": "Configuração de detecções",
+ "description": "Configurações que definem para quais objetos rastreados são geradas detecções (sem gerar alerta) e como essas detecções são retidas.",
+ "enabled": {
+ "label": "Ativar detecções"
+ }
+ }
}
}
diff --git a/web/public/locales/pt-BR/config/groups.json b/web/public/locales/pt-BR/config/groups.json
index a392ecc761..cfe1a88297 100644
--- a/web/public/locales/pt-BR/config/groups.json
+++ b/web/public/locales/pt-BR/config/groups.json
@@ -64,5 +64,10 @@
"retention": "Retenção",
"events": "Eventos"
}
+ },
+ "ffmpeg": {
+ "cameras": {
+ "cameraFfmpeg": "Argumentos do FFmpeg específicos para a câmera"
+ }
}
}
diff --git a/web/public/locales/pt-BR/config/validation.json b/web/public/locales/pt-BR/config/validation.json
index 3fc8086682..073d1a5785 100644
--- a/web/public/locales/pt-BR/config/validation.json
+++ b/web/public/locales/pt-BR/config/validation.json
@@ -28,5 +28,8 @@
"detectRequired": "Pelo menos um fluxo de entrada deve ter a função 'detectar' atribuída.",
"hwaccelDetectOnly": "Somente o fluxo de entrada com a função de detecção pode definir argumentos de aceleração de hardware."
}
+ },
+ "detect": {
+ "dimensionMustBeEven": "Deve ser um número par."
}
}
diff --git a/web/public/locales/pt-BR/views/chat.json b/web/public/locales/pt-BR/views/chat.json
new file mode 100644
index 0000000000..8f63a9e1a2
--- /dev/null
+++ b/web/public/locales/pt-BR/views/chat.json
@@ -0,0 +1,63 @@
+{
+ "documentTitle": "Frigate-Chat",
+ "title": "Chat-Frigate",
+ "subtitle": "Seu assistente de IA para gerenciamento de câmeras e insights",
+ "placeholder": "Pergunte o que quiser...",
+ "error": "Algo deu errado. Por favor, tente novamente.",
+ "processing": "Processando...",
+ "toolsUsed": "Usado: {{tools}}",
+ "showTools": "Mostrar ferramentas ({{count}})",
+ "hideTools": "Ocultar ferramentas",
+ "call": "Chamar",
+ "result": "Resultado",
+ "arguments": "Argumentos:",
+ "response": "Resposta:",
+ "attachment_chip_label": "{{label}} em {{camera}}",
+ "attachment_chip_remove": "Remover anexo",
+ "open_in_explore": "Abrir no Explorar",
+ "attach_event_aria": "Anexar evento {{eventId}}",
+ "attachment_picker_paste_label": "Ou cole o ID do evento",
+ "attachment_picker_attach": "Anexar",
+ "attachment_picker_placeholder": "Anexar um evento",
+ "quick_reply_find_similar": "Encontre avistamentos semelhantes",
+ "quick_reply_tell_me_more": "Conte-me mais sobre isso",
+ "quick_reply_when_else": "Em que outra ocasião isso foi visto?",
+ "quick_reply_find_similar_text": "Encontre avistamentos semelhantes a este.",
+ "quick_reply_tell_me_more_text": "Conte-me mais sobre este.",
+ "quick_reply_when_else_text": "Em que outra ocasião isso foi visto?",
+ "anchor": "Referência",
+ "similarity_score": "Semelhança",
+ "no_similar_objects_found": "Nenhum objeto semelhante encontrado.",
+ "semantic_search_required": "A busca semântica deve estar habilitada para encontrar objetos semelhantes.",
+ "send": "Enviar",
+ "suggested_requests": "Experimente perguntar:",
+ "starting_requests": {
+ "show_recent_events": "Mostrar eventos recentes",
+ "show_camera_status": "Mostrar status da câmera",
+ "recap": "O que aconteceu enquanto eu estava fora?",
+ "watch_camera": "Monitore uma câmera em busca de atividade"
+ },
+ "starting_requests_prompts": {
+ "show_recent_events": "Mostre-me os eventos recentes da última hora",
+ "show_camera_status": "Qual é o status atual das minhas câmeras?",
+ "recap": "O que aconteceu enquanto eu estava fora?",
+ "watch_camera": "Fique de olho na porta da frente e me avise se alguém aparecer"
+ },
+ "new_chat": "Novo chat",
+ "settings": {
+ "title": "Configurações do chat",
+ "show_stats": {
+ "title": "Mostrar estatísticas",
+ "desc": "Exibir a taxa de geração e o tamanho do contexto para respostas de chat.",
+ "while_generating": "Durante a geração",
+ "always": "Sempre"
+ },
+ "auto_scroll": {
+ "title": "Rolagem automática",
+ "desc": "Acompanhe as novas mensagens à medida que chegam."
+ }
+ },
+ "stats": {
+ "context": "{{tokens}} tokens"
+ }
+}
diff --git a/web/public/locales/pt-BR/views/classificationModel.json b/web/public/locales/pt-BR/views/classificationModel.json
index afa3fafbb5..39bc880a87 100644
--- a/web/public/locales/pt-BR/views/classificationModel.json
+++ b/web/public/locales/pt-BR/views/classificationModel.json
@@ -13,10 +13,10 @@
"toast": {
"success": {
"deletedCategory_one": "Classe Apagada",
- "deletedCategory_many": "",
+ "deletedCategory_many": "Classes apagadas",
"deletedCategory_other": "",
- "deletedImage_one": "Imagens Apagadas",
- "deletedImage_many": "",
+ "deletedImage_one": "Imagen Apagada",
+ "deletedImage_many": "Imagens Apagadas",
"deletedImage_other": "",
"categorizedImage": "Imagem Classificada com Sucesso",
"trainedModel": "Modelo treinado com sucesso.",
@@ -29,14 +29,15 @@
"reclassifiedImage": "Imagem reclassificada com sucesso"
},
"error": {
- "deleteImageFailed": "Falha ao deletar:{{errorMessage}}",
- "deleteCategoryFailed": "Falha ao deletar classe:{{errorMessage}}",
+ "deleteImageFailed": "Falha ao excluir:{{errorMessage}}",
+ "deleteCategoryFailed": "Falha ao excluir classe:{{errorMessage}}",
"categorizeFailed": "Falha ao categorizar imagem:{{errorMessage}}",
"deleteModelFailed": "Falha ao excluir o modelo: {{errorMessage}}",
"trainingFailed": "Treinamento do modelo falhou. Verifique os logs do Frigate para mais detalhes.",
"trainingFailedToStart": "Falha ao iniciar o treinamento do modelo: {{errorMessage}}",
"updateModelFailed": "Falha ao atualizar modelo: {{errorMessage}}",
- "renameCategoryFailed": "Falha ao renomear classe: {{errorMessage}}"
+ "renameCategoryFailed": "Falha ao renomear classe: {{errorMessage}}",
+ "reclassifyFailed": "Falha ao reclassificar imagem: {{errorMessage}}"
}
},
"deleteCategory": {
@@ -93,7 +94,14 @@
},
"noModels": {
"object": {
- "title": "Nenhum Modelo de Classificação de Objetos"
+ "title": "Nenhum Modelo de Classificação de Objetos",
+ "description": "Crie um modelo personalizado para classificar objetos detectados.",
+ "buttonText": "Criar modelo de objeto"
+ },
+ "state": {
+ "title": "Modelos de Classificação Sem Estado",
+ "description": "Crie um modelo personalizado para monitorar e classificar mudanças de estado em áreas específicas da câmera.",
+ "buttonText": "Criar modelo de estado"
}
},
"deleteDatasetImages": {
@@ -103,5 +111,13 @@
"title": "Editar Modelo de Classificação",
"descriptionState": "Edite as classes deste modelo de classificação de estado. As alterações exigirão treinar novamente o modelo.",
"descriptionObject": "Edite o tipo de objeto e tipo de classificação para este modelo de classificação de objeto."
+ },
+ "reclassifyImageAs": "Reclassificar imagem como:",
+ "reclassifyImage": "Reclassificar imagem",
+ "wizard": {
+ "title": "Criar nova classificação",
+ "steps": {
+ "nameAndDefine": "Nomear e definir"
+ }
}
}
diff --git a/web/public/locales/pt-BR/views/exports.json b/web/public/locales/pt-BR/views/exports.json
index db100ff0cf..83a5c298b2 100644
--- a/web/public/locales/pt-BR/views/exports.json
+++ b/web/public/locales/pt-BR/views/exports.json
@@ -22,7 +22,8 @@
"downloadVideo": "Baixar vídeo",
"editName": "Editar nome",
"deleteExport": "Apagar exportação",
- "assignToCase": "Adicionar ao caso"
+ "assignToCase": "Adicionar ao caso",
+ "removeFromCase": "Remover da caixa"
},
"headings": {
"uncategorizedExports": "Exportações não categorizadas",
diff --git a/web/public/locales/pt-BR/views/faceLibrary.json b/web/public/locales/pt-BR/views/faceLibrary.json
index 7e8c8f56c3..9e7a5b34c2 100644
--- a/web/public/locales/pt-BR/views/faceLibrary.json
+++ b/web/public/locales/pt-BR/views/faceLibrary.json
@@ -72,7 +72,11 @@
"title": "Reconhecimentos Recentes",
"aria": "Selecionar reconhecimentos recentes",
"empty": "Não há tentativas recentes de reconhecimento facial",
- "titleShort": "Recente"
+ "titleShort": "Recente",
+ "emptyNoLibrary": {
+ "title": "Carregue uma foto de rosto",
+ "description": "Você deve adicionar pelo menos um rosto à biblioteca para que o reconhecimento facial funcione."
+ }
},
"selectFace": "Selecionar Rosto",
"trainFaceAs": "Treinar Rosto como:",
@@ -89,7 +93,8 @@
"renamedFace": "O rosto foi renomeado com sucesso para {{name}}",
"deletedName_one": "{{count}} rosto foi deletado com sucesso.",
"deletedName_many": "{{count}} rostos foram deletados com sucesso.",
- "deletedName_other": "{{count}} rostos foram deletados com sucesso."
+ "deletedName_other": "{{count}} rostos foram deletados com sucesso.",
+ "reclassifiedFace": "Rosto reclassificado com sucesso."
},
"error": {
"uploadingImageFailed": "Falha ao enviar a imagem: {{errorMessage}}",
@@ -98,7 +103,10 @@
"deleteNameFailed": "Falha ao deletar nome: {{errorMessage}}",
"renameFaceFailed": "Falha ao renomear rosto: {{errorMessage}}",
"trainFailed": "Falha ao treinar: {{errorMessage}}",
- "updateFaceScoreFailed": "Falha ao atualizar pontuação de rosto: {{errorMessage}}"
+ "updateFaceScoreFailed": "Falha ao atualizar pontuação de rosto: {{errorMessage}}",
+ "reclassifyFailed": "Falha ao reclassificar o rosto: {{errorMessage}}"
}
- }
+ },
+ "reclassifyFaceAs": "Reclassificar rosto como:",
+ "reclassifyFace": "Reclassificar rosto"
}
diff --git a/web/public/locales/pt-BR/views/live.json b/web/public/locales/pt-BR/views/live.json
index c2459b6403..79d446d5f5 100644
--- a/web/public/locales/pt-BR/views/live.json
+++ b/web/public/locales/pt-BR/views/live.json
@@ -17,7 +17,8 @@
"clickMove": {
"label": "Clique no quadro para centralizar a câmera",
"enable": "Ativar clique para mover",
- "disable": "Desativar clique para mover"
+ "disable": "Desativar clique para mover",
+ "enableWithZoom": "Habilitar clicar para mover / arrastar para zoom"
},
"left": {
"label": "Mova a câmera PTZ para a esquerda"
@@ -57,7 +58,9 @@
},
"camera": {
"enable": "Ativar Câmera",
- "disable": "Desabilitar Câmera"
+ "disable": "Desabilitar Câmera",
+ "turnOn": "Ligar a câmera",
+ "turnOff": "Desligar a câmera"
},
"muteCameras": {
"enable": "Silenciar Todas as Câmeras",
@@ -69,7 +72,8 @@
},
"recording": {
"enable": "Ativar Gravação",
- "disable": "Desativar Gravação"
+ "disable": "Desativar Gravação",
+ "disabledInConfig": "A gravação deve ser habilitada primeiro nas configurações desta câmera."
},
"snapshots": {
"enable": "Permitir Capturas de Imagem",
@@ -149,7 +153,8 @@
"snapshots": "Capturas de Imagem",
"audioDetection": "Detecção de Áudio",
"autotracking": "Auto Rastreamento",
- "transcription": "Transcrição de Áudio"
+ "transcription": "Transcrição de Áudio",
+ "camera": "Câmera"
},
"history": {
"label": "Exibir gravação histórica"
diff --git a/web/public/locales/pt-BR/views/motionSearch.json b/web/public/locales/pt-BR/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/pt-BR/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/pt-BR/views/replay.json b/web/public/locales/pt-BR/views/replay.json
new file mode 100644
index 0000000000..58ccb6963e
--- /dev/null
+++ b/web/public/locales/pt-BR/views/replay.json
@@ -0,0 +1,19 @@
+{
+ "title": "Depuração de Replay",
+ "description": "“Reproduza gravações de câmera para depuração. A lista de objetos exibe um resumo com atraso temporal, dos objetos detectados e a aba de mensagens mostra um fluxo de mensagens internas do Frigate provenientes da gravação reproduzida.",
+ "websocket_messages": "Mensagens",
+ "dialog": {
+ "title": "Iniciar Replay de Depuração",
+ "description": "Crie uma câmera de reprodução temporária que exiba imagens históricas em loop para depurar problemas de detecção e rastreamento de objetos. A câmera de reprodução terá a mesma configuração de detecção da câmera de origem. Escolha um intervalo de tempo para começar.",
+ "camera": "Câmera de origem",
+ "timeRange": "Intervalo de tempo",
+ "preset": {
+ "1m": "Último minuto",
+ "5m": "Últimos 5 minutos",
+ "timeline": "Da Linha do Tempo",
+ "custom": "Personalizada"
+ },
+ "startButton": "Iniciar Reprodução",
+ "selectFromTimeline": "Selecionar"
+ }
+}
diff --git a/web/public/locales/pt-BR/views/settings.json b/web/public/locales/pt-BR/views/settings.json
index 7998227749..00d86e28b1 100644
--- a/web/public/locales/pt-BR/views/settings.json
+++ b/web/public/locales/pt-BR/views/settings.json
@@ -15,7 +15,8 @@
"globalConfig": "Configuração Global - Frigate",
"cameraConfig": "Configuração da Câmera - Frigate",
"maintenance": "Manutenção - Frigate",
- "profiles": "Perfis - Frigate"
+ "profiles": "Perfis - Frigate",
+ "detectorsAndModel": "Detectores e modelo - Frigate"
},
"menu": {
"ui": "UI",
@@ -34,7 +35,50 @@
"general": "Geral",
"globalConfig": "Configuração global",
"system": "Sistema",
- "integrations": "Integrações"
+ "integrations": "Integrações",
+ "uiSettings": "Configurações de interface",
+ "profiles": "Perfis",
+ "globalDetect": "Detecção de objeto",
+ "globalRecording": "Gravando",
+ "globalFfmpeg": "FFmpeg",
+ "globalMotion": "Detecção de movimento",
+ "globalObjects": "Objetos",
+ "globalReview": "Revisar",
+ "globalSnapshots": "Instantâneos",
+ "globalAudioEvents": "Detecção de áudio",
+ "globalLivePlayback": "Reprodução ao vivo",
+ "globalTimestampStyle": "Estilo de carimbo de data/hora",
+ "systemDatabase": "Banco de dados",
+ "systemTls": "TLS",
+ "systemAuthentication": "Autenticação",
+ "systemNetworking": "Rede",
+ "systemProxy": "Proxy",
+ "systemUi": "IU",
+ "systemLogging": "Registro",
+ "systemEnvironmentVariables": "Variáveis de ambiente",
+ "systemTelemetry": "Telemetria",
+ "systemBirdseye": "Birdseye",
+ "systemFfmpeg": "FFmpeg",
+ "systemDetectorsAndModel": "Detectores e modelo",
+ "systemMqtt": "MQTT",
+ "systemGo2rtcStreams": "Fluxo de go2rtc",
+ "integrationSemanticSearch": "Busca semântica",
+ "integrationGenerativeAi": "IA generativa",
+ "integrationFaceRecognition": "Reconhecimento facial",
+ "integrationLpr": "Reconhecimento de placas",
+ "integrationObjectClassification": "Classificação de objetos",
+ "integrationAudioTranscription": "Transcrição de áudio",
+ "cameraDetect": "Detecção de objetos",
+ "cameraFfmpeg": "Fluxos (FFmpeg)",
+ "cameraRecording": "Gravação",
+ "cameraSnapshots": "Instantâneos",
+ "cameraMotion": "Detecção de movimento",
+ "cameraObjects": "Objetos",
+ "cameraConfigReview": "Análise",
+ "cameraAudioEvents": "Detecção de áudio",
+ "cameraAudioTranscription": "Transcrição de áudio",
+ "cameraNotifications": "Notificações",
+ "cameraLivePlayback": "Reprodução ao vivo"
},
"dialog": {
"unsavedChanges": {
@@ -922,6 +966,15 @@
"overriddenGlobal": "Substituir (Global)",
"overriddenGlobalTooltip": "Esta câmera substitui as configurações globais desta seção",
"overriddenBaseConfig": "Substituído (Configuração base)",
- "overriddenBaseConfigTooltip": "O perfil {{profile}} substitui as configurações desta seção"
+ "overriddenBaseConfigTooltip": "O perfil {{profile}} substitui as configurações desta seção",
+ "overriddenBaseConfigNoDeltas": "O perfil {{profile}} substitui esta seção, mas nenhum valor de campo difere da configuração base.",
+ "overriddenInCameras": {
+ "profilePrefix": "{{profile}} perfil: {{fields}}"
+ }
+ },
+ "menuDot": {
+ "overrideGlobal": "Esta seção substitui a configuração global",
+ "overrideProfile": "Esta seção é substituída pelo perfil {{profile}}",
+ "unsaved": "Esta seção contém alterações não salvas"
}
}
diff --git a/web/public/locales/pt-BR/views/system.json b/web/public/locales/pt-BR/views/system.json
index 9226297196..431fd6ba06 100644
--- a/web/public/locales/pt-BR/views/system.json
+++ b/web/public/locales/pt-BR/views/system.json
@@ -50,8 +50,10 @@
"lpr": "LPR",
"camera_activity": "Atividade da câmera",
"system": "Sistema",
- "camera": "Camera"
- }
+ "camera": "Camera",
+ "all_cameras": "Todas as cameras"
+ },
+ "empty": "Nenhuma mensagem capturada ainda"
}
},
"general": {
diff --git a/web/public/locales/pt/audio.json b/web/public/locales/pt/audio.json
index 3bf1ba60b4..e0a4f354f2 100644
--- a/web/public/locales/pt/audio.json
+++ b/web/public/locales/pt/audio.json
@@ -425,5 +425,76 @@
"chop": "Corte",
"eruption": "Erupção",
"environmental_noise": "Ruído Ambiental",
- "field_recording": "Gravação de Campo"
+ "field_recording": "Gravação de Campo",
+ "change_ringing": "Mudar Toque",
+ "shofar": "Berrante",
+ "liquid": "Líquido",
+ "splash": "Respingar",
+ "slosh": "Respingo",
+ "squish": "Esmagar",
+ "drip": "Pingar",
+ "pour": "Entornar",
+ "trickle": "Gotejar",
+ "gush": "Jorrar",
+ "fill": "Preencher",
+ "spray": "Borrifar",
+ "pump": "Bombear",
+ "stir": "Mexer",
+ "boiling": "A ferver",
+ "sonar": "Sonar",
+ "arrow": "Flecha",
+ "whoosh": "Uau",
+ "thump": "Baque",
+ "thunk": "Tombo",
+ "electronic_tuner": "Afinador Eletrônico",
+ "effects_unit": "Unidade de Efeitos",
+ "chorus_effect": "Efeito Coro",
+ "basketball_bounce": "Quique da bola",
+ "bang": "Batida",
+ "slap": "Tapa",
+ "whack": "Bater",
+ "smash": "Esmagar",
+ "breaking": "A quebrar",
+ "bouncing": "A quicar",
+ "whip": "Chicote",
+ "flap": "Guia",
+ "scratch": "Arranhão",
+ "scrape": "Raspagem",
+ "rub": "Esfregar",
+ "roll": "Rolar",
+ "crushing": "Esmagamento",
+ "crumpling": "Amarrotar",
+ "tearing": "A rasgar",
+ "beep": "Bip",
+ "ping": "Pingo",
+ "ding": "Campainha",
+ "clang": "Estridente",
+ "squeal": "Guincho",
+ "creak": "Ranger",
+ "rustle": "Farfalhar",
+ "whir": "Zumbir",
+ "clatter": "Barulho",
+ "sizzle": "Chiado",
+ "clicking": "A clicar",
+ "clickety_clack": "Tique-taque",
+ "rumble": "Estrondo",
+ "plop": "Ploft",
+ "hum": "Zumbir",
+ "boing": "Poin",
+ "crunch": "Mastigar",
+ "sine_wave": "Onda Senoidal",
+ "harmonic": "Harmonica",
+ "chirp_tone": "Som Agudo",
+ "pulse": "Pulso",
+ "inside": "Dentro",
+ "outside": "Fora",
+ "reverberation": "Reverberação",
+ "echo": "Eco",
+ "noise": "Ruído",
+ "mains_hum": "Zumbido Elétrico",
+ "distortion": "Distorção",
+ "sidetone": "Retorno de Voz",
+ "cacophony": "Cacofonia",
+ "throbbing": "Latejante",
+ "vibration": "Vibração"
}
diff --git a/web/public/locales/pt/common.json b/web/public/locales/pt/common.json
index 557d6b48d6..7e7e101454 100644
--- a/web/public/locales/pt/common.json
+++ b/web/public/locales/pt/common.json
@@ -90,7 +90,8 @@
},
"inProgress": "Em andamento",
"invalidStartTime": "Horário de início inválido",
- "invalidEndTime": "Horário de término inválido"
+ "invalidEndTime": "Horário de término inválido",
+ "never": "Nunca"
},
"unit": {
"speed": {
@@ -146,7 +147,19 @@
"next": "Seguinte",
"play": "Tocar",
"pictureInPicture": "Imagem sobre Imagem",
- "continue": "Continuar"
+ "continue": "Continuar",
+ "add": "Adicionar",
+ "applying": "A aplicar…",
+ "undo": "Desfazer",
+ "copiedToClipboard": "Copiado para a área de transferência",
+ "modified": "Modificado",
+ "overridden": "Substituído",
+ "resetToGlobal": "Redefinir para o Global",
+ "resetToDefault": "Redefinir para o Padrão",
+ "saveAll": "Gravar Tudo",
+ "savingAll": "A gravar tudo…",
+ "undoAll": "Desfazer Tudo",
+ "retry": "Tente novamente"
},
"label": {
"back": "Voltar",
@@ -154,7 +167,8 @@
"show": "Exibir {{item}}",
"ID": "ID",
"none": "Nenhum",
- "all": "Todos"
+ "all": "Todos",
+ "other": "Outros"
},
"menu": {
"user": {
@@ -226,7 +240,10 @@
"bg": "Búlgaro",
"gl": "Galego",
"id": "Indonésio Bahasa",
- "ur": "Urdu"
+ "ur": "Urdu",
+ "hr": "Hrvatski (Croata)",
+ "bs": "Bosanski (Bósnio)",
+ "zhHant": "繁體中文 (Chinês Tradicional)"
},
"appearance": "Aparência",
"darkMode": {
@@ -257,7 +274,11 @@
"explore": "Explorar",
"review": "Rever",
"uiPlayground": "Área de Testes da IU",
- "classification": "Classificação"
+ "classification": "Classificação",
+ "profiles": "Perfis",
+ "actions": "Ações",
+ "features": "Funcionalidades",
+ "chat": "Chat"
},
"pagination": {
"previous": {
@@ -284,7 +305,8 @@
"error": {
"noMessage": "Não foi possível guardar as alterações da configuração",
"title": "Não foi possível guardar as alterações da configuração: {{errorMessage}}"
- }
+ },
+ "success": "Alterações gravadas com sucesso."
}
},
"accessDenied": {
@@ -310,5 +332,10 @@
"field": {
"optional": "Opcional",
"internalID": "o Frigate utiliza o ID na configuração e no banco de dados"
+ },
+ "no_items": "Sem elementos",
+ "validation_errors": "Erros de validação",
+ "credentialField": {
+ "savedPlaceholder": "Gravado - deixar em branco para manter a atual"
}
}
diff --git a/web/public/locales/pt/components/camera.json b/web/public/locales/pt/components/camera.json
index 3f7052c817..53e2853231 100644
--- a/web/public/locales/pt/components/camera.json
+++ b/web/public/locales/pt/components/camera.json
@@ -68,7 +68,10 @@
"stream": "Transmissão"
},
"birdseye": "Vista Aérea"
- }
+ },
+ "showAll": "Mostrar todos os Grupos de Câmeras",
+ "showLess": "Mostrar Menos",
+ "editGroups": "Editar Grupos de Câmeras"
},
"debug": {
"options": {
@@ -82,6 +85,7 @@
"zones": "Zonas",
"mask": "Máscara",
"motion": "Movimento",
- "regions": "Regiões"
+ "regions": "Regiões",
+ "paths": "Caminhos"
}
}
diff --git a/web/public/locales/pt/components/dialog.json b/web/public/locales/pt/components/dialog.json
index 2efa3d2b5f..966c9532e2 100644
--- a/web/public/locales/pt/components/dialog.json
+++ b/web/public/locales/pt/components/dialog.json
@@ -36,6 +36,9 @@
"ask_a": "Este objeto é um {{label}}?",
"ask_an": "Este objeto é um {{label}}?",
"ask_full": "Este objeto é um {{untranslatedLabel}} ({{translatedLabel}})?"
+ },
+ "toast": {
+ "error": "Não foi possível submeter para Frigate+. Por favor verifique a sua connecção e tente novamente."
}
}
},
@@ -76,6 +79,13 @@
"select": "Selecionar",
"name": {
"placeholder": "Nome da Exportação"
+ },
+ "case": {
+ "newCaseOption": "Criar um novo caso",
+ "newCaseNamePlaceholder": "Nome do novo caso"
+ },
+ "multiCamera": {
+ "selectFromTimeline": "Selecionar da Timeline"
}
},
"streaming": {
diff --git a/web/public/locales/pt/components/input.json b/web/public/locales/pt/components/input.json
index 1324ed188a..9861b92f5a 100644
--- a/web/public/locales/pt/components/input.json
+++ b/web/public/locales/pt/components/input.json
@@ -1,9 +1,9 @@
{
"button": {
"downloadVideo": {
- "label": "Transferir Vídeo",
+ "label": "Descarregar Vídeo",
"toast": {
- "success": "O vídeo do seu item de análise começou a ser transferido."
+ "success": "O vídeo do seu item de análise começou a ser descarregado."
}
}
}
diff --git a/web/public/locales/pt/views/chat.json b/web/public/locales/pt/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/pt/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/pt/views/motionSearch.json b/web/public/locales/pt/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/pt/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/pt/views/replay.json b/web/public/locales/pt/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/pt/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/pt/views/settings.json b/web/public/locales/pt/views/settings.json
index 1bab92d786..e5229ebe05 100644
--- a/web/public/locales/pt/views/settings.json
+++ b/web/public/locales/pt/views/settings.json
@@ -28,7 +28,8 @@
"triggers": "Gatilhos",
"cameraManagement": "Gestão",
"cameraReview": "Rever",
- "roles": "Papéis"
+ "roles": "Papéis",
+ "profiles": "Perfis"
},
"dialog": {
"unsavedChanges": {
diff --git a/web/public/locales/ro/common.json b/web/public/locales/ro/common.json
index 1938c3d11e..0ce39aef5c 100644
--- a/web/public/locales/ro/common.json
+++ b/web/public/locales/ro/common.json
@@ -136,7 +136,9 @@
"gl": "Galego (Galiciană)",
"id": "Bahasa Indonesia (Indoneziană)",
"ur": "اردو (Urdu)",
- "hr": "Hrvatski (Croată)"
+ "hr": "Hrvatski (Croată)",
+ "bs": "Bosanski (Bosniacă)",
+ "zhHant": "繁體中文 (Chineză tradițională)"
},
"theme": {
"default": "Implicit",
@@ -188,7 +190,8 @@
"classification": "Clasificare",
"chat": "Chat",
"actions": "Acțiuni",
- "profiles": "Profile"
+ "profiles": "Profile",
+ "features": "Funcționalități"
},
"button": {
"cameraAudio": "Sunet cameră",
@@ -321,5 +324,8 @@
"internalID": "ID-ul Intern pe care Frigate îl folosește în configurație și în baza de date"
},
"no_items": "Niciun element",
- "validation_errors": "Erori de validare"
+ "validation_errors": "Erori de validare",
+ "credentialField": {
+ "savedPlaceholder": "Salvat — lasă necompletat pentru a păstra valoarea curentă"
+ }
}
diff --git a/web/public/locales/ro/components/camera.json b/web/public/locales/ro/components/camera.json
index 35f57ff015..7ee0be4837 100644
--- a/web/public/locales/ro/components/camera.json
+++ b/web/public/locales/ro/components/camera.json
@@ -68,7 +68,10 @@
}
},
"birdseye": "Birdseye"
- }
+ },
+ "showAll": "Arată toate grupurile de camere",
+ "showLess": "Arată mai puțin",
+ "editGroups": "Editează grupurile de camere"
},
"debug": {
"options": {
diff --git a/web/public/locales/ro/components/dialog.json b/web/public/locales/ro/components/dialog.json
index ec929c0237..56bee6587a 100644
--- a/web/public/locales/ro/components/dialog.json
+++ b/web/public/locales/ro/components/dialog.json
@@ -59,6 +59,14 @@
"desc": {
"selected": "Ești sigur că vrei să ștergi toate videoclipurile înregistrate asociate acestui element de revizuire? Ține apăsată tasta Shift pentru a sări peste această confirmare pe viitor."
}
+ },
+ "shareTimestamp": {
+ "label": "Partajează timestamp-ul",
+ "title": "Partajează timestamp-ul",
+ "description": "Partajează un URL cu timestamp al poziției actuale din player sau alege un timestamp personalizat. Reține că acesta nu este un URL de partajare publică și este accesibil doar utilizatorilor care au acces la Frigate și la această cameră.",
+ "custom": "Timestamp personalizat",
+ "button": "Partajează URL-ul cu timestamp",
+ "shareTitle": "Timestamp review Frigate: {{camera}}"
}
},
"export": {
@@ -86,19 +94,80 @@
"toast": {
"success": "Exportul a început cu succes. Vizualizați fișierul pe pagina de exporturi.",
"error": {
- "failed": "Eroare la pornirea exportului: {{error}}",
+ "failed": "Nu s-a putut adăuga exportul în coadă: {{error}}",
"endTimeMustAfterStartTime": "Ora de sfârșit trebuie să fie după ora de început",
"noVaildTimeSelected": "Nu a fost selectat un interval de timp valid"
},
- "view": "Vizualizează"
+ "view": "Vizualizează",
+ "queued": "Export pus la coadă. Vezi progresul pe pagina de exporturi.",
+ "batchSuccess_one": "A început 1 export. Se deschide cazul acum.",
+ "batchSuccess_few": "Au început {{count}} exporturi. Se deschide cazul acum.",
+ "batchSuccess_other": "Au început {{count}} de exporturi. Se deschide cazul acum.",
+ "batchPartial": "Au început {{successful}} din {{total}} exporturi. Camere eșuate: {{failedCameras}}",
+ "batchFailed": "Eșec la pornirea a {{total}} exporturi. Camere eșuate: {{failedCameras}}",
+ "batchQueuedSuccess_one": "1 export pus la coadă. Se deschide cazul acum.",
+ "batchQueuedSuccess_few": "{{count}} exporturi puse la coadă. Se deschide cazul acum.",
+ "batchQueuedSuccess_other": "{{count}} de exporturi puse la coadă. Se deschide cazul acum.",
+ "batchQueuedPartial": "S-au pus la coadă {{successful}} din {{total}} exporturi. Camere eșuate: {{failedCameras}}",
+ "batchQueueFailed": "Eșec la punerea la coadă a {{total}} exporturi. Camere eșuate: {{failedCameras}}"
},
"fromTimeline": {
"saveExport": "Salvează exportul",
- "previewExport": "Previzualizează exportul"
+ "previewExport": "Previzualizează exportul",
+ "queueingExport": "Se adaugă exportul la coadă...",
+ "useThisRange": "Folosește acest interval"
},
"case": {
"label": "Caz",
- "placeholder": "Selectează caz"
+ "placeholder": "Selectează caz",
+ "newCaseOption": "Creează un caz nou",
+ "newCaseNamePlaceholder": "Nume caz nou",
+ "newCaseDescriptionPlaceholder": "Descrierea cazului",
+ "nonAdminHelp": "Un caz nou va fi creat pentru aceste exporturi."
+ },
+ "queueing": "Se adaugă exportul la coadă...",
+ "tabs": {
+ "export": "O singură cameră",
+ "multiCamera": "Mai multe camere"
+ },
+ "multiCamera": {
+ "timeRange": "Interval de timp",
+ "selectFromTimeline": "Selectează din timeline",
+ "cameraSelection": "Camere",
+ "cameraSelectionHelp": "Camerele cu obiecte urmărite în acest interval de timp sunt pre-selectate",
+ "checkingActivity": "Se verifică activitatea camerei...",
+ "noCameras": "Nu sunt camere disponibile",
+ "detectionCount_one": "1 obiect urmărit",
+ "detectionCount_few": "{{count}} obiecte urmărite",
+ "detectionCount_other": "{{count}} de obiecte urmărite",
+ "nameLabel": "Nume export",
+ "namePlaceholder": "Nume de bază opțional pentru aceste exporturi",
+ "queueingButton": "Se adaugă exporturile la coadă...",
+ "exportButton_one": "Exportă 1 cameră",
+ "exportButton_few": "Exportă {{count}} camere",
+ "exportButton_other": "Exportă {{count}} de camere"
+ },
+ "multi": {
+ "title_one": "Exportă o revizuire",
+ "title_few": "Exportă {{count}} revizuiri",
+ "title_other": "Exportă {{count}} de revizuiri",
+ "description": "Exportă fiecare revizuire selectată. Toate exporturile vor fi grupate sub un singur caz.",
+ "descriptionNoCase": "Exportă fiecare revizuire selectată.",
+ "caseNamePlaceholder": "Export revizuiri - {{date}}",
+ "exportButton_one": "Exportă o revizuire",
+ "exportButton_few": "Exportă {{count}} revizuiri",
+ "exportButton_other": "Exportă {{count}} de revizuiri",
+ "exportingButton": "Se exportă...",
+ "toast": {
+ "started_one": "A început 1 export. Se deschide cazul acum.",
+ "started_few": "Au început {{count}} exporturi. Se deschide cazul acum.",
+ "started_other": "Au început {{count}} de exporturi. Se deschide cazul acum.",
+ "startedNoCase_one": "A început 1 export.",
+ "startedNoCase_few": "Au început {{count}} exporturi.",
+ "startedNoCase_other": "Au început {{count}} de exporturi.",
+ "partial": "Au început {{successful}} din {{total}} exporturi. Au eșuat: {{failedItems}}",
+ "failed": "Eșec la pornirea a {{total}} exporturi. Au eșuat: {{failedItems}}"
+ }
}
},
"streaming": {
diff --git a/web/public/locales/ro/components/player.json b/web/public/locales/ro/components/player.json
index bbd8ceab88..71cb3cbde9 100644
--- a/web/public/locales/ro/components/player.json
+++ b/web/public/locales/ro/components/player.json
@@ -4,7 +4,8 @@
"noPreviewFoundFor": "Nu există previzualizari pentru {{cameraName}}",
"submitFrigatePlus": {
"title": "Trimiteti acest cadru catre Frigate+?",
- "submit": "Trimite"
+ "submit": "Trimite",
+ "previewError": "Nu s-a putut încărca previzualizarea snapshot-ului. S-ar putea ca înregistrarea să nu fie disponibilă în acest moment."
},
"livePlayerRequiredIOSVersion": "iOS 17.1 sau mai recent este necesar pentru acest tip de stream live.",
"streamOffline": {
@@ -47,5 +48,6 @@
"success": {
"submittedFrigatePlus": "Cadru trimis cu Succes catre Frigate+"
}
- }
+ },
+ "cameraOff": "Camera este oprită"
}
diff --git a/web/public/locales/ro/config/cameras.json b/web/public/locales/ro/config/cameras.json
index 01c256adf4..7c68458241 100644
--- a/web/public/locales/ro/config/cameras.json
+++ b/web/public/locales/ro/config/cameras.json
@@ -13,7 +13,7 @@
"description": "Activată"
},
"audio": {
- "label": "Evenimente audio",
+ "label": "Detecție audio",
"description": "Setări pentru detectarea evenimentelor bazate pe sunet pentru această cameră.",
"enabled": {
"label": "Activare detecție audio",
@@ -29,11 +29,15 @@
},
"listen": {
"label": "Tipuri ascultate",
- "description": "Lista de evenimente audio de detectat (ex: lătrat, alarmă_incendiu, țipăt, vorbire)."
+ "description": "Lista de evenimente audio de detectat (ex: lătrat, alarmă_incendiu, vorbire)."
},
"filters": {
"label": "Filtre audio",
- "description": "Setări de filtrare per tip audio, cum ar fi pragul de încredere."
+ "description": "Setări de filtrare per tip audio, cum ar fi pragul de încredere.",
+ "threshold": {
+ "label": "Încredere audio minimă",
+ "description": "Pragul minim de încredere pentru ca evenimentul audio să fie luat în considerare."
+ }
},
"enabled_in_config": {
"label": "Stare audio originală",
@@ -148,11 +152,11 @@
}
},
"ffmpeg": {
- "label": "FFmpeg",
- "description": "Setări FFmpeg: cale binar, argumente, accelerare hardware și ieșiri per rol.",
+ "label": "Stream-uri (FFmpeg)",
+ "description": "Intrările stream-ului camerei și opțiunile FFmpeg, inclusiv calea către binar, argumentele, hwaccel și argumentele de ieșire per rol.",
"path": {
"label": "Cale FFmpeg",
- "description": "Calea către binarul FFmpeg sau un alias de versiune (\"5.0\" sau \"7.0\")."
+ "description": "Calea către binarul FFmpeg sau un alias de versiune (\"7.0\" sau \"8.0\")."
},
"global_args": {
"label": "Argumente globale FFmpeg",
@@ -485,6 +489,10 @@
"hwaccel_args": {
"label": "Argumente hwaccel export",
"description": "Argumente de accelerare hardware pentru operațiunile de export/transcodare."
+ },
+ "max_concurrent": {
+ "description": "Numărul maxim de sarcini de export de procesat în același timp.",
+ "label": "Număr maxim de exporturi simultane"
}
},
"preview": {
@@ -678,7 +686,7 @@
},
"timestamp_style": {
"label": "Stil timestamp",
- "description": "Opțiuni de stilizare pentru timestamp-ul din flux, aplicate înregistrărilor și snapshot-urilor.",
+ "description": "Opțiuni de stilizare pentru marcajele de timp aplicate snapshot-urilor și vizualizării Debug.",
"position": {
"label": "Poziție timestamp",
"description": "Unde apare data/ora pe imagine (stânga-sus/dreapta-sus etc.)."
@@ -858,6 +866,10 @@
"dashboard": {
"label": "Arată în interfață",
"description": "Comută vizibilitatea acestei camere peste tot în interfața Frigate. Dezactivarea acestei opțiuni va necesita editarea manuală a configurației pentru a vedea din nou camera în interfață."
+ },
+ "review": {
+ "label": "Arată în Revizuire",
+ "description": "Comută dacă această cameră este vizibilă în Rrevizuire (pagina de revizuire și filtrul ei de camere, revizuirea mișcărilor și vizualizarea istoricului)."
}
},
"webui_url": {
diff --git a/web/public/locales/ro/config/global.json b/web/public/locales/ro/config/global.json
index d07e3bab4b..1dca8a48ae 100644
--- a/web/public/locales/ro/config/global.json
+++ b/web/public/locales/ro/config/global.json
@@ -1,6 +1,6 @@
{
"audio": {
- "label": "Evenimente audio",
+ "label": "Detecție audio",
"enabled": {
"label": "Activare detecție audio",
"description": "Activează sau dezactivează detecția audio pentru toate camerele."
@@ -15,11 +15,15 @@
},
"listen": {
"label": "Tipuri ascultate",
- "description": "Lista de evenimente audio de detectat (ex: lătrat, alarmă_incendiu, țipăt, vorbire)."
+ "description": "Lista de evenimente audio de detectat (ex: lătrat, alarmă_incendiu, vorbire)."
},
"filters": {
"label": "Filtre audio",
- "description": "Setări de filtrare per tip audio, cum ar fi pragul de încredere."
+ "description": "Setări de filtrare per tip audio, cum ar fi pragul de încredere.",
+ "threshold": {
+ "label": "Încredere audio minimă",
+ "description": "Pragul minim de încredere pentru ca evenimentul audio să fie luat în considerare."
+ }
},
"enabled_in_config": {
"label": "Stare audio originală",
@@ -216,7 +220,7 @@
"description": "Setări FFmpeg: cale binar, argumente, accelerare hardware și ieșiri per rol.",
"path": {
"label": "Cale FFmpeg",
- "description": "Calea către binarul FFmpeg sau un alias de versiune (\"5.0\" sau \"7.0\")."
+ "description": "Calea către binarul FFmpeg sau un alias de versiune (\"7.0\" sau \"8.0\")."
},
"global_args": {
"label": "Argumente globale FFmpeg",
@@ -512,6 +516,41 @@
"label": "Stare GenAI originală",
"description": "Indică dacă GenAI a fost activat în configurația inițială."
}
+ },
+ "filters_attribute": {
+ "label": "Filtre de atribute",
+ "description": "Filtre aplicate atributelor detectate pentru a reduce rezultatele fals pozitive (arie, raport, încredere).",
+ "min_area": {
+ "label": "Aria minimă a atributului",
+ "description": "Aria minimă a casetei de încadrare (pixeli sau procentaj) necesară pentru acest atribut. Poate fi în pixeli (int) sau procentaj (între 0.000001 și 0.99)."
+ },
+ "max_area": {
+ "label": "Aria maximă a atributului",
+ "description": "Aria minimă a casetei de încadrare (pixeli sau procentaj) necesară pentru acest atribut. Poate fi în pixeli (int) sau procentaj (între 0.000001 și 0.99)."
+ },
+ "min_ratio": {
+ "label": "Raport de aspect minim",
+ "description": "Raportul minim lățime/înălțime necesar pentru ca o casetă de încadrare să fie validă."
+ },
+ "max_ratio": {
+ "label": "Raport de aspect maxim",
+ "description": "Raportul maxim lățime/înălțime permis pentru ca o casetă de încadrare să fie validă."
+ },
+ "threshold": {
+ "label": "Prag de încredere",
+ "description": "Pragul mediu de încredere a detecției necesar pentru ca atributul să fie considerat un rezultat adevărat pozitiv."
+ },
+ "min_score": {
+ "label": "Încredere minimă",
+ "description": "Încrederea minimă de detecție pe un singur cadru necesară pentru a asocia acest atribut cu obiectul său părinte."
+ },
+ "mask": {
+ "label": "Mască de filtrare",
+ "description": "Coordonatele poligonului care definesc unde se aplică acest filtru în cadru."
+ },
+ "raw_mask": {
+ "label": "Mască brută"
+ }
}
},
"record": {
@@ -594,6 +633,10 @@
"hwaccel_args": {
"label": "Argumente hwaccel export",
"description": "Argumente de accelerare hardware pentru operațiunile de export/transcodare."
+ },
+ "max_concurrent": {
+ "description": "Numărul maxim de sarcini de export de procesat în același timp.",
+ "label": "Număr maxim de exporturi simultane"
}
},
"preview": {
@@ -1131,7 +1174,7 @@
},
"default_role": {
"label": "Rol implicit",
- "description": "Rolul implicit atribuit utilizatorilor autentificați prin proxy când nu se aplică nicio mapare de rol (admin sau viewer)."
+ "description": "Rolul implicit atribuit utilizatorilor autentificați prin proxy când nu se aplică nicio mapare de rol."
},
"separator": {
"label": "Caracter separator",
@@ -1161,8 +1204,8 @@
"description": "Activează monitorizarea lățimii de bandă a rețelei pe proces pentru procesele ffmpeg ale camerelor și detectoare (necesită capabilități)."
},
"intel_gpu_device": {
- "label": "Dispozitiv SR-IOV",
- "description": "Identificator de dispozitiv folosit când GPU-urile Intel sunt tratate ca SR-IOV pentru a repara statisticile GPU."
+ "label": "Dispozitiv GPU Intel",
+ "description": "Adresa magistralei PCI sau calea dispozitivului DRM (ex./dev/dri/card1) folosită pentru a fixa statisticile GPU Intel la un anumit dispozitiv când sunt prezente mai multe."
}
},
"version_check": {
@@ -2161,7 +2204,7 @@
},
"roles": {
"label": "Roluri",
- "description": "Roluri GenAI (unelte, viziune, înglobări); un furnizor per rol."
+ "description": "Roluri GenAI (chat, descrieri, înglobări); un furnizor per rol."
},
"provider_options": {
"label": "Opțiuni furnizor",
@@ -2294,6 +2337,10 @@
"dashboard": {
"label": "Arată în interfață",
"description": "Comută dacă această cameră este vizibilă peste tot în interfața Frigate. Dezactivarea acestei opțiuni va necesita editarea manuală a config-ului pentru a vedea din nou camera în interfață."
+ },
+ "review": {
+ "label": "Arată în Revizuire",
+ "description": "Comută dacă această cameră este vizibilă în Revizuire (pagina de revizuire și filtrul ei de camere, revizuirea mișcărilor și vizualizarea istoricului)."
}
},
"profiles": {
diff --git a/web/public/locales/ro/config/validation.json b/web/public/locales/ro/config/validation.json
index 3ec9691f6e..4f0dbab781 100644
--- a/web/public/locales/ro/config/validation.json
+++ b/web/public/locales/ro/config/validation.json
@@ -28,5 +28,8 @@
"detectRequired": "Cel puțin un stream trebuie să aibă atribuit rolul 'detect'.",
"hwaccelDetectOnly": "Doar stream-ul cu rolul 'detect' poate defini argumente pentru accelerare hardware."
}
+ },
+ "detect": {
+ "dimensionMustBeEven": "Trebuie să fie un număr par."
}
}
diff --git a/web/public/locales/ro/objects.json b/web/public/locales/ro/objects.json
index 90dfc34cb7..122244d5c4 100644
--- a/web/public/locales/ro/objects.json
+++ b/web/public/locales/ro/objects.json
@@ -121,5 +121,10 @@
"royal_mail": "Royal Mail",
"school_bus": "Autobus Scolar",
"skunk": "Sconcs",
- "kangaroo": "Cangur"
+ "kangaroo": "Cangur",
+ "baby": "Bebeluș",
+ "baby_stroller": "Cărucior de copii",
+ "rickshaw": "Ricșă",
+ "Rodent": "Rozătoare",
+ "rodent": "Rozătoare"
}
diff --git a/web/public/locales/ro/views/chat.json b/web/public/locales/ro/views/chat.json
new file mode 100644
index 0000000000..36a892cee1
--- /dev/null
+++ b/web/public/locales/ro/views/chat.json
@@ -0,0 +1,72 @@
+{
+ "documentTitle": "Chat - Frigate",
+ "title": "Chat Frigate",
+ "subtitle": "Asistentul tău AI pentru gestionarea camerelor și informații",
+ "placeholder": "Întreabă orice...",
+ "error": "Ceva a mers prost. Te rog încearcă din nou.",
+ "processing": "Procesare...",
+ "toolsUsed": "Folosit: {{tools}}",
+ "showTools": "Arată uneltele ({{count}})",
+ "hideTools": "Ascunde uneltele",
+ "call": "Apelează",
+ "result": "Rezultat",
+ "arguments": "Argumente:",
+ "response": "Răspuns:",
+ "attachment_chip_label": "{{label}} pe {{camera}}",
+ "attachment_chip_remove": "Elimină atașamentul",
+ "open_in_explore": "Deschide în Explorare",
+ "attach_event_aria": "Atașează evenimentul {{eventId}}",
+ "attachment_picker_paste_label": "Sau lipește ID-ul evenimentului",
+ "attachment_picker_attach": "Atașează",
+ "attachment_picker_placeholder": "Atașează un eveniment",
+ "quick_reply_find_similar": "Găsește apariții similare",
+ "quick_reply_tell_me_more": "Spune-mi mai multe despre asta",
+ "quick_reply_when_else": "Când a mai fost văzut?",
+ "quick_reply_find_similar_text": "Găsește apariții similare cu aceasta.",
+ "quick_reply_tell_me_more_text": "Spune-mi mai multe despre acesta.",
+ "quick_reply_when_else_text": "Când a mai fost văzut acesta?",
+ "anchor": "Referință",
+ "similarity_score": "Similaritate",
+ "no_similar_objects_found": "Nu au fost găsite obiecte similare.",
+ "semantic_search_required": "Căutarea semantică trebuie să fie activată pentru a găsi obiecte similare.",
+ "send": "Trimite",
+ "suggested_requests": "Încearcă să întrebi:",
+ "starting_requests": {
+ "show_recent_events": "Arată evenimentele recente",
+ "show_camera_status": "Arată starea camerei",
+ "recap": "Ce s-a întâmplat cât am fost plecat?",
+ "watch_camera": "Urmărește o cameră pentru activitate"
+ },
+ "starting_requests_prompts": {
+ "show_recent_events": "Arată-mi evenimentele recente din ultima oră",
+ "show_camera_status": "Care este starea actuală a camerelor mele?",
+ "recap": "Ce s-a întâmplat cât am fost plecat?",
+ "watch_camera": "Urmărește ușa din față și anunță-mă dacă apare cineva"
+ },
+ "new_chat": "Chat nou",
+ "settings": {
+ "title": "Setări chat",
+ "show_stats": {
+ "title": "Afișează statistici",
+ "desc": "Afișează rata de generare și dimensiunea contextului pentru răspunsurile de chat.",
+ "always": "Întotdeauna",
+ "while_generating": "În timpul generării"
+ },
+ "auto_scroll": {
+ "title": "Derulare automată",
+ "desc": "Urmărește mesajele noi pe măsură ce sosesc."
+ }
+ },
+ "stats": {
+ "tokens_per_second": "{{rate}} t/s",
+ "context": "{{tokens}} token-uri"
+ },
+ "reasoning": {
+ "active": "Raționament…",
+ "show": "Afișează raționamentul",
+ "hide": "Ascunde raționamentul"
+ },
+ "thinking": {
+ "toggle": "Comută gândirea"
+ }
+}
diff --git a/web/public/locales/ro/views/events.json b/web/public/locales/ro/views/events.json
index 455257a92d..5adb14169c 100644
--- a/web/public/locales/ro/views/events.json
+++ b/web/public/locales/ro/views/events.json
@@ -25,7 +25,9 @@
},
"documentTitle": "Revizuieste - Frigate",
"recordings": {
- "documentTitle": "Inregistrari - frigate"
+ "documentTitle": "Inregistrari - frigate",
+ "invalidSharedLink": "Nu s-a putut deschide linkul înregistrării cu timestamp din cauza unei erori de parsare.",
+ "invalidSharedCamera": "Nu s-a putut deschide linkul înregistrării cu timestamp din cauza unei camere necunoscute sau neautorizate."
},
"calendarFilter": {
"last24Hours": "Ultimele 24 de ore"
@@ -39,7 +41,7 @@
"camera": "Camera foto",
"detections": "Detecții",
"detected": "detectat",
- "selected_one": "{{count}} selectate",
+ "selected_one": "{{count}} selectat",
"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 5d4057b0b2..afdb9d8b65 100644
--- a/web/public/locales/ro/views/explore.json
+++ b/web/public/locales/ro/views/explore.json
@@ -229,7 +229,7 @@
"aria": "Descarcă snapshot curat"
},
"debugReplay": {
- "label": "Reluare de depanare",
+ "label": "Reluare depanare",
"aria": "Vezi acest obiect urmărit în vizualizarea de reluare de depanare"
},
"more": {
@@ -289,7 +289,10 @@
"zones": "Zone",
"ratio": "Raport",
"area": "Aria",
- "score": "Scor"
+ "score": "Scor",
+ "computedScore": "Scor calculat",
+ "topScore": "Cel mai mare scor",
+ "toggleAdvancedScores": "Comută scorurile avansate"
}
},
"annotationSettings": {
diff --git a/web/public/locales/ro/views/exports.json b/web/public/locales/ro/views/exports.json
index 1b1e0b2d87..be984037f2 100644
--- a/web/public/locales/ro/views/exports.json
+++ b/web/public/locales/ro/views/exports.json
@@ -14,7 +14,9 @@
"toast": {
"error": {
"renameExportFailed": "Redenumirea exportului a eșuat: {{errorMessage}}",
- "assignCaseFailed": "Actualizarea atribuirii cazului a eșuat: {{errorMessage}}"
+ "assignCaseFailed": "Actualizarea atribuirii cazului a eșuat: {{errorMessage}}",
+ "caseSaveFailed": "Salvarea cazului a eșuat: {{errorMessage}}",
+ "caseDeleteFailed": "Ștergerea cazului a eșuat: {{errorMessage}}"
}
},
"tooltip": {
@@ -22,7 +24,8 @@
"downloadVideo": "Descarcă video",
"editName": "Editează numele",
"deleteExport": "Șterge exportul",
- "assignToCase": "Adaugă la un caz"
+ "assignToCase": "Adaugă la un caz",
+ "removeFromCase": "Elimină din caz"
},
"headings": {
"cases": "Cazuri",
@@ -35,5 +38,91 @@
"newCaseOption": "Creează un caz nou",
"nameLabel": "Numele cazului",
"descriptionLabel": "Descriere"
+ },
+ "toolbar": {
+ "newCase": "Caz nou",
+ "addExport": "Adaugă export",
+ "editCase": "Editează cazul",
+ "deleteCase": "Șterge cazul"
+ },
+ "deleteCase": {
+ "label": "Șterge cazul",
+ "desc": "Sigur vrei să ștergi {{caseName}}?",
+ "descKeepExports": "Exporturile vor rămâne disponibile ca exporturi necategorizate.",
+ "descDeleteExports": "Toate exporturile din acest caz vor fi șterse definitiv.",
+ "deleteExports": "Șterge și exporturile"
+ },
+ "caseCard": {
+ "emptyCase": "Niciun export încă"
+ },
+ "jobCard": {
+ "defaultName": "Export {{camera}}",
+ "queued": "În așteptare",
+ "running": "În rulare",
+ "preparing": "Pregătire",
+ "copying": "Copiere",
+ "encoding": "Codare",
+ "encodingRetry": "Codare (reîncercare)",
+ "finalizing": "Finalizare"
+ },
+ "caseView": {
+ "noDescription": "Fără descriere",
+ "createdAt": "Creat {{value}}",
+ "exportCount_one": "1 export",
+ "exportCount_other": "{{count}} exporturi",
+ "cameraCount_one": "1 cameră",
+ "cameraCount_other": "{{count}} camere",
+ "showMore": "Afișează mai mult",
+ "showLess": "Afișează mai puțin",
+ "emptyTitle": "Acest caz este gol",
+ "emptyDescription": "Adaugă exporturile necategorizate existente pentru a menține cazul organizat.",
+ "emptyDescriptionNoExports": "Nu există încă exporturi necategorizate disponibile pentru a fi adăugate."
+ },
+ "caseEditor": {
+ "createTitle": "Creează caz",
+ "editTitle": "Editează cazul",
+ "namePlaceholder": "Nume caz",
+ "descriptionPlaceholder": "Adaugă note sau context pentru acest caz"
+ },
+ "addExportDialog": {
+ "title": "Adaugă export la {{caseName}}",
+ "searchPlaceholder": "Caută exporturi necategorizate",
+ "empty": "Niciun export necategorizat nu se potrivește cu această căutare.",
+ "addButton_one": "Adaugă 1 export",
+ "addButton_other": "Adaugă {{count}} exporturi",
+ "adding": "Se adaugă..."
+ },
+ "selected_one": "{{count}} selectat",
+ "selected_other": "{{count}} selectate",
+ "bulkActions": {
+ "addToCase": "Adaugă la caz",
+ "moveToCase": "Mută la caz",
+ "removeFromCase": "Elimină din caz",
+ "delete": "Șterge",
+ "deleteNow": "Șterge acum"
+ },
+ "bulkDelete": {
+ "title": "Șterge exporturile",
+ "desc_one": "Sigur vrei să ștergi {{count}} export?",
+ "desc_other": "Sigur vrei să ștergi {{count}} exporturi?"
+ },
+ "bulkRemoveFromCase": {
+ "title": "Elimină din caz",
+ "desc_one": "Elimini {{count}} export din acest caz?",
+ "desc_other": "Elimini {{count}} exporturi din acest caz?",
+ "descKeepExports": "Exporturile vor fi mutate la necategorizate.",
+ "descDeleteExports": "Exporturile vor fi șterse definitiv.",
+ "deleteExports": "Șterge exporturile în schimb"
+ },
+ "bulkToast": {
+ "success": {
+ "delete": "Exporturile au fost șterse cu succes",
+ "reassign": "Alocarea cazului a fost actualizată cu succes",
+ "remove": "Exporturile au fost eliminate din caz cu succes"
+ },
+ "error": {
+ "deleteFailed": "Ștergerea exporturilor a eșuat: {{errorMessage}}",
+ "reassignFailed": "Actualizarea alocării cazului a eșuat: {{errorMessage}}"
+ }
}
}
diff --git a/web/public/locales/ro/views/faceLibrary.json b/web/public/locales/ro/views/faceLibrary.json
index 15979a6c7a..a6227cdc6b 100644
--- a/web/public/locales/ro/views/faceLibrary.json
+++ b/web/public/locales/ro/views/faceLibrary.json
@@ -30,7 +30,11 @@
"empty": "Nu există încercări recente de recunoaștere facială",
"title": "Recunoașteri Recente",
"aria": "Selectează Recunoașteri Recente",
- "titleShort": "Recent"
+ "titleShort": "Recent",
+ "emptyNoLibrary": {
+ "title": "Încarcă o față",
+ "description": "Trebuie să adaugi cel puțin o față în librărie pentru ca recunoașterea facială să funcționeze."
+ }
},
"steps": {
"description": {
diff --git a/web/public/locales/ro/views/live.json b/web/public/locales/ro/views/live.json
index 6b8c8c979e..971d3f32ea 100644
--- a/web/public/locales/ro/views/live.json
+++ b/web/public/locales/ro/views/live.json
@@ -58,7 +58,9 @@
},
"camera": {
"enable": "Activează camera",
- "disable": "Dezactivează camera"
+ "disable": "Dezactivează camera",
+ "turnOn": "Camera este pornittă",
+ "turnOff": "Oprește camera"
},
"muteCameras": {
"enable": "Dezactivează sunetul pentru toate camerele",
@@ -70,7 +72,8 @@
},
"recording": {
"enable": "Activează înregistrarea",
- "disable": "Dezactivează înregistrarea"
+ "disable": "Dezactivează înregistrarea",
+ "disabledInConfig": "Înregistrarea trebuie mai întâi activată în Setări pentru această cameră."
},
"snapshots": {
"disable": "Dezactivează snapshoturile",
@@ -150,7 +153,8 @@
"snapshots": "Snapshot-uri",
"audioDetection": "Detectare sunet",
"autotracking": "Urmărire automată",
- "transcription": "Transcriere audio"
+ "transcription": "Transcriere audio",
+ "camera": "Cameră"
},
"history": {
"label": "Afișează înregistrările istorice"
diff --git a/web/public/locales/ro/views/motionSearch.json b/web/public/locales/ro/views/motionSearch.json
new file mode 100644
index 0000000000..1b455df083
--- /dev/null
+++ b/web/public/locales/ro/views/motionSearch.json
@@ -0,0 +1,82 @@
+{
+ "documentTitle": "Căutare mișcare - Frigate",
+ "title": "Căutare mișcare",
+ "description": "Desenează un poligon pentru a defini regiunea de interes și specifică un interval de timp pentru a căuta schimbări de mișcare în acea regiune.",
+ "selectCamera": "Căutarea de mișcare se încarcă",
+ "startSearch": "Începe căutarea",
+ "searchStarted": "Căutarea a început",
+ "searchCancelled": "Căutare anulată",
+ "cancelSearch": "Anulează",
+ "searching": "Căutare în curs.",
+ "searchComplete": "Căutare finalizată",
+ "noResultsYet": "Rulează o căutare pentru a găsi schimbări de mișcare în regiunea selectată",
+ "noChangesFound": "Nu au fost detectate schimbări de pixeli în regiunea selectată",
+ "changesFound_one": "Am găsit {{count}} schimbare de mișcare",
+ "changesFound_few": "Am găsit {{count}} schimbări de mișcare",
+ "changesFound_other": "Am găsit {{count}} de schimbări de mișcare",
+ "framesProcessed": "{{count}} cadre procesate",
+ "jumpToTime": "Sari la acest timp",
+ "results": "Rezultate",
+ "showSegmentHeatmap": "Hartă termică",
+ "newSearch": "Căutare nouă",
+ "clearResults": "Curăță rezultatele",
+ "clearROI": "Curăță poligonul",
+ "polygonControls": {
+ "points_one": "{{count}} punct",
+ "points_few": "{{count}} puncte",
+ "points_other": "{{count}} de puncte",
+ "undo": "Anulează ultimul punct",
+ "reset": "Resetează poligonul",
+ "moveMode": "Mută",
+ "drawMode": "Desenează"
+ },
+ "motionHeatmapLabel": "Harta termică a mișcării",
+ "dialog": {
+ "title": "Căutare mișcare",
+ "cameraLabel": "Cameră",
+ "previewAlt": "Previzualizarea camerei pentru {{camera}}"
+ },
+ "timeRange": {
+ "title": "Interval de căutare",
+ "start": "Timp de început",
+ "end": "Timp de sfârșit"
+ },
+ "settings": {
+ "title": "Setări de căutare",
+ "parallelMode": "Mod paralel",
+ "parallelModeDesc": "Scanează mai multe intervale de înregistrare în același timp (mai rapid; utilizează mai multe resurse de decodare)",
+ "threshold": "Prag de sensibilitate",
+ "thresholdDesc": "Valorile mai mici detectează schimbări mai mici (1-255)",
+ "minArea": "Arie minimă de schimbare",
+ "minAreaDesc": "Dimensiunea minimă a unei singure regiuni în mișcare, ca procent din regiunea de interes",
+ "frameSkip": "Omitere cadre",
+ "frameSkipDesc": "Procesează fiecare al N-lea cadru. Setează asta la rata de cadre a camerei tale pentru a procesa un cadru pe secundă (ex. 5 pentru o cameră de 5 FPS, 30 pentru o cameră de 30 FPS). Valorile mai mari vor fi mai rapide, dar pot rata evenimente scurte de mișcare.",
+ "maxResults": "Rezultate maxime",
+ "maxResultsDesc": "Oprește-te după acest număr de marcaje de timp potrivite"
+ },
+ "errors": {
+ "noCamera": "Te rog selectează o cameră",
+ "noROI": "Te rog desenează o regiune de interes",
+ "noTimeRange": "Te rog selectează un interval de timp",
+ "invalidTimeRange": "Timpul de sfârșit trebuie să fie după timpul de început",
+ "searchFailed": "Căutarea a eșuat: {{message}}",
+ "polygonTooSmall": "Poligonul trebuie să aibă cel puțin 3 puncte",
+ "unknown": "Eroare necunoscută"
+ },
+ "changePercentage": "{{percentage}}% schimbat",
+ "metrics": {
+ "title": "Metrici de căutare",
+ "segmentsScanned": "Segmente scanate",
+ "segmentsProcessed": "Procesat",
+ "segmentsSkippedInactive": "Omis (fără activitate)",
+ "segmentsSkippedHeatmap": "Omis (fără suprapunere ROI)",
+ "fallbackFullRange": "Scanare completă de rezervă",
+ "framesDecoded": "Cadre decodate",
+ "wallTime": "Timp de căutare",
+ "segmentErrors": "Erori segment",
+ "seconds": "{{seconds}}s",
+ "minutesSeconds": "{{minutes}}m {{seconds}}s",
+ "scanSummary": "{{segments}} segmente · {{time}}"
+ },
+ "scanning": "Scanare {{time}}"
+}
diff --git a/web/public/locales/ro/views/replay.json b/web/public/locales/ro/views/replay.json
new file mode 100644
index 0000000000..b3c854f742
--- /dev/null
+++ b/web/public/locales/ro/views/replay.json
@@ -0,0 +1,59 @@
+{
+ "title": "Reluare de depanare",
+ "description": "Redă înregistrările camerei pentru depanare. Lista de obiecte arată un rezumat decalat în timp al obiectelor detectate, iar tab-ul Mesaje arată un flux de mesaje interne ale Frigate din înregistrarea redată.",
+ "websocket_messages": "Mesaje",
+ "dialog": {
+ "title": "Pornește reluarea de depanare",
+ "description": "Creează o cameră temporară de reluare care rulează în buclă înregistrări istorice pentru depanarea problemelor de detecție și urmărire a obiectelor. Camera de reluare va avea aceeași configurație de detecție ca și camera sursă. Alege un interval de timp pentru a începe.",
+ "camera": "Cameră sursă",
+ "timeRange": "Interval de timp",
+ "preset": {
+ "1m": "Ultimul minut",
+ "5m": "Ultimele 5 minute",
+ "timeline": "Din cronologie",
+ "custom": "Personalizat"
+ },
+ "startButton": "Începe reluarea",
+ "selectFromTimeline": "Selectează",
+ "starting": "Pornire reluare...",
+ "startLabel": "Început",
+ "endLabel": "Sfârșit",
+ "toast": {
+ "error": "Pornirea reluării de depanare a eșuat: {{error}}",
+ "alreadyActive": "O sesiune de reluare este deja activă",
+ "stopError": "Oprirea reluării de depanare a eșuat: {{error}}",
+ "goToReplay": "Mergi la reluare"
+ }
+ },
+ "page": {
+ "noSession": "Nicio sesiune de reluare de depanare activă",
+ "noSessionDesc": "Pornește o reluare de depanare din vizualizarea Istoric dând click pe butonul Acțiuni din bara de instrumente și alegând Reluare depanare.",
+ "goToRecordings": "Mergi la istoric",
+ "preparingClip": "Pregătire clip…",
+ "preparingClipDesc": "Frigate îmbină înregistrările pentru intervalul de timp selectat. Acest lucru poate dura un minut pentru intervale mai mari.",
+ "startingCamera": "Pornire reluare depanare…",
+ "startError": {
+ "title": "Pornirea reluării de depanare a eșuat",
+ "back": "Înapoi la istoric"
+ },
+ "sourceCamera": "Camera sursă",
+ "replayCamera": "Camera de reluare",
+ "initializingReplay": "Inițializare reluare depanare...",
+ "stoppingReplay": "Oprire reluare depanare...",
+ "stopReplay": "Oprește reluarea",
+ "confirmStop": {
+ "title": "Oprești reluarea de depanare?",
+ "description": "Aceasta va opri sesiunea și va șterge toate datele temporare. Ești sigur?",
+ "confirm": "Oprește reluarea",
+ "cancel": "Anulează"
+ },
+ "activity": "Activitate",
+ "objects": "Listă de obiecte",
+ "audioDetections": "Detecții audio",
+ "noActivity": "Nicio activitate detectată",
+ "activeTracking": "Urmărire activă",
+ "noActiveTracking": "Nicio urmărire activă",
+ "configuration": "Configurație",
+ "configurationDesc": "Ajustează setările de detecție a mișcării și urmărire a obiectelor pentru camera de reluare de depanare. Nicio modificare nu este salvată în fișierul tău de configurare Frigate."
+ }
+}
diff --git a/web/public/locales/ro/views/settings.json b/web/public/locales/ro/views/settings.json
index 632f6137eb..c47c028377 100644
--- a/web/public/locales/ro/views/settings.json
+++ b/web/public/locales/ro/views/settings.json
@@ -16,7 +16,8 @@
"globalConfig": "Configurație Globală - Frigate",
"cameraConfig": "Configurație Cameră - Frigate",
"maintenance": "Mentenanță - Frigate",
- "profiles": "Profile - Frigate"
+ "profiles": "Profile - Frigate",
+ "detectorsAndModel": "Detectoare și model - Frigate"
},
"menu": {
"ui": "Interfață (UI)",
@@ -30,8 +31,8 @@
"frigateplus": "Frigate+",
"triggers": "Declanșatori",
"roles": "Roluri",
- "cameraManagement": "Gestionare",
- "cameraReview": "Recenzie",
+ "cameraManagement": "Gestionare cameră",
+ "cameraReview": "Revizuire",
"general": "General",
"globalConfig": "Configurație globală",
"system": "Sistem",
@@ -43,8 +44,8 @@
"globalFfmpeg": "FFmpeg",
"globalMotion": "Detecție mișcare",
"globalObjects": "Obiecte",
- "globalReview": "Recenzie",
- "globalAudioEvents": "Evenimente audio",
+ "globalReview": "Revizuire",
+ "globalAudioEvents": "Detecție audio",
"globalLivePlayback": "Redare live",
"globalTimestampStyle": "Stil timestamp",
"systemDatabase": "Bază de date",
@@ -68,13 +69,13 @@
"integrationObjectClassification": "Clasificare obiecte",
"integrationAudioTranscription": "Transcriere audio",
"cameraDetect": "Detecție obiecte",
- "cameraFfmpeg": "FFmpeg",
+ "cameraFfmpeg": "Stream-uri (FFmpeg)",
"cameraRecording": "Înregistrare",
"cameraSnapshots": "Snapshot-uri",
"cameraMotion": "Detecție mișcare",
"cameraObjects": "Obiecte",
- "cameraConfigReview": "Recenzie",
- "cameraAudioEvents": "Evenimente audio",
+ "cameraConfigReview": "Revizuire",
+ "cameraAudioEvents": "Detecție audio",
"cameraAudioTranscription": "Transcriere audio",
"cameraNotifications": "Notificări",
"cameraLivePlayback": "Redare live",
@@ -91,7 +92,8 @@
"regionGrid": "Grilă regiune",
"uiSettings": "Setări UI",
"profiles": "Profile",
- "systemGo2rtcStreams": "stream-uri go2rtc"
+ "systemGo2rtcStreams": "stream-uri go2rtc",
+ "systemDetectorsAndModel": "Detectori și model"
},
"dialog": {
"unsavedChanges": {
@@ -145,7 +147,7 @@
"title": "Calendar",
"firstWeekday": {
"label": "Prima zi a săptămânii",
- "desc": "Ziua cu care încep săptămânile în calendarul de recenzii.",
+ "desc": "Ziua cu care încep săptămânile în calendarul de revizuire.",
"sunday": "Duminică",
"monday": "Luni"
}
@@ -590,7 +592,7 @@
"admin": "Administrator",
"adminDesc": "Acces complet la toate funcțiile.",
"viewer": "Vizualizator",
- "viewerDesc": "Limitat la tablouri de bord Live, Recenzii, Explorare și Exporturi.",
+ "viewerDesc": "Limitat la tablouri de bord Live, Revizuire, Explorare și Exporturi.",
"customDesc": "Rol personalizat cu acces la camere specifice."
},
"select": "Selectează un rol",
@@ -710,7 +712,8 @@
"notificationUnavailable": {
"documentation": "Citește documentația",
"desc": "Notificările push web necesită un context securizat (https://…). Aceasta este o limitare a browserului. Accesează Frigate în mod securizat pentru a utiliza notificările.",
- "title": "Notificări Indisponibile"
+ "title": "Notificări Indisponibile",
+ "descPwa": "Pe iOS, notificările web push sunt disponibile doar când Frigate este instalat pe ecranul principal. Deschide meniul Partajare , alege Adaugă pe ecranul principal , apoi deschide Frigate din noua pictogramă pentru a înregistra acest dispozitiv pentru notificări."
},
"cameras": {
"title": "Camere",
@@ -778,10 +781,18 @@
"baseModel": "Model de Bază",
"loading": "Se încarcă informațiile despre model…",
"error": "Eroare la încărcarea informațiilor despre model",
- "availableModels": "Modele Disponibile",
+ "availableModels": "Modele Frigate+ disponibile",
"modelType": "Tip Model",
"trainDate": "Data Antrenării",
- "cameras": "Camere"
+ "cameras": "Camere",
+ "noModelLoaded": "Niciun model Frigate+ nu este încărcat în prezent.",
+ "selectModel": "Selectează un model",
+ "noModelsAvailable": "Niciun model disponibil",
+ "filter": {
+ "ariaLabel": "Filtrează modelele după tip",
+ "baseModels": "Modele de bază",
+ "fineTunedModels": "Modele optimizate"
+ }
},
"toast": {
"error": "Eroare la salvarea modificărilor de config: {{errorMessage}}",
@@ -796,7 +807,8 @@
"currentModel": "Model Actual",
"otherModels": "Alte Modele",
"configuration": "Configurație"
- }
+ },
+ "changeInDetectorsAndModel": "Schimbă modelul"
},
"motionDetectionTuner": {
"unsavedChanges": "Modificări nesalvate la reglajul de mișcare ({{camera}})",
@@ -1066,7 +1078,7 @@
"brands": {
"reolink-rtsp": "RTSP Reolink nu este recomandat. Activează HTTP în setările firmware ale camerei și repornește asistentul."
},
- "customUrlRtspRequired": "URL-urile personalizate trebuie să înceapă cu „rtsp://”. Configurarea manuală este necesară pentru stream-urile care nu sunt RTSP."
+ "customUrlRtspRequired": "URL-urile personalizate trebuie să înceapă cu „rtsp://” sau „rtsps://”. Configurarea manuală este necesară pentru stream-urile care nu sunt RTSP."
},
"docs": {
"reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras"
@@ -1282,7 +1294,8 @@
},
"hikvision": {
"substreamWarning": "Substream 1 este limitat la o rezoluție mică. Multe camere Hikvision suportă substream-uri adiționale care trebuie activate din setările camerei. Se recomandă verificarea și utilizarea acestora."
- }
+ },
+ "resolutionUnknown": "Rezoluția acestui stream nu a putut fi sondată. Ar trebui să setezi manual rezoluția de detecție în Setări sau în configurația ta."
}
}
},
@@ -1293,13 +1306,45 @@
"selectCamera": "Selectează o Cameră",
"backToSettings": "Înapoi la Setări Cameră",
"streams": {
- "title": "Activează / Dezactivează Camere",
+ "title": "Stare și detalii cameră",
"desc": "Dezactivează temporar o cameră până la repornirea Frigate. Dezactivarea unei camere oprește complet procesarea streamingului acestei camere de către Frigate. Detecția, înregistrarea și depanarea vor fi indisponibile. Notă: Aceasta nu dezactivează restreamingul go2rtc. ",
"enableLabel": "Camere activate",
- "enableDesc": "Dezactivează temporar o cameră până la repornirea Frigate. Dezactivarea oprește procesarea stream-urilor pentru această cameră. Detecția, înregistrarea și depanarea vor fi indisponibile. Notă: Acest lucru nu dezactivează restream-urile go2rtc. ",
+ "enableDesc": "Dezactivează temporar o cameră activată până la repornirea Frigate. Dezactivarea unei camere oprește complet procesarea de către Frigate a fluxurilor acestei camere. Detectarea, înregistrarea și depanarea vor fi indisponibile. Notă: Acest lucru nu dezactivează retransmisiile go2rtc.. Trage de mâner pentru a reordona camerele așa cum apar în interfață. Ordinea camerelor activate va fi reflectată în întreaga interfață, inclusiv în tabloul de bord Live și în meniurile derulante pentru selectarea camerelor.",
"disableLabel": "Camere dezactivate",
"disableDesc": "Activează o cameră care este ascunsă în interfață și dezactivată în configurație. Este necesară repornirea Frigate după activare.",
- "enableSuccess": "Am activat {{cameraName}} în configurație. Repornește Frigate pentru a aplica modificările."
+ "enableSuccess": "S-a activat {{cameraName}}. Repornește Frigate pentru a aplica.",
+ "friendlyName": {
+ "edit": "Editează numele afișat al camerei",
+ "title": "Editează numele afișat",
+ "description": "Setează numele afișat pentru această cameră în întreaga interfață Frigate. Lasă necompletat pentru a folosi ID-ul camerei.",
+ "rename": "Redenumește"
+ },
+ "reorderHandle": "Trage pentru a reordona",
+ "saving": "Se salvează…",
+ "saved": "Salvat",
+ "details": {
+ "edit": "Editează detaliile camerei",
+ "title": "Editează detaliile camerei",
+ "description": "Actualizează numele de afișare, URL-ul extern și vizibilitatea folosite pentru această cameră în tot UI-ul Frigate.",
+ "friendlyNameLabel": "Nume afișat",
+ "friendlyNameHelp": "Numele prietenos afișat pentru această cameră în întreaga interfață Frigate. Lasă gol pentru a utiliza ID-ul camerei.",
+ "webuiUrlLabel": "URL-ul interfeței web a camerei",
+ "webuiUrlHelp": "URL pentru a vizita interfața web a camerei direct din vizualizarea Depanare (Debug). Lasă gol pentru a dezactiva linkul.",
+ "webuiUrlInvalid": "Trebuie să fie un URL valid (de exemplu, https://exemplu.com).",
+ "dashboardLabel": "Arată pe dashboard-ul Live",
+ "reviewLabel": "Arată în Revizuire",
+ "dashboardHelp": "Arată această cameră pe dashboard-ul Live.",
+ "reviewHelp": "Arată această cameră în revizuiri, inclusiv filtrul de camere, revizuirea mișcărilor și vizualizarea istoricului."
+ },
+ "label": "Stare cameră",
+ "description": "Setează starea de funcționare pentru fiecare cameră.Pornit : stream-urile sunt procesate normal.Oprit : pune temporar pe pauză procesarea. Nu se menține după repornirile Frigate.Dezactivat : oprește procesarea și salvează modificarea în configurația ta. Este necesară o repornire pentru a reactiva o cameră dezactivată.Notă: Dezactivarea nu afectează restream-urile go2rtc. Trage de mâner pentru a reordona camerele active așa cum apar în interfață, inclusiv în panoul Live și în meniurile drop-down de selecție a camerei.",
+ "disabledSubheading": "Dezactivat în configurație",
+ "status": {
+ "on": "Pornit",
+ "off": "Oprit",
+ "disabled": "Dezactivat"
+ },
+ "disableSuccess": "S-a dezactivat {{cameraName}} și s-a salvat în configurație."
},
"cameraConfig": {
"add": "Adaugă Cameră",
@@ -1345,10 +1390,109 @@
"profiles": {
"title": "Suprascrieri profil cameră",
"selectLabel": "Selectează profilul",
- "description": "Configurează care camere sunt activate sau dezactivate când un profil este activat. Camerele setate pe \"Moștenire\" își păstrează starea de bază de activare.",
+ "description": "Configurează ce camere sunt pornite sau oprite când un profil este activat. Camerele setate pe \"Moștenește\" își păstrează starea implicită.",
"inherit": "Moștenire",
"enabled": "Activat",
- "disabled": "Dezactivat"
+ "disabled": "Dezactivat",
+ "on": "Pornit",
+ "off": "Oprit"
+ },
+ "cameraType": {
+ "title": "Tip cameră",
+ "label": "Tip cameră",
+ "description": "Setează tipul pentru fiecare cameră. Camerele LPR dedicate sunt camere cu un singur scop, cu zoom optic puternic pentru a captura plăcuțele de înmatriculare ale vehiculelor aflate la distanță. Majoritatea camerelor ar trebui să folosească tipul normal de cameră, cu excepția cazului în care camera este special pentru LPR și are o vedere strâns focalizată pe plăcuțele de înmatriculare.",
+ "normal": "Normal",
+ "dedicatedLpr": "LPR dedicat",
+ "saveSuccess": "Tipul camerei a fost actualizat pentru {{cameraName}}. Repornește Frigate pentru a aplica modificările."
+ },
+ "description": "Adaugă, editează și șterge camere, controlează starea fiecărei camere și configurează excepții pe profil și pe tip de cameră. Pentru a configura stream-uri, detecție, mișcare și alte setări specifice camerelor, alege secțiunea corespunzătoare din Configurare cameră.",
+ "clone": {
+ "sectionTitle": "Clonează setările",
+ "sectionDescription": "Copiază configurația de la o cameră la altă cameră sau la una nouă.",
+ "button": "Clonează setările",
+ "title": "Clonează setările camerei",
+ "description": "Copiază configurația unei camere la una sau mai multe alte camere sau la o cameră nouă. Identitatea (nume, nume prietenos, URL interfață web, ordine de afișare) nu este niciodată copiată.",
+ "source": {
+ "label": "Cameră sursă",
+ "required": "Selectează o cameră sursă",
+ "placeholder": "Selectează o cameră sursă"
+ },
+ "target": {
+ "newRadio": "Cameră nouă",
+ "newNameLabel": "Numele camerei",
+ "legend": "Țintă",
+ "newNamePlaceholder": "ex., usa_spate sau Ușa din spate",
+ "newNameRequired": "Numele camerei este obligatoriu",
+ "newNameInvalid": "Nume cameră invalid",
+ "newNameCollision": "O cameră cu acest nume există deja",
+ "newStreamsForced": "Stream-urile sunt mereu copiate pentru o cameră nouă.",
+ "allCameras": "Toate camerele",
+ "existingCamerasRadio": "Camere existente",
+ "existingPlaceholder": "Selectează cel puțin o cameră",
+ "existingDisabled": "Nu există alte camere către care să copiezi"
+ },
+ "categories": {
+ "legend": "Setări de clonat",
+ "selectAll": "Selectează tot",
+ "selectNone": "Deselectează tot",
+ "description": "Alege ce setări să copiezi de la camera sursă.",
+ "general": "General",
+ "resetDefaults": "Resetează la setările implicite",
+ "spatial": "Setări spațiale",
+ "streams": "Stream-uri",
+ "spatialWarningTitle": "Nepotrivire de rezoluție",
+ "spatialWarning": "Rezoluția de detecție ({{srcWidth}}×{{srcHeight}}) a camerei sursă {{srcCamera}} este diferă de: {{cameras}}. Poligoanele s-ar putea să nu se alinieze pe acele camere. Aceste setări implicite sunt dezactivate; activează-le pentru a copia ca atare.",
+ "restartHint": "Repornire necesară",
+ "items": {
+ "record": "Înregistrare",
+ "snapshots": "Snapshot-uri",
+ "review": "Revizuire",
+ "objects": "Obiecte",
+ "motion": "Detecție mișcare",
+ "audio": "Detecție sunet",
+ "notifications": "Notificări",
+ "birdseye": "Birdseye",
+ "audio_transcription": "Transcriere audio",
+ "mqtt": "MQTT",
+ "onvif": "ONVIF",
+ "timestamp_style": "Stil marcaj temporal",
+ "lpr": "Recunoașterea plăcuțelor de înmatriculare",
+ "face_recognition": "Recunoaștere facială",
+ "semantic_search": "Căutare semantică",
+ "genai": "AI Generativ",
+ "type": "Tip cameră (normală / LPR dedicată)",
+ "profiles": "Profile",
+ "zones": "Zone",
+ "detect": "Dimensiuni de detecție",
+ "motion_mask": "Măști de mișcare",
+ "object_masks": "Măști de obiecte",
+ "ffmpeg_live": "URL-uri și roluri pentru stream-uri"
+ }
+ },
+ "footer": {
+ "changeCount_one": "{{count}} modificare va fi aplicată",
+ "changeCount_few": "{{count}} modificări vor fi aplicate",
+ "changeCount_other": "{{count}} de modificări vor fi aplicate",
+ "restartNeeded": "Va fi necesară o repornire pentru anumite modificări.",
+ "submit": "Clonare",
+ "submitting": "Se clonează…",
+ "liveOnly": "Toate modificările se vor aplica în timp real, fără repornire."
+ },
+ "toast": {
+ "success": "Setări copiate la {{cameraName}}",
+ "successWithRestart": "Setări copiate la {{cameraName}}. Reporniți Frigate pentru a aplica toate modificările.",
+ "successMulti_one": "Setări copiate la {{count}} cameră",
+ "successMulti_few": "Setări copiate la {{count}} camere",
+ "successMulti_other": "Setări copiate la {{count}} de camere",
+ "partialFailure": "{{successCount}} secțiuni aplicate; '{{failedSection}}' a eșuat: {{errorMessage}}",
+ "successMultiWithRestart_one": "Setări copiate la {{count}} cameră. Reporniți Frigate pentru a aplica toate modificările.",
+ "successMultiWithRestart_few": "Setări copiate la {{count}} camere. Reporniți Frigate pentru a aplica toate modificările.",
+ "successMultiWithRestart_other": "Setări copiate la {{count}} de camere. Reporniți Frigate pentru a aplica toate modificările.",
+ "partialFailureMulti": "Copiat la {{successCount}} cameră(e); a eșuat pentru {{failed}}: {{errorMessage}}",
+ "newCameraPartialFailure": "Camera {{cameraName}} a fost creată, dar unele setări nu au putut fi copiate: {{errorMessage}}",
+ "sourceMissing": "Camera sursă nu mai există",
+ "submitError": "Clonarea camerei a eșuat: {{errorMessage}}"
+ }
}
},
"cameraReview": {
@@ -1547,7 +1691,17 @@
}
},
"cameraInputs": {
- "itemTitle": "Stream-ul {{index}}"
+ "itemTitle": "Stream-ul {{index}}",
+ "sourceMode": {
+ "restream": "Restream (go2rtc)",
+ "manual": "Cale de intrare manuală",
+ "go2rtcStreamLabel": "Stream go2rtc",
+ "go2rtcStreamPlaceholder": "Selectează un stream go2rtc",
+ "noGo2rtcStreams": "Niciun stream go2rtc configurat",
+ "availableStreams": "Stream-uri disponibile",
+ "go2rtcStreamSearch": "Caută stream-uri...",
+ "noMatchingStreams": "Niciun stream corespunzător"
+ }
},
"restartRequiredField": "Necesită repornire",
"restartRequiredFooter": "Configurația a fost modificată - Necesită repornire",
@@ -1648,7 +1802,9 @@
"options": {
"embeddings": "Înglobare",
"vision": "Viziune",
- "tools": "Instrumente"
+ "tools": "Instrumente",
+ "descriptions": "Descrieri",
+ "chat": "Chat"
}
},
"semanticSearchModel": {
@@ -1661,7 +1817,46 @@
"empty": "Nicio etichetă disponibilă",
"allNonAlertDetections": "Toată activitatea fără alertă va fi inclusă ca detecții."
},
- "addCustomLabel": "Adaugă etichetă personalizată..."
+ "addCustomLabel": "Adaugă etichetă personalizată...",
+ "genaiModel": {
+ "placeholder": "Selectează sau introdu un model…",
+ "search": "Caută sau introdu un model…",
+ "noModels": "Niciun model disponibil",
+ "available": "Modele disponibile",
+ "useCustom": "Folosește \"{{value}}\"",
+ "refresh": "Reîmprospătează modelele",
+ "probeFailed": "Nu s-au putut interoga modelele",
+ "fetchedModels": "Lista de modele preluată cu succes"
+ },
+ "knownPlates": {
+ "namePlaceholder": "ex. Mașina soției",
+ "platePlaceholder": "Număr plăcuță sau regex"
+ },
+ "semanticSearchModelSize": {
+ "notApplicable": "Nu se aplică pentru furnizorii de GenAI"
+ },
+ "liveStreams": {
+ "streamNameLabel": "Nume stream",
+ "go2rtcStreamLabel": "stream go2rtc",
+ "go2rtcStreamPlaceholder": "Selectează un stream go2rtc",
+ "streamNamePlaceholder": "ex., Stream HD principal",
+ "go2rtcStreamSearch": "Căutați sau introduceți un nume de stream…",
+ "noGo2rtcStreams": "Niciun stream go2rtc configurat",
+ "availableStreams": "Stream-uri disponibile",
+ "useCustom": "Folosește \"{{value}}\"",
+ "addStream": "Adaugă stream"
+ },
+ "ptzPresets": {
+ "placeholder": "Selectați sau introduceți o presetare...",
+ "search": "Căutați sau introduceți o presetare...",
+ "available": "Presetări cameră",
+ "noPresets": "Nu sunt presetări disponibile",
+ "useCustom": "Folosește \"{{value}}\""
+ },
+ "defaultRole": {
+ "admin": "Administrator",
+ "viewer": "Vizualizator"
+ }
},
"globalConfig": {
"title": "Configurare Globală",
@@ -1696,7 +1891,10 @@
"saveAllPartial_few": "{{successCount}} din {{totalCount}} secțiuni salvate. {{failCount}} eșuate.",
"saveAllPartial_other": "{{successCount}} din {{totalCount}} de secțiuni salvate. {{failCount}} eșuate.",
"saveAllFailure": "Eroare la salvarea tuturor secțiunilor.",
- "applied": "Setările au fost aplicate cu succes"
+ "applied": "Setările au fost aplicate cu succes",
+ "saveAllSuccessRestartRequired_one": "{{count}} secțiune salvată cu succes. Repornește Frigate pentru a aplica modificările.",
+ "saveAllSuccessRestartRequired_few": "{{count}} secțiuni salvate cu succes. Repornește Frigate pentru a aplica modificările.",
+ "saveAllSuccessRestartRequired_other": "{{count}} de secțiuni salvate cu succes. Repornește Frigate pentru a aplica modificările."
},
"unsavedChanges": "Ai modificări nesalvate",
"confirmReset": "Confirmă Resetarea",
@@ -1706,7 +1904,30 @@
"overriddenGlobal": "Suprascris (global)",
"overriddenGlobalTooltip": "Această cameră suprascrie setările globale de configurare din această secțiune",
"overriddenBaseConfig": "Suprascris (configurația de bază)",
- "overriddenBaseConfigTooltip": "Profilul {{profile}} suprascrie setările de configurare din această secțiune"
+ "overriddenBaseConfigTooltip": "Profilul {{profile}} suprascrie setările de configurare din această secțiune",
+ "overriddenInCameras": {
+ "label_one": "Suprascris în {{count}} cameră",
+ "label_few": "Suprascris în {{count}} camere",
+ "label_other": "Suprascris în {{count}} de camere",
+ "tooltip_one": "{{count}} cameră suprascrie valorile din această secțiune. Click pentru a vedea detaliile.",
+ "tooltip_few": "{{count}} camere suprascriu valorile din această secțiune. Click pentru a vedea detaliile.",
+ "tooltip_other": "{{count}} de camere suprascriu valorile din această secțiune. Click pentru a vedea detaliile.",
+ "heading_one": "Această secțiune globală are câmpuri care sunt suprascrise în {{count}} cameră.",
+ "heading_few": "Această secțiune globală are câmpuri care sunt suprascrise în {{count}} camere.",
+ "heading_other": "Această secțiune globală are câmpuri care sunt suprascrise în {{count}} de camere.",
+ "othersField_one": "{{count}} alta",
+ "othersField_few": "{{count}} alte",
+ "othersField_other": "{{count}} de alte",
+ "profilePrefix": "Profil {{profile}}: {{fields}}"
+ },
+ "overriddenGlobalHeading_one": "Această cameră suprascrie {{count}} câmp din configurația globală:",
+ "overriddenGlobalHeading_few": "Această cameră suprascrie {{count}} câmpuri din configurația globală:",
+ "overriddenGlobalHeading_other": "Această cameră suprascrie {{count}} de câmpuri din configurația globală:",
+ "overriddenGlobalNoDeltas": "Această cameră suprascrie configurația globală, dar nicio valoare a câmpurilor nu diferă.",
+ "overriddenBaseConfigHeading_one": "Profilul {{profile}} suprascrie {{count}} câmp din configurația de bază:",
+ "overriddenBaseConfigHeading_few": "Profilul {{profile}} suprascrie {{count}} câmpuri din configurația de bază:",
+ "overriddenBaseConfigHeading_other": "Profilul {{profile}} suprascrie {{count}} de câmpuri din configurația de bază:",
+ "overriddenBaseConfigNoDeltas": "Profilul {{profile}} suprascrie această secțiune, dar nicio valoare a câmpurilor nu diferă de configurația de bază."
},
"profiles": {
"title": "Profile",
@@ -1790,8 +2011,18 @@
"audioMp3": "Transcodează în MP3",
"audioExclude": "Exclude",
"hardwareNone": "Fără accelerare hardware",
- "hardwareAuto": "Accelerare hardware automată"
- }
+ "hardwareAuto": "Automat (recomandat)",
+ "hardwareVaapi": "VAAPI",
+ "hardwareCuda": "CUDA",
+ "hardwareV4l2m2m": "V4L2 M2M",
+ "hardwareDxva2": "DXVA2",
+ "hardwareVideotoolbox": "VideoToolbox",
+ "addVideoCodec": "Adaugă codec video",
+ "addAudioCodec": "Adaugă codec audio",
+ "removeCodec": "Elimină codecul"
+ },
+ "streamNumber": "Stream {{index}}",
+ "sourceNumber": "Sursă {{index}}"
},
"timestampPosition": {
"tl": "Sus stânga",
@@ -1801,13 +2032,21 @@
},
"onvif": {
"profileAuto": "Auto",
- "profileLoading": "Se încarcă profilurile..."
+ "profileLoading": "Se încarcă profilurile...",
+ "autotracking": {
+ "zooming": {
+ "disabled": "Dezactivat",
+ "absolute": "Absolut",
+ "relative": "Relativ"
+ }
+ }
},
"configMessages": {
"review": {
"recordDisabled": "Înregistrarea este dezactivată, elementele de revizuire nu vor fi generate.",
"detectDisabled": "Detecția obiectelor este dezactivată. Elementele de revizuire necesită obiecte detectate pentru a categorisi alertele și detecțiile.",
- "allNonAlertDetections": "Toată activitatea fără alertă va fi inclusă ca detecții."
+ "allNonAlertDetections": "Toată activitatea fără alertă va fi inclusă ca detecții.",
+ "genaiImageSourceRecordingsRecordDisabled": "Sursa imaginii este setată pe 'recordings', dar înregistrarea este dezactivată. Frigate va reveni la imaginile de previzualizare."
},
"audio": {
"noAudioRole": "Niciun flux nu are rolul audio definit. Trebuie să activați rolul audio pentru ca detecția audio să funcționeze."
@@ -1816,15 +2055,24 @@
"audioDetectionDisabled": "Detecția audio nu este activată pentru această cameră. Transcrierea audio necesită ca detecția audio să fie activă."
},
"detect": {
- "fpsGreaterThanFive": "Setarea cadrelor pe secundă pentru detecție la o valoare mai mare de 5 nu este recomandată."
+ "fpsGreaterThanFive": "Setarea FPS-ului de detecție mai mare de 5 nu este recomandată. Valorile mai mari pot cauza probleme de performanță și nu vor oferi niciun beneficiu.",
+ "disabled": "Detecția de obiecte este dezactivată. Snapshot-urile, elementele de revizuire și îmbogățirile precum recunoașterea facială, recunoașterea plăcuțelor de înmatriculare și AI-ul generativ nu vor funcționa.",
+ "resolutionShouldBeMultipleOfFour": "Pentru rezultate optime, lățimea și înălțimea de detecție ar trebui să fie multipli de 4. Alte valori pare pot produce artefacte vizuale sau o ușoară distorsionare în fluxul de detecție.",
+ "aspectRatioMismatch": "Lățimea și înălțimea introduse nu se potrivesc cu raportul de aspect al rezoluției actuale de detecție. Acest lucru poate produce o imagine întinsă sau distorsionată.",
+ "maxFramesSet": "Setarea numărului maxim de cadre suprascrie comportamentul implicit și dezactivează urmărirea obiectelor staționare. Există foarte puține situații în care acest lucru este necesar, folosește cu precauție.",
+ "squareResolution": "O rezoluție de detecție pătrată este neobișnuită. Lățimea și înălțimea de detecție ar trebui să se potrivească cu raportul de aspect al camerei tale (de exemplu, 16:9), nu cu dimensiunile modelului de detecție a obiectelor. Un raport de aspect nepotrivit poate întinde imaginea și reduce acuratețea detecției.",
+ "resolutionHigh": "Această rezoluție de detecție este mai mare decât cea recomandată și poate cauza un consum crescut de resurse fără a îmbunătăți acuratețea detecției. O rezoluție de detecție de 1080p sau mai mică este recomandată pentru majoritatea camerelor.",
+ "globalResolutionMultipleCameras": "O rezoluție de detecție globală este setată în timp ce sunt configurate mai multe camere. Dacă nu cumva toate camerele împart aceeași rezoluție și același raport de aspect, lățimea și înălțimea de detecție ar trebui definite per cameră pentru a se potrivi cu raportul de aspect nativ al fiecărei camere."
},
"faceRecognition": {
- "globalDisabled": "Recunoașterea facială nu este activată la nivel global. Activați-o în setările globale pentru ca recunoașterea facială la nivel de cameră să funcționeze.",
- "personNotTracked": "Recunoașterea facială necesită urmărirea obiectului „person”. Asigurați-vă că „person” este în lista de urmărire a obiectelor."
+ "globalDisabled": "Îmbogățirea pentru recunoaștere facială trebuie activată pentru ca funcțiile de recunoaștere facială să funcționeze pe această cameră.",
+ "personNotTracked": "Recunoașterea facială necesită ca obiectul 'person' să fie urmărit. Activează 'person' în Obiecte pentru această cameră.",
+ "modelSizeLarge": "Modelul 'large' necesită un GPU sau NPU pentru o performanță rezonabilă. Folosește 'small' pe sistemele doar cu CPU."
},
"lpr": {
- "globalDisabled": "Recunoașterea plăcuțelor de înmatriculare nu este activată la nivel global. Activați-o în setările globale pentru ca recunoașterea la nivel de cameră să funcționeze.",
- "vehicleNotTracked": "Recunoașterea plăcuțelor de înmatriculare necesită ca „car” sau „motorcycle” să fie urmărite."
+ "globalDisabled": "Îmbogățirea pentru recunoașterea plăcuțelor de înmatriculare trebuie activată pentru ca funcțiile LPR să funcționeze pe această cameră.",
+ "vehicleNotTracked": "Recunoașterea plăcuțelor de înmatriculare necesită ca „car” sau „motorcycle” să fie urmărite.",
+ "modelSizeLarge": "Modelul 'large' este optimizat pentru plăcuțele de înmatriculare pe mai multe rânduri. Modelul 'small' oferă o performanță mai bună decât 'large' și ar trebui folosit cu excepția cazului în care regiunea ta folosește formate de plăcuțe pe mai multe rânduri."
},
"record": {
"noRecordRole": "Niciun flux nu are rolul de înregistrare definit. Înregistrarea nu va funcționa."
@@ -1838,6 +2086,117 @@
"detectors": {
"mixedTypes": "Toți detectorii trebuie să folosească același tip. Șterge detectorii existenți pentru a folosi un alt tip.",
"mixedTypesSuggestion": "Toți detectorii trebuie să folosească același tip. Șterge detectorii existenți sau selectează {{type}}."
+ },
+ "objects": {
+ "genaiNoDescriptionsProvider": "Trebuie să configurezi un furnizor GenAI cu rolul 'descriptions' pentru ca descrierile să fie generate."
+ },
+ "semanticSearch": {
+ "jinav2SmallModelSize": "Dimensiunea 'small' cu modelul Jina V2 are un cost ridicat de RAM și inferență. Modelul 'large' cu un GPU dedicat este recomandat."
+ },
+ "onvif": {
+ "autotrackingNoZones": "Autotracking-ul necesită cel puțin o zonă. Definește o zonă pentru această cameră în Măști / Zone, apoi seteaz-o ca zonă obligatorie mai jos."
+ },
+ "ffmpeg": {
+ "hwaccelManualNotRecommended": "Argumentele manuale pentru accelerarea hardware nu sunt recomandate. Dacă nu există o cerință specifică, selectează presetarea care se potrivește cu hardware-ul tău."
}
+ },
+ "birdseye": {
+ "trackingMode": {
+ "objects": "Obiecte",
+ "motion": "Mișcare",
+ "continuous": "Continuu"
+ },
+ "cameraOrder": {
+ "label": "Ordinea camerelor",
+ "description": "Trage camerele pentru a le seta ordinea în aranjamentul Birdseye.",
+ "saving": "Se salvează…",
+ "saved": "Salvat",
+ "reorderHandle": "Trage pentru a reordona"
+ }
+ },
+ "snapshot": {
+ "retainMode": {
+ "all": "Toate",
+ "motion": "Mișcare",
+ "active_objects": "Obiecte active"
+ }
+ },
+ "ui": {
+ "timeFormat": {
+ "browser": "Browser",
+ "12hour": "12 ore",
+ "24hour": "24 de ore"
+ },
+ "TimeOrDateStyle": {
+ "long": "Lung",
+ "medium": "Mediu",
+ "short": "Scurt",
+ "full": "Complet"
+ },
+ "unitSystem": {
+ "metric": "Metric",
+ "imperial": "Imperial"
+ }
+ },
+ "review": {
+ "imageSource": {
+ "recordings": "Înregistrări",
+ "previews": "Previzualizări"
+ }
+ },
+ "logger": {
+ "logLevel": {
+ "debug": "Depanare",
+ "info": "Informații",
+ "warning": "Avertisment",
+ "error": "Eroare",
+ "critical": "Critic"
+ }
+ },
+ "modelSize": {
+ "small": "Mic",
+ "large": "Mare"
+ },
+ "retainMode": {
+ "all": "Toate",
+ "motion": "Mișcare",
+ "active_objects": "Obiecte active"
+ },
+ "previewQuality": {
+ "medium": "Mediu",
+ "very_high": "Foarte ridicat",
+ "high": "Ridicat",
+ "low": "Scăzut",
+ "very_low": "Foarte scăzut"
+ },
+ "menuDot": {
+ "overrideGlobal": "Această secțiune suprascrie configurația globală",
+ "overrideProfile": "Această secțiune este suprascrisă de profilul {{profile}}",
+ "unsaved": "Această secțiune are modificări nesalvate"
+ },
+ "detectorsAndModel": {
+ "title": "Detectori și model",
+ "description": "Configurează backend-ul detectorului care rulează detecția obiectelor și modelul pe care îl folosește. Modificările sunt salvate împreună, astfel încât detectorul și modelul să rămână sincronizate.",
+ "cardTitles": {
+ "model": "Model de detecție",
+ "detector": "Hardware detector"
+ },
+ "tabs": {
+ "plus": "Frigate+",
+ "custom": "Model personalizat"
+ },
+ "mismatch": {
+ "warning": "Modelul curent Frigate+ \"{{model}}\" necesită detectorul {{required}}. Alege un model compatibil mai jos sau treci la Model personalizat înainte de a salva."
+ },
+ "plusModel": {
+ "requiresDetector": "Necesită: {{detector}}",
+ "noModelSelected": "Selectează un model Frigate+"
+ },
+ "toast": {
+ "saveSuccess": "Setările pentru detectoare și model au fost salvate. Repornește Frigate pentru a aplica modificările.",
+ "saveError": "Nu s-au putut salva setările pentru detector și model"
+ },
+ "unsavedChanges": "Modificări nesalvate pentru detector și model",
+ "restartRequired": "Repornire necesară (detector sau model schimbat)"
}
}
diff --git a/web/public/locales/ro/views/system.json b/web/public/locales/ro/views/system.json
index e829edd602..7c0f6342f5 100644
--- a/web/public/locales/ro/views/system.json
+++ b/web/public/locales/ro/views/system.json
@@ -68,7 +68,7 @@
"series": {
"go2rtc": "go2rtc",
"recording": "înregistrare",
- "review_segment": "segment recenzie",
+ "review_segment": "segment revizuire",
"embeddings": "înglobări",
"audio_detector": "detector audio"
}
@@ -172,9 +172,9 @@
"yolov9_plate_detection_speed": "Viteză Detecție Numere YOLOv9",
"text_embedding_speed": "Viteză înglobări de text",
"yolov9_plate_detection": "Detecție Numere YOLOv9",
- "review_description": "Descriere Recenzie",
- "review_description_speed": "Viteză Descriere Recenzie",
- "review_description_events_per_second": "Descriere Recenzie",
+ "review_description": "Descriere revizuire",
+ "review_description_speed": "Viteză descriere revizuire",
+ "review_description_events_per_second": "Descriere revizuire",
"object_description": "Descriere Obiect",
"object_description_speed": "Viteză Descriere Obiect",
"object_description_events_per_second": "Descriere Obiect",
@@ -201,7 +201,22 @@
"title": "Info Sondă Cameră"
},
"fps": "FPS:",
- "unknown": "Necunoscut"
+ "unknown": "Necunoscut",
+ "keyframes": {
+ "title": "Analiză keyframe",
+ "analyzing": "Se analizează keyframe-urile... {{seconds}} secunde rămase",
+ "stillAnalyzing": "Încă se analizează keyframe-urile...",
+ "keyframeCount": "Keyframe-uri observate:",
+ "recordStream": "Stream de înregistrare:",
+ "observedDuration": "Durată observată:",
+ "gap": "Interval keyframe (min / med / max):",
+ "ok": "Keyframe-uri la fiecare ~{{seconds}}s, bune pentru înregistrare și redare.",
+ "segmentLength": "Lungime segment de înregistrare:",
+ "warning": "Keyframe-uri rare sau variabile (cel mai lung interval ~{{seconds}}s), probabil un codec smart (H.264+/H.265+), acest lucru nu este recomandat.",
+ "error": "Intervalul keyframe (~{{seconds}}s) depășește lungimea segmentului de înregistrare ({{segmentTime}}s). Unele segmente pot să nu aibă niciun keyframe, ceea ce întrerupe redarea. Dezactivează codecul smart/+ de pe cameră sau scurtează intervalul keyframe.",
+ "unknown": "Nu s-a putut determina distanțarea keyframe-urilor.",
+ "recordDisabled": "Înregistrarea este dezactivată pentru această cameră."
+ }
},
"label": {
"capture": "captură",
@@ -241,6 +256,9 @@
"expectedFps": "FPS așteptat",
"reconnectsLastHour": "Reconectări (ultima oră)",
"stallsLastHour": "Blocaje (ultima oră)"
+ },
+ "noCameras": {
+ "title": "Nicio cameră găsită"
}
},
"stats": {
diff --git a/web/public/locales/ru/common.json b/web/public/locales/ru/common.json
index 54e214855d..5e27bc19e6 100644
--- a/web/public/locales/ru/common.json
+++ b/web/public/locales/ru/common.json
@@ -130,7 +130,19 @@
"export": "Экспортировать",
"deleteNow": "Удалить сейчас",
"next": "Следующий",
- "continue": "Продолжить"
+ "continue": "Продолжить",
+ "add": "Добавить",
+ "applying": "Применяется…",
+ "undo": "Отменить",
+ "copiedToClipboard": "Скопировано в буфер обмена",
+ "saveAll": "Сохранить все",
+ "savingAll": "Сохранение всего…",
+ "undoAll": "Откатить всё",
+ "retry": "Повторить",
+ "modified": "Изменено",
+ "overridden": "Переопределено",
+ "resetToGlobal": "Сброс к Глобальному значению",
+ "resetToDefault": "Сброс к значению по умолчанию"
},
"label": {
"back": "Вернуться",
@@ -169,7 +181,7 @@
"languages": "Языки",
"language": {
"en": "English (Английский)",
- "zhCN": "简体中文 (Упрощённый китайский)",
+ "zhCN": "简体中文 (Упрощённый Китайский)",
"es": "Español (Испанский)",
"hi": "हिन्दी (Хинди)",
"fr": "Français (Французский)",
@@ -209,7 +221,10 @@
"bg": "Български (Болгарский)",
"gl": "Galego (Галисийский)",
"id": "Bahasa Indonesia (Индонезийский)",
- "ur": "اردو (Урду)"
+ "ur": "اردو (Урду)",
+ "zhHant": "繁體中文 (Традиционный Китайский)",
+ "hr": "Hrvatski (Хорватский)",
+ "bs": "Bosanski (Боснийский)"
},
"darkMode": {
"withSystem": {
@@ -260,7 +275,11 @@
"setPassword": "Установить пароль"
},
"appearance": "Внешний вид",
- "classification": "Распознование"
+ "classification": "Распознование",
+ "profiles": "Профили",
+ "chat": "Чат",
+ "actions": "Действия",
+ "features": "Функции"
},
"pagination": {
"label": "пагинация",
@@ -291,7 +310,8 @@
"noMessage": "Не удалось сохранить изменения конфигурации",
"title": "Не удалось сохранить изменения конфигурации: {{errorMessage}}"
},
- "title": "Сохранить"
+ "title": "Сохранить",
+ "success": "Изменения конфигурации сохранены успешно."
}
},
"role": {
@@ -312,5 +332,10 @@
"field": {
"optional": "Необязательный",
"internalID": "Внутренний идентификатор Frigate, используемый в конфигурации и базе данных"
+ },
+ "validation_errors": "Ошибки Валидации",
+ "no_items": "Нет элементов",
+ "credentialField": {
+ "savedPlaceholder": "Сохранено — оставьте пустым, чтобы сохранить текущее значение"
}
}
diff --git a/web/public/locales/ru/components/camera.json b/web/public/locales/ru/components/camera.json
index 8a8c1a4922..3ff007c1f3 100644
--- a/web/public/locales/ru/components/camera.json
+++ b/web/public/locales/ru/components/camera.json
@@ -2,7 +2,7 @@
"group": {
"label": "Группы камер",
"add": "Добавить группу камер",
- "edit": "Редактирование группы камер",
+ "edit": "Редактировать группу камер",
"delete": {
"label": "Удалить группу камер",
"confirm": {
@@ -68,7 +68,10 @@
"placeholder": "Выбрать поток"
},
"birdseye": "Birdseye"
- }
+ },
+ "showAll": "Показать все группы камер",
+ "showLess": "Показать меньше",
+ "editGroups": "Редактировать группы камер"
},
"debug": {
"options": {
@@ -82,6 +85,7 @@
"zones": "Зоны",
"mask": "Маска",
"motion": "Движение",
- "regions": "Регионы"
+ "regions": "Регионы",
+ "paths": "Пути"
}
}
diff --git a/web/public/locales/ru/components/dialog.json b/web/public/locales/ru/components/dialog.json
index b935670c2e..787fa1b3f1 100644
--- a/web/public/locales/ru/components/dialog.json
+++ b/web/public/locales/ru/components/dialog.json
@@ -6,7 +6,8 @@
"title": "Frigate перезапускается",
"content": "Эта страница перезагрузится через {{countdown}} сек.",
"button": "Принудительная перезагрузка"
- }
+ },
+ "description": "Это перезагрузки перезагрузит Frigate."
},
"explore": {
"plus": {
@@ -35,6 +36,9 @@
"label": "Подтвердить эту метку для Frigate Plus",
"ask_a": "Это объект — {{label}}?",
"ask_full": "Это объект — {{untranslatedLabel}} ({{translatedLabel}})?"
+ },
+ "toast": {
+ "error": "Не удалось подключиться к Frigate+. Пожалуйста, проверьте настройки сети и попробуйте еще раз."
}
}
},
@@ -71,11 +75,51 @@
"noVaildTimeSelected": "Не выбран допустимый временной диапазон",
"endTimeMustAfterStartTime": "Время окончания должно быть после времени начала"
},
- "view": "Просмотр"
+ "view": "Просмотр",
+ "queued": "Экспорт поставлен в очередь. Следите за ходом выполнения на странице экспорта.",
+ "batchSuccess_one": "Начат {{count}} экспорт. Открываем случай.",
+ "batchSuccess_few": "Начато {{count}} экспорта. Открываем случай.",
+ "batchSuccess_many": "Начато {{count}} экспортов. Открываем случай.",
+ "batchPartial": "Начато {{successful}} из {{total}} экспортов. Неудавшиеся камеры: {{failedCameras}}",
+ "batchFailed": "Неудавшихся экспортов: {{total}}. Неудавшиеся камеры: {{failedCameras}}",
+ "batchQueuedSuccess_one": "1 экспорт поставлен в очередь. Открываем случай.",
+ "batchQueuedSuccess_few": "{{count}} экспорта поставлено в очередь. Открываем случай.",
+ "batchQueuedSuccess_many": "{{count}} экспортов поставлены в очередь. Открываем случай."
},
"fromTimeline": {
"saveExport": "Сохранить экспорт",
"previewExport": "Предпросмотр экспорта"
+ },
+ "case": {
+ "label": "Случай",
+ "placeholder": "Выберите случай",
+ "newCaseOption": "Создайте новый случай",
+ "newCaseNamePlaceholder": "Имя нового случая",
+ "newCaseDescriptionPlaceholder": "Описание Случая",
+ "nonAdminHelp": "Для этих экспортов будет создан новый случай."
+ },
+ "queueing": "Помещаем Экспорт в очередь...",
+ "tabs": {
+ "export": "Одиночная Камера",
+ "multiCamera": "Мульти-Камера"
+ },
+ "multiCamera": {
+ "timeRange": "Временной промежуток",
+ "selectFromTimeline": "Выбрать на таймлайне",
+ "cameraSelection": "Камеры",
+ "cameraSelectionHelp": "Камеры с отслеживаемыми объектами в этом временном диапазоне выбраны заранее",
+ "searchOrSelectGroup": "Найдите или выберите группу камер...",
+ "selectAll": "Выберите все камеры",
+ "clearSelection": "Очистить выбор",
+ "selectWithActivity": "Камеры с отслеживаемыми объектами",
+ "selectGroup": "Выбрать группу",
+ "noMatchingCameras": "Камеры по вашему запросу не найдены",
+ "selectedCount": "{{selected}} / {{total}} выбрано",
+ "checkingActivity": "Проверка доступности камеры...",
+ "noCameras": "Нет доступных камер",
+ "detectionCount_one": "{{count}} отслеживаемый объект",
+ "detectionCount_few": "{{count}} отслеживаемых объекта",
+ "detectionCount_many": "{{count}} отслеживаемых объектов"
}
},
"streaming": {
diff --git a/web/public/locales/ru/config/global.json b/web/public/locales/ru/config/global.json
index 5e7de1ab3b..64b4958111 100644
--- a/web/public/locales/ru/config/global.json
+++ b/web/public/locales/ru/config/global.json
@@ -83,5 +83,59 @@
"label": "Конфигурация стационарных объектов",
"description": "Настройки для обнаружения и управления объектами, которые остаются неподвижными в течение определенного периода времени."
}
+ },
+ "version": {
+ "label": "Текущая версия конфигурации",
+ "description": "Число или строка версии текущей конфигурации, которая может использоваться для определения миграций или форматирования изменений."
+ },
+ "safe_mode": {
+ "label": "Безопасный режим",
+ "description": "Когда включено, Frigate запустится в безопасном режиме с ограниченными функциями для поиска неисправностей."
+ },
+ "environment_vars": {
+ "label": "Переменные окружения",
+ "description": "Пары ключ/значения для переменных окружения которые необходимо задать для процесса Frigate в Home Assistant OS. Пользователи, которые не исползуют HAOS должны испольовать переменные окружения в Docker."
+ },
+ "logger": {
+ "label": "Логирование",
+ "description": "Управляет уровнем логирования по умолчанию и переопределением уровня для каждого компонента.",
+ "default": {
+ "label": "Уровень логирования",
+ "description": "Стандартный глобальный уровень логирования (debug, info, warning, error)."
+ },
+ "logs": {
+ "label": "Уровень логирования для каждого процесса"
+ }
+ },
+ "auth": {
+ "label": "Аутентификация",
+ "description": "Настройки аутентификации и сеанса, включая параметры cookie и ограничения скорости.",
+ "enabled": {
+ "label": "Включить аутентификацию",
+ "description": "Включить встроенную аутентификацию для интерфейса Frigate."
+ },
+ "reset_admin_password": {
+ "label": "Сбросить пароль администратора",
+ "description": "Если выбрано, сбросить пароль администратора при запуске и отобразить новый пароль в логе."
+ },
+ "cookie_name": {
+ "label": "Имя куки JWT",
+ "description": "Имя куки, используемого для хранения JWT токена для стандартной аутенфикации."
+ },
+ "cookie_secure": {
+ "label": "Флаг \"безопасный куки\"",
+ "description": "Устанавливает флаг \"secure\" на куки аутенфикации; должно быть включено когда используется TLS."
+ },
+ "session_length": {
+ "label": "Длинна сессии",
+ "description": "Длина сессии в секундах для JWT сессий."
+ },
+ "refresh_time": {
+ "label": "Окно обновления сессии",
+ "description": "Когда сессия в стольки секундах от истечения, обновить её обратно к полной длительности."
+ },
+ "failed_login_rate_limit": {
+ "label": "Лимит неудавшихся попыток логина"
+ }
}
}
diff --git a/web/public/locales/ru/config/groups.json b/web/public/locales/ru/config/groups.json
index 0967ef424b..a7c9152f5b 100644
--- a/web/public/locales/ru/config/groups.json
+++ b/web/public/locales/ru/config/groups.json
@@ -1 +1,65 @@
-{}
+{
+ "audio": {
+ "global": {
+ "sensitivity": "Общая чувствительность",
+ "detection": "Общее обнаружение"
+ },
+ "cameras": {
+ "detection": "Обнаружение",
+ "sensitivity": "Чувствительность"
+ }
+ },
+ "timestamp_style": {
+ "global": {
+ "appearance": "Глобальный вид"
+ },
+ "cameras": {
+ "appearance": "Вид"
+ }
+ },
+ "motion": {
+ "global": {
+ "sensitivity": "Глобальная чувствительность",
+ "algorithm": "Глобальный алгоритм"
+ },
+ "cameras": {
+ "sensitivity": "Чувствительность",
+ "algorithm": "Алгоритм"
+ }
+ },
+ "detect": {
+ "global": {
+ "resolution": "Глобальное разрешение",
+ "tracking": "Глобальное отслеживание"
+ },
+ "cameras": {
+ "resolution": "Разрешение",
+ "tracking": "Отслеживание"
+ }
+ },
+ "objects": {
+ "global": {
+ "tracking": "Глобальное отслеживание",
+ "filtering": "Глобальная фильтрация"
+ },
+ "cameras": {
+ "tracking": "Отслеживание",
+ "filtering": "Фильтрация"
+ }
+ },
+ "record": {
+ "global": {
+ "retention": "Глобальное сохранение данных",
+ "events": "Глобальные события"
+ },
+ "cameras": {
+ "retention": "Сохранение данных",
+ "events": "События"
+ }
+ },
+ "ffmpeg": {
+ "cameras": {
+ "cameraFfmpeg": "Аргументы FFmpeg для этой камеры"
+ }
+ }
+}
diff --git a/web/public/locales/ru/config/validation.json b/web/public/locales/ru/config/validation.json
index 0967ef424b..2314881ed9 100644
--- a/web/public/locales/ru/config/validation.json
+++ b/web/public/locales/ru/config/validation.json
@@ -1 +1,32 @@
-{}
+{
+ "maximum": "Должно быть максимум {{limit}}",
+ "exclusiveMinimum": "Должно быть больше {{limit}}",
+ "exclusiveMaximum": "Должно быть не более {{limit}}",
+ "minLength": "Должно быть не менее {{limit}} символов",
+ "maxLength": "Должно быть не более {{limit}} символов",
+ "minItems": "Должно быть не менее {{limit}} значений",
+ "maxItems": "Должно быть не более {{limit}} значений",
+ "pattern": "Неправильный формат",
+ "required": "Это поле обязательно",
+ "type": "Неправильный тип значения",
+ "enum": "Должно быть одним из списка разрешенных значений",
+ "const": "Значение не совпадает с ожидаемой константой",
+ "uniqueItems": "Все значения должны быть уникальны",
+ "format": "Неправильный формат",
+ "additionalProperties": "Неизвестное значение недопустимо",
+ "oneOf": "Должно совпадать только с одной из разрешенных схем",
+ "anyOf": "Должно совпадать как минимум с одной из разрешенных схем",
+ "proxy": {
+ "header_map": {
+ "roleHeaderRequired": "Заголовок роли требуется когда маппинги ролей настроены."
+ }
+ },
+ "ffmpeg": {
+ "inputs": {
+ "rolesUnique": "Каждой роли может быть назначен только один входной поток.",
+ "detectRequired": "Как минимум один входной поток должен быть назначен роли 'detect'.",
+ "hwaccelDetectOnly": "Только входной поток с ролью detect может настраивать аппаратное ускорение."
+ }
+ },
+ "minimum": "Должно быть минимум {{limit}}"
+}
diff --git a/web/public/locales/ru/views/chat.json b/web/public/locales/ru/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/ru/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/ru/views/classificationModel.json b/web/public/locales/ru/views/classificationModel.json
index 6dbe7a4b16..1213912961 100644
--- a/web/public/locales/ru/views/classificationModel.json
+++ b/web/public/locales/ru/views/classificationModel.json
@@ -18,11 +18,11 @@
"toast": {
"success": {
"deletedCategory_one": "Класс удалён",
- "deletedCategory_few": "",
- "deletedCategory_many": "",
+ "deletedCategory_few": "Класса удалено",
+ "deletedCategory_many": "Классов удалено",
"deletedImage_one": "Изображения удалены",
- "deletedImage_few": "",
- "deletedImage_many": "",
+ "deletedImage_few": "Изображения удалено",
+ "deletedImage_many": "Изображений удалено",
"deletedModel_one": "Успешно удалена {{count}} модель",
"deletedModel_few": "Успешно удалены {{count}} модели",
"deletedModel_many": "Успешно удалены {{count}} моделей",
@@ -30,7 +30,8 @@
"trainedModel": "Модель успешно обучена.",
"trainingModel": "Обучение модели успешно запущено.",
"updatedModel": "Конфигурация модели успешно обновлена",
- "renamedCategory": "Класс успешно переименован в {{name}}"
+ "renamedCategory": "Класс успешно переименован в {{name}}",
+ "reclassifiedImage": "Изображение успешно переклассифцировано"
},
"error": {
"deleteImageFailed": "Не удалось удалить: {{errorMessage}}",
diff --git a/web/public/locales/ru/views/events.json b/web/public/locales/ru/views/events.json
index a506ea4529..20fa143aed 100644
--- a/web/public/locales/ru/views/events.json
+++ b/web/public/locales/ru/views/events.json
@@ -16,7 +16,9 @@
"description": "Элементы обзора могут быть созданы для камеры только в том случае, если запись включена для этой камеры."
}
},
- "timeline": "Таймлайн",
+ "timeline": {
+ "label": "Хронология"
+ },
"timeline.aria": "Выбор таймлайна",
"events": {
"label": "События",
diff --git a/web/public/locales/ru/views/exports.json b/web/public/locales/ru/views/exports.json
index c14a578cab..70f8753b6e 100644
--- a/web/public/locales/ru/views/exports.json
+++ b/web/public/locales/ru/views/exports.json
@@ -2,7 +2,9 @@
"documentTitle": "Экспорт - Frigate",
"search": "Поиск",
"noExports": "Не найдено файлов экспорта",
- "deleteExport": "Удалить экспорт",
+ "deleteExport": {
+ "label": "Удалить экспорт"
+ },
"deleteExport.desc": "Вы уверены, что хотите удалить {{exportName}}?",
"editExport": {
"title": "Переименовать экспорт",
@@ -11,13 +13,27 @@
},
"toast": {
"error": {
- "renameExportFailed": "Не удалось переименовать экспорт: {{errorMessage}}"
+ "renameExportFailed": "Не удалось переименовать экспорт: {{errorMessage}}",
+ "assignCaseFailed": "Не удалось обновить назначение случая: {{errorMessage}}"
}
},
"tooltip": {
"shareExport": "Поделиться экспортом",
"downloadVideo": "Скачать видео",
"editName": "Изменить название",
- "deleteExport": "Удалить экспорт"
+ "deleteExport": "Удалить экспорт",
+ "assignToCase": "Добавить в случай"
+ },
+ "headings": {
+ "cases": "Случаи",
+ "uncategorizedExports": "Некатегоризированные экспорты"
+ },
+ "caseDialog": {
+ "title": "Добавить в случай",
+ "description": "Выберите существующий случай или создайте новый.",
+ "selectLabel": "Случай",
+ "newCaseOption": "Создать новый случай",
+ "nameLabel": "Название случая",
+ "descriptionLabel": "Описание"
}
}
diff --git a/web/public/locales/ru/views/faceLibrary.json b/web/public/locales/ru/views/faceLibrary.json
index 90aa901d18..d3950b3a92 100644
--- a/web/public/locales/ru/views/faceLibrary.json
+++ b/web/public/locales/ru/views/faceLibrary.json
@@ -13,7 +13,8 @@
"description": {
"placeholder": "Введите название коллекции",
"addFace": "Добавьте новую коллекцию в библиотеку лиц, загрузив свое первое изображение.",
- "invalidName": "Недопустимое имя. Имена могут содержать только буквы, цифры, пробелы, апострофы, подчёркивания и дефисы."
+ "invalidName": "Недопустимое имя. Имена могут содержать только буквы, цифры, пробелы, апострофы, подчёркивания и дефисы.",
+ "nameCannotContainHash": "Имя не может содержать #."
},
"createFaceLibrary": {
"desc": "Создание новой коллекции",
diff --git a/web/public/locales/ru/views/live.json b/web/public/locales/ru/views/live.json
index 3cf017a943..437a44af61 100644
--- a/web/public/locales/ru/views/live.json
+++ b/web/public/locales/ru/views/live.json
@@ -1,5 +1,7 @@
{
- "documentTitle": "Прямой эфир - Frigate",
+ "documentTitle": {
+ "default": "Прямой эфир - Frigate"
+ },
"documentTitle.withCamera": "{{camera}} - Прямой эфир - Frigate",
"lowBandwidthMode": "Экономичный режим",
"twoWayTalk": {
@@ -15,7 +17,8 @@
"clickMove": {
"label": "Кликните в кадре для центрирования камеры",
"enable": "Включить перемещение по клику",
- "disable": "Отключить перемещение по клику"
+ "disable": "Отключить перемещение по клику",
+ "enableWithZoom": "Включить \"клик для перемещения / перетащить для масштабирования\""
},
"left": {
"label": "Переместить PTZ-камеру влево"
diff --git a/web/public/locales/ru/views/motionSearch.json b/web/public/locales/ru/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/ru/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/ru/views/replay.json b/web/public/locales/ru/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/ru/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/ru/views/settings.json b/web/public/locales/ru/views/settings.json
index 504c51178a..cee2a74993 100644
--- a/web/public/locales/ru/views/settings.json
+++ b/web/public/locales/ru/views/settings.json
@@ -12,7 +12,12 @@
"notifications": "Настройки уведомлений - Frigate",
"enrichments": "Настройки обогащения - Frigate",
"cameraManagement": "Управление камерами - Frigate",
- "cameraReview": "Настройки просмотра камеры - Frigate"
+ "cameraReview": "Настройки просмотра камеры - Frigate",
+ "globalConfig": "Глобальная конфигурация - Frigate",
+ "cameraConfig": "Настройки камеры - Frigate",
+ "maintenance": "Обслуживание - Frigate",
+ "profiles": "Профили - Frigate",
+ "detectorsAndModel": "Обнаружения и модель - Frigate"
},
"menu": {
"cameras": "Настройки камеры",
@@ -28,7 +33,44 @@
"triggers": "Триггеры",
"cameraManagement": "Управление",
"cameraReview": "Обзор",
- "roles": "Роли"
+ "roles": "Роли",
+ "general": "Общее",
+ "globalConfig": "Глобальная конфигурация",
+ "system": "Система",
+ "integrations": "Интеграции",
+ "uiSettings": "Настройки интерфейса",
+ "profiles": "Профили",
+ "globalDetect": "Обнаружение объектов",
+ "globalReview": "Обзор событий",
+ "globalAudioEvents": "Обнаружение аудио",
+ "systemAuthentication": "Аутентификация",
+ "systemNetworking": "Сеть",
+ "systemProxy": "Прокси",
+ "systemUi": "Интерфейс",
+ "systemLogging": "Логирование",
+ "systemEnvironmentVariables": "Переменные окружения",
+ "systemTelemetry": "Телеметрия",
+ "systemBirdseye": "Режим Birdseye",
+ "systemFfmpeg": "FFmpeg",
+ "systemDetectorsAndModel": "Детекторы и модель",
+ "systemMqtt": "MQTT",
+ "systemGo2rtcStreams": "потоки go2rtc",
+ "integrationSemanticSearch": "Семантический поиск",
+ "integrationGenerativeAi": "Генеративный ИИ",
+ "integrationFaceRecognition": "Распознавание лиц",
+ "integrationLpr": "Распознавание номерных знаков",
+ "integrationObjectClassification": "Классификация объектов",
+ "integrationAudioTranscription": "Расшифровка аудиозаписи",
+ "cameraDetect": "Обнаружение объектов",
+ "cameraFfmpeg": "Потоки (FFmpeg)",
+ "cameraRecording": "Запись",
+ "cameraSnapshots": "Снимки",
+ "cameraMotion": "Обнаружение движения",
+ "cameraObjects": "Объекты",
+ "cameraConfigReview": "Обзор событий",
+ "cameraAudioEvents": "Обнаружение аудио",
+ "cameraAudioTranscription": "Расшифровка аудиозаписи",
+ "cameraNotifications": "Уведомления"
},
"dialog": {
"unsavedChanges": {
@@ -1190,7 +1232,28 @@
"backToSettings": "Вернуться к настройкам камеры",
"streams": {
"title": "Включить / Отключить камеры",
- "desc": "Временно отключить камеру до перезапуска Frigate. Отключение камеры полностью останавливает обработку потоков этой камеры в Frigate. Обнаружение, запись и отладка будут недоступны. Примечание: Это не отключает рестриминг go2rtc. "
+ "desc": "Временно отключить камеру до перезапуска Frigate. Отключение камеры полностью останавливает обработку потоков этой камеры в Frigate. Обнаружение, запись и отладка будут недоступны. Примечание: Это не отключает рестриминг go2rtc. ",
+ "label": "Состояние камеры",
+ "status": {
+ "disabled": "Отключено"
+ },
+ "enableSuccess": "Включена {{cameraName}}. Перезапустите Frigate для применения изменений.",
+ "disableSuccess": "Отключено {{cameraName}} и сохранено в конфигурации.",
+ "reorderHandle": "Перетяните для изменения порядка",
+ "saving": "Сохранение…",
+ "saved": "Сохранено",
+ "details": {
+ "edit": "Редактировать детали камеры",
+ "title": "Редактировать детали камеры",
+ "description": "Обновите отображаемое имя, внешний URL и видимость для этой камеры в пользовательском интерфейсе Frigate.",
+ "friendlyNameLabel": "Отображаемое Имя",
+ "friendlyNameHelp": "Дружественное имя, которое отображается для этой камеры в пользовательском интерфейсе Frigate. Оставьте пустым для использования идентификатора камеры.",
+ "webuiUrlLabel": "Ссылка на Web интерфейс камеры",
+ "webuiUrlHelp": "URL-адрес для посещения веб-UI камеры непосредственно с отладочного меню. Оставьте пустым, чтобы отключить ссылку.",
+ "webuiUrlInvalid": "Должен быть действительный URL (например, https://example.com).",
+ "dashboardLabel": "Показывать на Live dashboard",
+ "dashboardHelp": "Показывать эту камеру на Live dashboard."
+ }
},
"cameraConfig": {
"add": "Добавить камеру",
@@ -1220,6 +1283,11 @@
"toast": {
"success": "Камера {{cameraName}} успешно сохранена"
}
+ },
+ "deleteCameraDialog": {
+ "confirmWarning": "Удаление {{cameraName}} не может быть отменено.",
+ "success": "Камера {{cameraName}} успешно удалена",
+ "error": "Не удалось удалить камеру {{cameraName}}"
}
},
"cameraReview": {
@@ -1258,5 +1326,11 @@
"success": "Конфигурация классификации обзора была сохранена. Перезапустите Frigate для применения изменений."
}
}
+ },
+ "button": {
+ "overriddenGlobal": "Перезаписано (глобально)",
+ "overriddenGlobalTooltip": "Эта камера перезаписывает глобальные настройки в этой секции",
+ "overriddenBaseConfig": "Перезаписано (базовые настройки)",
+ "overriddenBaseConfigTooltip": "Перезаписи настроек профиля {{profile}} в этой секции"
}
}
diff --git a/web/public/locales/ru/views/system.json b/web/public/locales/ru/views/system.json
index 0317136326..887678a201 100644
--- a/web/public/locales/ru/views/system.json
+++ b/web/public/locales/ru/views/system.json
@@ -7,7 +7,8 @@
"logs": {
"frigate": "Логи Frigate - Frigate",
"go2rtc": "Логи Go2RTC - Frigate",
- "nginx": "Логи Nginx - Frigate"
+ "nginx": "Логи Nginx - Frigate",
+ "websocket": "Логи сообщений - Frigate"
}
},
"title": "Система",
@@ -33,6 +34,27 @@
"fetchingLogsFailed": "Ошибка получения логов: {{errorMessage}}",
"whileStreamingLogs": "Ошибка при потоковой передаче логов: {{errorMessage}}"
}
+ },
+ "websocket": {
+ "label": "Сообщения",
+ "pause": "Пауза",
+ "resume": "Продолжить",
+ "clear": "Очистить",
+ "filter": {
+ "all": "Все топики",
+ "topics": "Топики",
+ "events": "События",
+ "classification": "Классификация",
+ "face_recognition": "Распознавание лиц",
+ "lpr": "Распознавание номерных знаков",
+ "camera_activity": "Активность камеры",
+ "system": "Система",
+ "camera": "Камера",
+ "all_cameras": "Все камеры",
+ "cameras_count_one": "{{count}} камера",
+ "cameras_count_other": "{{count}} камеры"
+ },
+ "empty": "Сообщения ещё не были получены"
}
},
"general": {
diff --git a/web/public/locales/sk/common.json b/web/public/locales/sk/common.json
index 2a23946763..f78c9109e7 100644
--- a/web/public/locales/sk/common.json
+++ b/web/public/locales/sk/common.json
@@ -98,7 +98,7 @@
"kbps": "kB/s",
"mbps": "MB/s",
"gbps": "GB/s",
- "kbph": "kb/hour",
+ "kbph": "kb/hodina",
"mbph": "MB/hodinu",
"gbph": "GB/hodinu"
}
@@ -149,7 +149,16 @@
"unsuspended": "Zrušte pozastavenie",
"play": "Hrať",
"unselect": "Zrušte výber",
- "continue": "Pokračovať"
+ "continue": "Pokračovať",
+ "copiedToClipboard": "Skopírované do schránky",
+ "modified": "Upravené",
+ "overridden": "Prepísané",
+ "resetToGlobal": "Obnoviť na globálne",
+ "resetToDefault": "Obnoviť predvolené",
+ "saveAll": "Uložiť všetko",
+ "savingAll": "Ukladá sa všetko…",
+ "undoAll": "Vrátiť späť všetko",
+ "retry": "Skúste to znova"
},
"menu": {
"system": "Systém",
@@ -252,7 +261,11 @@
},
"review": "Recenzia",
"explore": "Preskúmať",
- "classification": "Klasifikácia"
+ "classification": "Klasifikácia",
+ "profiles": "Profily",
+ "actions": "Akcie",
+ "features": "Vlastnosti",
+ "chat": "Chat"
},
"toast": {
"copyUrlToClipboard": "Adresa URL bola skopírovaná do schránky.",
@@ -261,7 +274,8 @@
"error": {
"title": "Chyba pri ukladaní zmien konfigurácie: {{errorMessage}}",
"noMessage": "Chyba pri ukladaní zmien konfigurácie"
- }
+ },
+ "success": "Zmeny konfigurácie boli úspešne uložené."
}
},
"role": {
@@ -303,5 +317,10 @@
"field": {
"optional": "Voliteľné",
"internalID": "Interné ID Frigate používa v konfigurácii a databáze"
+ },
+ "no_items": "Žiadne položky",
+ "validation_errors": "Chyby overenia",
+ "credentialField": {
+ "savedPlaceholder": "Uložené – pre aktuálne informácie nechajte prázdne"
}
}
diff --git a/web/public/locales/sk/components/camera.json b/web/public/locales/sk/components/camera.json
index e2245bd071..0048c19e91 100644
--- a/web/public/locales/sk/components/camera.json
+++ b/web/public/locales/sk/components/camera.json
@@ -68,7 +68,10 @@
}
},
"birdseye": "Vtáčie oko"
- }
+ },
+ "showAll": "Zobraziť všetky skupiny kamier",
+ "showLess": "Zobraziť menej",
+ "editGroups": "Upraviť skupiny kamier"
},
"debug": {
"options": {
diff --git a/web/public/locales/sk/components/dialog.json b/web/public/locales/sk/components/dialog.json
index 6904bc0d24..6eab29150d 100644
--- a/web/public/locales/sk/components/dialog.json
+++ b/web/public/locales/sk/components/dialog.json
@@ -6,7 +6,8 @@
"title": "Frigate sa reštartuje",
"content": "Táto stránka bude obnovená o {{countdown}} sekúnd.",
"button": "Vynútiť opätovné načítanie teraz"
- }
+ },
+ "description": "Počas reštartu bude Frigate nakrátko zastavený."
},
"explore": {
"plus": {
@@ -64,6 +65,25 @@
"fromTimeline": {
"saveExport": "Uložiť Export",
"previewExport": "Export ukážky"
+ },
+ "case": {
+ "newCaseOption": "Vytvoriť nový prípad",
+ "newCaseNamePlaceholder": "Názov nového prípadu",
+ "newCaseDescriptionPlaceholder": "Popis prípadu",
+ "label": "Prípad",
+ "nonAdminHelp": "Pre tieto exporty bude vytvorený nový prípad.",
+ "placeholder": "Vyberte prípad"
+ },
+ "queueing": "Zaradenie exportu do frontu...",
+ "tabs": {
+ "export": "Jedna kamera",
+ "multiCamera": "Viac kamier"
+ },
+ "multiCamera": {
+ "cameraSelection": "Kamery",
+ "cameraSelectionHelp": "Kamery so sledovanými objektmi v tomto časovom rozsahu sú vopred vybrané",
+ "checkingActivity": "Kontrola aktivity kamery...",
+ "noCameras": "Nie sú k dispozícii žiadne kamery"
}
},
"streaming": {
diff --git a/web/public/locales/sk/config/cameras.json b/web/public/locales/sk/config/cameras.json
index 0967ef424b..f6bfc32594 100644
--- a/web/public/locales/sk/config/cameras.json
+++ b/web/public/locales/sk/config/cameras.json
@@ -1 +1,26 @@
-{}
+{
+ "birdseye": {
+ "order": {
+ "label": "Pozícia",
+ "description": "Číselná pozícia ovládajúca poradie kamery v rozložení Birdseye."
+ }
+ },
+ "detect": {
+ "label": "Detekcia objektov",
+ "enabled": {
+ "label": "Povoliť detekciu objektov"
+ }
+ },
+ "ffmpeg": {
+ "output_args": {
+ "detect": {
+ "label": "Zistiť výstupné argumenty",
+ "description": "Predvolené výstupné argumenty pre detekciu streamov rolí."
+ },
+ "record": {
+ "label": "Zaznamenajte výstupné argumenty",
+ "description": "Predvolené výstupné argumenty pre streamy rolí záznamov."
+ }
+ }
+ }
+}
diff --git a/web/public/locales/sk/config/global.json b/web/public/locales/sk/config/global.json
index 0967ef424b..f6bfc32594 100644
--- a/web/public/locales/sk/config/global.json
+++ b/web/public/locales/sk/config/global.json
@@ -1 +1,26 @@
-{}
+{
+ "birdseye": {
+ "order": {
+ "label": "Pozícia",
+ "description": "Číselná pozícia ovládajúca poradie kamery v rozložení Birdseye."
+ }
+ },
+ "detect": {
+ "label": "Detekcia objektov",
+ "enabled": {
+ "label": "Povoliť detekciu objektov"
+ }
+ },
+ "ffmpeg": {
+ "output_args": {
+ "detect": {
+ "label": "Zistiť výstupné argumenty",
+ "description": "Predvolené výstupné argumenty pre detekciu streamov rolí."
+ },
+ "record": {
+ "label": "Zaznamenajte výstupné argumenty",
+ "description": "Predvolené výstupné argumenty pre streamy rolí záznamov."
+ }
+ }
+ }
+}
diff --git a/web/public/locales/sk/views/chat.json b/web/public/locales/sk/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/sk/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/sk/views/classificationModel.json b/web/public/locales/sk/views/classificationModel.json
index 7b5c0e59c8..8075240248 100644
--- a/web/public/locales/sk/views/classificationModel.json
+++ b/web/public/locales/sk/views/classificationModel.json
@@ -58,7 +58,7 @@
},
"renameCategory": {
"title": "Premenovať Triedu",
- "desc": "Zadajte nový názov pre {{name}}. Budete musieť model pretrénovať, aby sa zmena názvu prejavila."
+ "desc": "Zadajte nové meno pre {{name}}. Budete musieť model pretrénovať, aby sa zmena názvu prejavila."
},
"description": {
"invalidName": "Neplatné meno. Mená môžu obsahovať iba písmená, čísla, medzery, apostrofy, podčiarkovníky a spojovníky."
diff --git a/web/public/locales/sk/views/motionSearch.json b/web/public/locales/sk/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/sk/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/sk/views/replay.json b/web/public/locales/sk/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/sk/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/sk/views/settings.json b/web/public/locales/sk/views/settings.json
index 6a451dc5ff..dbb8faf98a 100644
--- a/web/public/locales/sk/views/settings.json
+++ b/web/public/locales/sk/views/settings.json
@@ -18,7 +18,7 @@
"enrichments": "Obohatenia",
"cameras": "Nastavenia kamier",
"masksAndZones": "Masky / Zóny",
- "motionTuner": "Ladič detekcie pohybu",
+ "motionTuner": "Ladenie detekcie pohybu",
"debug": "Ladenie",
"users": "Uživatelia",
"notifications": "Notifikacie",
@@ -26,7 +26,8 @@
"triggers": "Spúšťače",
"roles": "Roly",
"cameraManagement": "Manažment",
- "cameraReview": "Recenzia"
+ "cameraReview": "Recenzia",
+ "profiles": "Profily"
},
"dialog": {
"unsavedChanges": {
diff --git a/web/public/locales/sl/views/chat.json b/web/public/locales/sl/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/sl/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/sl/views/motionSearch.json b/web/public/locales/sl/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/sl/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/sl/views/replay.json b/web/public/locales/sl/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/sl/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/sq/views/chat.json b/web/public/locales/sq/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/sq/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/sq/views/motionSearch.json b/web/public/locales/sq/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/sq/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/sq/views/replay.json b/web/public/locales/sq/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/sq/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/sr/audio.json b/web/public/locales/sr/audio.json
index fea4e3e77d..3d0bebf44e 100644
--- a/web/public/locales/sr/audio.json
+++ b/web/public/locales/sr/audio.json
@@ -1,24 +1,24 @@
{
- "speech": "Govor",
- "scream": "Vrisak",
- "babbling": "Brbljanje",
- "bicycle": "Bicikla",
- "yell": "Vikati",
- "car": "Automobil",
- "bellow": "Ispod",
- "motorcycle": "Motor",
- "whoop": "Opa",
- "whispering": "Šaptanje",
- "bus": "Autobus",
- "laughter": "Smeh",
- "train": "Voz",
- "boat": "Brod",
- "crying": "Plač",
+ "speech": "Говор",
+ "scream": "Вриска",
+ "babbling": "Брбљање",
+ "bicycle": "Бицикл",
+ "yell": "Викање",
+ "car": "Аутомобил",
+ "bellow": "Рика",
+ "motorcycle": "Мотор",
+ "whoop": "Опа",
+ "whispering": "Шаптање",
+ "bus": "Аутобус",
+ "laughter": "Смех",
+ "train": "Воз",
+ "boat": "Брод",
+ "crying": "Плач",
"sigh": "Уздах",
- "singing": "Пријављивање",
- "choir": "Збор",
+ "singing": "Певање",
+ "choir": "Хор",
"yodeling": "Јодловање",
- "chant": "Певање",
+ "chant": "Појање",
"mantra": "Мантра",
"bird": "Птица",
"snicker": "Кикот",
@@ -37,18 +37,18 @@
"wheeze": "Шиштање",
"snoring": "Хркање",
"gasp": "Задиханост",
- "pant": "Задиханост",
+ "pant": "Дахтање",
"snort": "Шмркање",
"cough": "Кашаљ",
"throat_clearing": "Прочишћавање грла",
"sneeze": "Кијање",
"sniff": "Њушкање",
"run": "Трчање",
- "shuffle": "Насумично",
- "footsteps": "Корачање",
+ "shuffle": "Вучење ногама",
+ "footsteps": "Кораци",
"chewing": "Жвакање",
"biting": "Угриз",
- "gargling": "Гргорење",
+ "gargling": "Гргљање",
"stomach_rumble": "Крчање стомака",
"camera": "Камера",
"burping": "Подригивање",
@@ -62,6 +62,442 @@
"cheering": "Навијање",
"applause": "Аплауз",
"chatter": "Жамор",
- "crowd": "Маса",
- "children_playing": "Деца се играју"
+ "crowd": "Гомила",
+ "children_playing": "Деца се играју",
+ "heart_murmur": "Шум на срцу",
+ "animal": "Животиња",
+ "pets": "Кућни љубимци",
+ "bark": "Лавеж",
+ "yip": "Цвиљење",
+ "howl": "Завијање",
+ "bow_wow": "Ав-ав",
+ "growling": "Режање",
+ "whimper_dog": "Цвиљење пса",
+ "purr": "Предење",
+ "meow": "Мјаукање",
+ "hiss": "Шиштање",
+ "caterwaul": "Мачје дерање",
+ "livestock": "Стока",
+ "clip_clop": "Топот копита",
+ "neigh": "Рзање",
+ "cattle": "Говеда",
+ "moo": "Мукање",
+ "cowbell": "Звоно за краве",
+ "pig": "Свиња",
+ "oink": "Рокћање",
+ "goat": "Коза",
+ "bleat": "Блејање",
+ "fowl": "Живина",
+ "chicken": "Кокошка",
+ "cluck": "Кокодакање",
+ "cock_a_doodle_doo": "Кукурикање",
+ "turkey": "Ћурка",
+ "gobble": "Глоглотање",
+ "duck": "Патка",
+ "quack": "Квакање",
+ "goose": "Гуска",
+ "honk": "Гакање",
+ "wild_animals": "Дивље животиње",
+ "roaring_cats": "Дивље мачке",
+ "roar": "Рика",
+ "chirp": "Цвркут",
+ "squawk": "Крештање",
+ "pigeon": "Голуб",
+ "coo": "Гукање",
+ "crow": "Врана",
+ "caw": "Грактање",
+ "owl": "Сова",
+ "hoot": "Хукање",
+ "flapping_wings": "Лепет крила",
+ "dogs": "Пси",
+ "rats": "Пацови",
+ "mouse": "Миш",
+ "patter": "Тапкање",
+ "insect": "Инсект",
+ "cricket": "Цврчак",
+ "mosquito": "Комарац",
+ "fly": "Мува",
+ "buzz": "Зујање",
+ "frog": "Жаба",
+ "croak": "Крекетање",
+ "snake": "Змија",
+ "rattle": "Звечка",
+ "whale_vocalization": "Вокализација кита",
+ "music": "Музика",
+ "musical_instrument": "Музички инструмент",
+ "plucked_string_instrument": "Жичани инструмент",
+ "guitar": "Гитара",
+ "electric_guitar": "Електрична гитара",
+ "bass_guitar": "Бас гитара",
+ "acoustic_guitar": "Акустична гитара",
+ "steel_guitar": "Челична гитара",
+ "tapping": "Тапкање",
+ "strum": "Брење",
+ "banjo": "Банџо",
+ "sitar": "Ситар",
+ "mandolin": "Мандолина",
+ "zither": "Цитра",
+ "ukulele": "Укулеле",
+ "keyboard": "Клавијатура",
+ "piano": "Клавир",
+ "electric_piano": "Електрични клавир",
+ "organ": "Оргуље",
+ "electronic_organ": "Електронске оргуље",
+ "hammond_organ": "Хамонд оргуље",
+ "synthesizer": "Синтисајзер",
+ "sampler": "Семплер",
+ "harpsichord": "Чембало",
+ "percussion": "Перкусије",
+ "drum_kit": "Бубањ сет",
+ "drum_machine": "Бубањ машина",
+ "drum": "Бубањ",
+ "snare_drum": "Мали бубањ",
+ "rimshot": "Ударац по обруку",
+ "drum_roll": "Бубањ рол",
+ "bass_drum": "Велики бубањ",
+ "timpani": "Тимпани",
+ "tabla": "Табла",
+ "cymbal": "Чинела",
+ "hi_hat": "Хај-хет",
+ "wood_block": "Дрвени блок",
+ "tambourine": "Даире",
+ "maraca": "Маракас",
+ "gong": "Гонг",
+ "tubular_bells": "Тубуларна звона",
+ "mallet_percussion": "Перкусије палицама",
+ "marimba": "Маримба",
+ "glockenspiel": "Глокеншпил",
+ "vibraphone": "Вибрафон",
+ "steelpan": "Стилпан",
+ "orchestra": "Оркестар",
+ "brass_instrument": "Лимени инструмент",
+ "french_horn": "Хорна",
+ "trumpet": "Труба",
+ "trombone": "Тромбон",
+ "bowed_string_instrument": "Гудачки инструмент",
+ "string_section": "Гудачка секција",
+ "violin": "Виолина",
+ "pizzicato": "Пицикато",
+ "cello": "Виолончело",
+ "double_bass": "Контрабас",
+ "wind_instrument": "Дувачки инструмент",
+ "flute": "Флаута",
+ "saxophone": "Саксофон",
+ "clarinet": "Кларинет",
+ "harp": "Харфа",
+ "bell": "Звоно",
+ "church_bell": "Црквено звоно",
+ "jingle_bell": "Прапорац",
+ "bicycle_bell": "Звоно бицикла",
+ "tuning_fork": "Виљушка за штимовање",
+ "chime": "Звоњава",
+ "wind_chime": "Ветрено звонце",
+ "harmonica": "Усна хармоника",
+ "accordion": "Хармоника",
+ "bagpipes": "Гајде",
+ "didgeridoo": "Диџериду",
+ "theremin": "Теремин",
+ "singing_bowl": "Тибетанска здела",
+ "scratching": "Скречовање",
+ "pop_music": "Поп музика",
+ "hip_hop_music": "Хип-хоп музика",
+ "beatboxing": "Битбоксовање",
+ "rock_music": "Рок музика",
+ "heavy_metal": "Хеви метал",
+ "punk_rock": "Панк рок",
+ "grunge": "Гранџ",
+ "progressive_rock": "Прогресивни рок",
+ "rock_and_roll": "Рок енд рол",
+ "psychedelic_rock": "Психоделични рок",
+ "rhythm_and_blues": "Ритам и блуз",
+ "soul_music": "Соул музика",
+ "reggae": "Реге",
+ "country": "Кантри",
+ "swing_music": "Свинг музика",
+ "bluegrass": "Блуграс",
+ "funk": "Фанк",
+ "folk_music": "Народна музика",
+ "middle_eastern_music": "Блискоисточна музика",
+ "jazz": "Џез",
+ "disco": "Диско",
+ "classical_music": "Класична музика",
+ "opera": "Опера",
+ "electronic_music": "Електронска музика",
+ "house_music": "Хаус музика",
+ "techno": "Техно",
+ "dubstep": "Дабстеп",
+ "drum_and_bass": "Драм енд бејс",
+ "electronica": "Електроника",
+ "electronic_dance_music": "Електронска плесна музика",
+ "ambient_music": "Амбијент музика",
+ "trance_music": "Транс музика",
+ "music_of_latin_america": "Латиноамеричка музика",
+ "salsa_music": "Салса музика",
+ "flamenco": "Фламенко",
+ "blues": "Блуз",
+ "music_for_children": "Музика за децу",
+ "new-age_music": "Њу ејџ музика",
+ "vocal_music": "Вокална музика",
+ "a_capella": "А капела",
+ "music_of_africa": "Афричка музика",
+ "afrobeat": "Афробит",
+ "christian_music": "Хришћанска музика",
+ "gospel_music": "Госпел музика",
+ "music_of_asia": "Азијска музика",
+ "carnatic_music": "Карнатска музика",
+ "music_of_bollywood": "Боливудска музика",
+ "ska": "Ска",
+ "traditional_music": "Традиционална музика",
+ "independent_music": "Независна музика",
+ "song": "Песма",
+ "background_music": "Позадинска музика",
+ "theme_music": "Тематска музика",
+ "jingle": "Џингл",
+ "soundtrack_music": "Музика филмског записа",
+ "lullaby": "Успаванка",
+ "video_game_music": "Музика видео игара",
+ "christmas_music": "Божићна музика",
+ "dance_music": "Плесна музика",
+ "wedding_music": "Свадбена музика",
+ "happy_music": "Весела музика",
+ "sad_music": "Тужна музика",
+ "tender_music": "Нежна музика",
+ "exciting_music": "Узбудљива музика",
+ "angry_music": "Бесна музика",
+ "scary_music": "Страшна музика",
+ "wind": "Ветар",
+ "rustling_leaves": "Шуштање лишћа",
+ "wind_noise": "Шум ветра",
+ "thunderstorm": "Олуја",
+ "thunder": "Гром",
+ "water": "Вода",
+ "rain": "Киша",
+ "raindrop": "Кап кише",
+ "rain_on_surface": "Киша на површини",
+ "stream": "Поток",
+ "waterfall": "Водопад",
+ "ocean": "Океан",
+ "waves": "Таласи",
+ "steam": "Пара",
+ "gurgling": "Клокотање",
+ "fire": "Ватра",
+ "crackle": "Пуцкетање",
+ "vehicle": "Возило",
+ "sailboat": "Једрилица",
+ "rowboat": "Чамац",
+ "motorboat": "Моторни чамац",
+ "ship": "Лађа",
+ "motor_vehicle": "Моторно возило",
+ "toot": "Труба",
+ "car_alarm": "Аларм аутомобила",
+ "power_windows": "Електрични прозори",
+ "skidding": "Проклизавање",
+ "tire_squeal": "Шкрипа гума",
+ "car_passing_by": "Пролазак аутомобила",
+ "race_car": "Тркачки аутомобил",
+ "truck": "Камион",
+ "air_brake": "Ваздушна кочница",
+ "air_horn": "Ваздушна сирена",
+ "reversing_beeps": "Звук вожње уназад",
+ "ice_cream_truck": "Камион за сладолед",
+ "emergency_vehicle": "Возило хитних служби",
+ "police_car": "Полицијски аутомобил",
+ "ambulance": "Амбулантно возило",
+ "fire_engine": "Ватрогасно возило",
+ "traffic_noise": "Шум саобраћаја",
+ "rail_transport": "Железнички саобраћај",
+ "train_whistle": "Звиждук воза",
+ "train_horn": "Сирена воза",
+ "railroad_car": "Железнички вагон",
+ "train_wheels_squealing": "Шкрипа точкова воза",
+ "subway": "Метро",
+ "aircraft": "Авион",
+ "aircraft_engine": "Мотор авиона",
+ "jet_engine": "Млазни мотор",
+ "propeller": "Пропелер",
+ "helicopter": "Хеликоптер",
+ "fixed-wing_aircraft": "Авион са фиксним крилима",
+ "engine": "Мотор",
+ "light_engine": "Лаки мотор",
+ "dental_drill's_drill": "Зубарска бушилица",
+ "lawn_mower": "Косилица",
+ "chainsaw": "Моторна тестера",
+ "medium_engine": "Средњи мотор",
+ "heavy_engine": "Тешки мотор",
+ "engine_knocking": "Лупање мотора",
+ "engine_starting": "Покретање мотора",
+ "idling": "Празан ход",
+ "accelerating": "Убрзавање",
+ "door": "Врата",
+ "doorbell": "Звоно на вратима",
+ "ding-dong": "Динг-донг",
+ "sliding_door": "Клизна врата",
+ "slam": "Лупање",
+ "knock": "Куцање",
+ "tap": "Тапкање",
+ "squeak": "Шкрипа",
+ "cupboard_open_or_close": "Отварање или затварање ормарића",
+ "drawer_open_or_close": "Отварање или затварање фиоке",
+ "dishes": "Судови",
+ "cutlery": "Прибор за јело",
+ "chopping": "Сецкање",
+ "frying": "Пржење",
+ "microwave_oven": "Микроталасна пећница",
+ "blender": "Блендер",
+ "water_tap": "Славина",
+ "sink": "Судопера",
+ "bathtub": "Када",
+ "hair_dryer": "Фен за косу",
+ "toilet_flush": "Пуштање воде",
+ "toothbrush": "Четкица за зубе",
+ "electric_toothbrush": "Електрична четкица за зубе",
+ "vacuum_cleaner": "Усисивач",
+ "zipper": "Рајсфершлус",
+ "keys_jangling": "Звецкање кључева",
+ "coin": "Новчић",
+ "scissors": "Маказе",
+ "electric_shaver": "Електрични бријач",
+ "shuffling_cards": "Мешање карата",
+ "typing": "Куцање",
+ "typewriter": "Писаћа машина",
+ "computer_keyboard": "Тастатура",
+ "writing": "Писање",
+ "alarm": "Аларм",
+ "telephone": "Телефон",
+ "telephone_bell_ringing": "Звоњава телефона",
+ "ringtone": "Мелодија звона",
+ "telephone_dialing": "Бирање броја",
+ "dial_tone": "Тон за бирање",
+ "busy_signal": "Заузет сигнал",
+ "alarm_clock": "Будилник",
+ "siren": "Сирена",
+ "civil_defense_siren": "Сирена цивилне заштите",
+ "buzzer": "Зујалица",
+ "smoke_detector": "Детектор дима",
+ "fire_alarm": "Противпожарни аларм",
+ "foghorn": "Сирена за маглу",
+ "whistle": "Звиждук",
+ "steam_whistle": "Парна пиштаљка",
+ "mechanisms": "Механизми",
+ "ratchet": "Чегртаљка",
+ "clock": "Сат",
+ "tick": "Откуцај",
+ "tick-tock": "Тик-так",
+ "gears": "Зупчаници",
+ "pulleys": "Кајише",
+ "sewing_machine": "Шиваћа машина",
+ "mechanical_fan": "Вентилатор",
+ "air_conditioning": "Клима уређај",
+ "cash_register": "Каса",
+ "printer": "Штампач",
+ "single-lens_reflex_camera": "Рефлексни фотоапарат",
+ "tools": "Алати",
+ "hammer": "Чекић",
+ "jackhammer": "Пнеуматска бушилица",
+ "sawing": "Тестерисање",
+ "filing": "Турпијање",
+ "sanding": "Брушење",
+ "power_tool": "Електрични алат",
+ "drill": "Бушилица",
+ "explosion": "Експлозија",
+ "gunshot": "Пуцањ",
+ "machine_gun": "Митраљез",
+ "fusillade": "Рафал",
+ "artillery_fire": "Артиљеријска паљба",
+ "cap_gun": "Капислара",
+ "fireworks": "Ватромет",
+ "firecracker": "Петарда",
+ "burst": "Прскање",
+ "eruption": "Ерупција",
+ "boom": "Бум",
+ "wood": "Дрво",
+ "chop": "Сечење",
+ "splinter": "Пуцање дрвета",
+ "crack": "Прасак",
+ "glass": "Стакло",
+ "chink": "Звецкање",
+ "shatter": "Разбијање",
+ "silence": "Тишина",
+ "sound_effect": "Звучни ефекат",
+ "environmental_noise": "Амбијентална бука",
+ "static": "Статички шум",
+ "white_noise": "Бели шум",
+ "pink_noise": "Розе шум",
+ "television": "Телевизија",
+ "radio": "Радио",
+ "field_recording": "Теренски снимак",
+ "sodeling": "Напевавање",
+ "chird": "Цвркут",
+ "change_ringing": "Звоњава на промену",
+ "shofar": "Шофар",
+ "liquid": "Течност",
+ "splash": "Пљусак",
+ "slosh": "Шљапкање",
+ "squish": "Гњечење",
+ "drip": "Капање",
+ "pour": "Сипање",
+ "trickle": "Цурење",
+ "gush": "Млаз",
+ "fill": "Пуњење",
+ "spray": "Прскање",
+ "pump": "Пумпа",
+ "stir": "Мешање",
+ "boiling": "Кључање",
+ "sonar": "Сонар",
+ "arrow": "Стрела",
+ "whoosh": "Фијук",
+ "thump": "Тупи ударац",
+ "thunk": "Тупо лупање",
+ "electronic_tuner": "Електронски штимер",
+ "effects_unit": "Јединица за ефекте",
+ "chorus_effect": "Хорус ефекат",
+ "basketball_bounce": "Одбијање лопте",
+ "bang": "Тресак",
+ "slap": "Шамар",
+ "whack": "Ударац",
+ "smash": "Лом",
+ "breaking": "Ломљење",
+ "bouncing": "Одскакање",
+ "whip": "Бич",
+ "flap": "Лепет",
+ "scratch": "Гребање",
+ "scrape": "Стругање",
+ "rub": "Трљање",
+ "roll": "Котрљање",
+ "crushing": "Мрвљење",
+ "crumpling": "Гужвање",
+ "tearing": "Цепање",
+ "beep": "Бип",
+ "ping": "Пинг",
+ "ding": "Динг",
+ "clang": "Звекет",
+ "squeal": "Цвиљење",
+ "creak": "Шкрипање",
+ "rustle": "Шуштање",
+ "whir": "Бруј",
+ "clatter": "Клопарање",
+ "sizzle": "Цврчање",
+ "clicking": "Клацкање",
+ "clickety_clack": "Клик-клак",
+ "rumble": "Тутњава",
+ "plop": "Пљус",
+ "hum": "Брујање",
+ "zing": "Зврндање",
+ "boing": "Бојнг",
+ "crunch": "Крцкање",
+ "sine_wave": "Синусни талас",
+ "harmonic": "Хармоник",
+ "chirp_tone": "Цвркут тон",
+ "pulse": "Пулс",
+ "inside": "Унутра",
+ "outside": "Напољу",
+ "reverberation": "Реверберација",
+ "echo": "Ехо",
+ "noise": "Бука",
+ "mains_hum": "Брујање мреже",
+ "distortion": "Дисторзија",
+ "sidetone": "Бочни тон",
+ "cacophony": "Какофонија",
+ "throbbing": "Пулсирање",
+ "vibration": "Вибрација"
}
diff --git a/web/public/locales/sr/components/dialog.json b/web/public/locales/sr/components/dialog.json
index a97f7c4b6b..c4cd6401e4 100644
--- a/web/public/locales/sr/components/dialog.json
+++ b/web/public/locales/sr/components/dialog.json
@@ -98,7 +98,7 @@
"confirmDelete": {
"title": "Потврдите брисање",
"desc": {
- "selected": "Да ли сте сигурни да желите да обришете све видео снимке повезане са овом ставком? Држите притиснут Shift тастер да прескочите овај дијалог у будућности."
+ "selected": "Да ли сте сигурни да желите да обришете све видео снимке повезане са овом ставком? Држите притиснут Shift тастер да прескочите овај дијалог у будућности."
},
"toast": {
"success": "Видео снимак повезан са изабраним ставкама за преглед успешно је обрисан.",
diff --git a/web/public/locales/sr/objects.json b/web/public/locales/sr/objects.json
index e6683036a6..cf99ee993a 100644
--- a/web/public/locales/sr/objects.json
+++ b/web/public/locales/sr/objects.json
@@ -1,12 +1,12 @@
{
"person": "Osoba",
- "bicycle": "Bicikla",
- "car": "Automobil",
- "motorcycle": "Motor",
+ "bicycle": "Бицикл",
+ "car": "Аутомобил",
+ "motorcycle": "Мотор",
"airplane": "Avion",
- "bus": "Autobus",
- "train": "Voz",
- "boat": "Brod",
+ "bus": "Аутобус",
+ "train": "Воз",
+ "boat": "Брод",
"traffic_light": "Семафор",
"fire_hydrant": "Хидрант",
"street_sign": "Улични знак",
@@ -50,5 +50,18 @@
"spoon": "Кашика",
"bowl": "Посуда",
"banana": "Банана",
- "apple": "Јабука"
+ "apple": "Јабука",
+ "animal": "Животиња",
+ "bark": "Лавеж",
+ "goat": "Коза",
+ "mouse": "Миш",
+ "keyboard": "Клавијатура",
+ "vehicle": "Возило",
+ "door": "Врата",
+ "blender": "Блендер",
+ "sink": "Судопера",
+ "hair_dryer": "Фен за косу",
+ "toothbrush": "Четкица за зубе",
+ "scissors": "Маказе",
+ "clock": "Сат"
}
diff --git a/web/public/locales/sr/views/chat.json b/web/public/locales/sr/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/sr/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/sr/views/motionSearch.json b/web/public/locales/sr/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/sr/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/sr/views/replay.json b/web/public/locales/sr/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/sr/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/sv/common.json b/web/public/locales/sv/common.json
index a60cfb2afd..458cb287d9 100644
--- a/web/public/locales/sv/common.json
+++ b/web/public/locales/sv/common.json
@@ -114,7 +114,19 @@
"download": "Ladda ner",
"info": "Info",
"export": "Exportera",
- "continue": "Fortsätta"
+ "continue": "Fortsätta",
+ "add": "Lägg till",
+ "applying": "Verkställer…",
+ "undo": "Ångra",
+ "copiedToClipboard": "Kopieras till urklipp",
+ "modified": "Modifiera",
+ "overridden": "Åsidosatt",
+ "resetToGlobal": "Återställ till Global",
+ "resetToDefault": "Återställ till standard",
+ "saveAll": "Spara alla",
+ "savingAll": "Sparar alla…",
+ "retry": "Försök igen",
+ "undoAll": "Ångra alla"
},
"menu": {
"language": {
@@ -160,7 +172,9 @@
"gl": "Galego (Galiciska)",
"id": "Bahasa Indonesia (Indonesiska)",
"ur": "اردو (Urdu)",
- "hr": "Hrvatski (kroatiska)"
+ "hr": "Hrvatski (kroatiska)",
+ "zhHant": "繁體中文 (Traditionell kinesisk)",
+ "bs": "Bosanski (Bosniska)"
},
"darkMode": {
"withSystem": {
@@ -207,7 +221,7 @@
"help": "Hjälp",
"documentation": {
"title": "Dokumentation",
- "label": "Frigate dokumentation"
+ "label": "Frigate-dokumentation"
},
"uiPlayground": "UI Testmiljö",
"restart": "Starta om Frigate",
@@ -216,7 +230,11 @@
"configurationEditor": "Konfigurationsredigerare",
"withSystem": "System",
"appearance": "Utseende",
- "classification": "Klassificering"
+ "classification": "Klassificering",
+ "profiles": "Profiler",
+ "actions": "Åtgärder",
+ "features": "Funktioner",
+ "chat": "Chatt"
},
"pagination": {
"next": {
@@ -252,7 +270,8 @@
"error": {
"title": "Misslyckades med att spara konfigurationsändringar: {{errorMessage}}",
"noMessage": "Misslyckades med att spara konfigurationsändringar"
- }
+ },
+ "success": "Konfigurationsändringarna har sparats."
},
"copyUrlToClipboard": "Webbadressen har kopierats till urklipp."
},
@@ -296,5 +315,10 @@
"field": {
"optional": "Valfritt",
"internalID": "Det interna ID som Frigate använder i konfigurationen och databasen"
+ },
+ "no_items": "Inga artiklar",
+ "validation_errors": "Valideringsfel",
+ "credentialField": {
+ "savedPlaceholder": "Sparad — lämna tomt för att hålla dig aktuell"
}
}
diff --git a/web/public/locales/sv/components/camera.json b/web/public/locales/sv/components/camera.json
index 23de9471f6..85996ef221 100644
--- a/web/public/locales/sv/components/camera.json
+++ b/web/public/locales/sv/components/camera.json
@@ -17,7 +17,7 @@
"errorMessage": {
"mustLeastCharacters": "Gruppnamnet för kameror måste vara minst 2 tecken.",
"nameMustNotPeriod": "Kameragruppnamnet får inte innehålla en punkt.",
- "invalid": "Ogiltligt kameragruppnamn.",
+ "invalid": "Ogiltigt kameragruppnamn.",
"exists": "Kameragruppnamnet finns redan."
}
},
@@ -28,7 +28,7 @@
"title": "{{cameraName}} Streaminginställningar",
"desc": "Ändra alternativen för livestreaming för den här kameragruppens instrumentpanel. Dessa inställningar är enhets-/webbläsarspecifika. ",
"audioIsAvailable": "Ljud är tillgängligt för denna kameraström",
- "audioIsUnavailable": "Ljud är otillgängligt för denna kameraström",
+ "audioIsUnavailable": "Ljud är inte tillgängligt för den här kameraströmmen",
"audio": {
"tips": {
"title": "Ljud måste sändas från din kamera och konfigureras i go2rtc för den här strömmen.",
@@ -68,7 +68,10 @@
"cameras": {
"desc": "Välj kameror för denna guppen.",
"label": "Kameror"
- }
+ },
+ "showAll": "Visa alla kameragrupper",
+ "showLess": "Visa mindre",
+ "editGroups": "Redigera kameragrupper"
},
"debug": {
"options": {
@@ -82,6 +85,7 @@
"zones": "Zoner",
"mask": "Maskera",
"motion": "Rörelse",
- "regions": "Regioner"
+ "regions": "Regioner",
+ "paths": "Vägar"
}
}
diff --git a/web/public/locales/sv/components/dialog.json b/web/public/locales/sv/components/dialog.json
index 4b8899a2c3..5fc3794bd9 100644
--- a/web/public/locales/sv/components/dialog.json
+++ b/web/public/locales/sv/components/dialog.json
@@ -17,9 +17,9 @@
},
"review": {
"question": {
- "ask_a": "Är detta objektet {{label}}?",
- "ask_an": "Är detta objektet en {{label}}?",
- "ask_full": "Är detta objektet {{untranslatedLabel}} ({{translatedLabel}})?",
+ "ask_a": "Är detta objektet en/ett {{label}}?",
+ "ask_an": "Är detta objekt en/ett {{label}}?",
+ "ask_full": "Är detta objektet en/ett {{untranslatedLabel}} ({{translatedLabel}})?",
"label": "Bekräfta denna etikett för Frigate Plus"
},
"state": {
@@ -50,20 +50,81 @@
"placeholder": "Ge exporten ett namn"
},
"select": "Välj",
- "export": "Eksport",
+ "export": "Export",
"selectOrExport": "Välj eller exportera",
"toast": {
"success": "Exporten har startats. Visa filen på exportsidan.",
"error": {
- "failed": "Misslyckades med att starta exporten: {{error}}",
+ "failed": "Misslyckades med att köa exporten: {{error}}",
"endTimeMustAfterStartTime": "Sluttiden måste vara efter starttiden",
"noVaildTimeSelected": "Inget giltigt tidsintervall valt"
},
- "view": "Visa"
+ "view": "Visa",
+ "queued": "Exporten är i kö. Se förloppet på exportsidan.",
+ "batchSuccess_one": "Startade 1 export. Öppnar ärendet nu.",
+ "batchSuccess_other": "Startade {{count}} exporter. Öppnar ärendet nu.",
+ "batchPartial": "Startade {{successful}} av {{total}} exporter. Misslyckade kameror: {{failedCameras}}",
+ "batchFailed": "Misslyckades med att starta export av {{total}}. Kameror som inte fungerade: {{failedCameras}}",
+ "batchQueuedSuccess_one": "1 export har köats. Ärendet öppnas nu.",
+ "batchQueuedSuccess_other": "{{count}} exporter har köats. Öppnar ärendet nu.",
+ "batchQueuedPartial": "Köade {{successful}} av {{total}} exporter. Misslyckade kameror: {{failedCameras}}",
+ "batchQueueFailed": "Misslyckades med att köa {{total}} exporter. Felaktiga kameror: {{failedCameras}}"
},
"fromTimeline": {
"saveExport": "Spara export",
- "previewExport": "Förhandsgranska export"
+ "previewExport": "Förhandsgranska export",
+ "queueingExport": "Export i kö...",
+ "useThisRange": "Använd detta intervall"
+ },
+ "case": {
+ "newCaseOption": "Skapa nytt ärende",
+ "newCaseNamePlaceholder": "Nytt ärende namn",
+ "newCaseDescriptionPlaceholder": "Ärende beskrivning",
+ "label": "Ärende",
+ "nonAdminHelp": "Ett nytt ärende kommer att skapas för dessa exporter.",
+ "placeholder": "Välj ett ärende"
+ },
+ "queueing": "Export i kö...",
+ "tabs": {
+ "export": "Enskild kamera",
+ "multiCamera": "Flerkameror"
+ },
+ "multiCamera": {
+ "timeRange": "Tidsintervall",
+ "selectFromTimeline": "Välj från tidslinjen",
+ "cameraSelection": "Kameror",
+ "cameraSelectionHelp": "Kameror med spårade objekt inom detta tidsintervall är förvalda",
+ "checkingActivity": "Kontrollerar kameraaktivitet...",
+ "noCameras": "Inga kameror tillgängliga",
+ "detectionCount_one": "1 spårat objekt",
+ "detectionCount_other": "{{count}} spårade objekt",
+ "nameLabel": "Exportnamn",
+ "namePlaceholder": "Valfritt basnamn för dessa exporter",
+ "queueingButton": "Köar exporter...",
+ "exportButton_one": "Exportera 1 kamera",
+ "exportButton_other": "Exportera {{count}} kameror",
+ "selectAll": "Välj alla kameror",
+ "selectWithActivity": "Kameror med spårade objekt",
+ "selectGroup": "Välj grupp",
+ "noMatchingCameras": "Ingen kamera matchar din sökning"
+ },
+ "multi": {
+ "title_one": "Exportera 1 recension",
+ "title_other": "Exportera {{count}} recensioner",
+ "description": "Exportera varje vald recension. Alla exporter grupperas under ett enda ärende.",
+ "descriptionNoCase": "Exportera varje vald recension.",
+ "caseNamePlaceholder": "Granska export - {{date}}",
+ "exportButton_one": "Exportera 1 recension",
+ "exportButton_other": "Exportera {{count}} recensioner",
+ "exportingButton": "Exporterar...",
+ "toast": {
+ "started_one": "Startade 1 export. Öppnar ärendet nu.",
+ "started_other": "Startade {{count}} exporter. Öppnar ärendet nu.",
+ "startedNoCase_one": "Startade 1 export.",
+ "startedNoCase_other": "Startade {{count}} exporter.",
+ "partial": "Startade {{successful}} av {{total}} exporter. Misslyckades: {{failedItems}}",
+ "failed": "Misslyckades med att starta exporten av {{total}}. Misslyckades: {{failedItems}}"
+ }
}
},
"streaming": {
@@ -111,6 +172,14 @@
"markAsReviewed": "Markera som granskad",
"deleteNow": "Ta bort nu",
"markAsUnreviewed": "Markera som ogranskad"
+ },
+ "shareTimestamp": {
+ "label": "Dela tidsstämpel",
+ "title": "Dela tidsstämpel",
+ "description": "Dela en tidsstämplad URL för spelarens nuvarande position eller välj en anpassad tidsstämpel. Observera att detta inte är en offentlig delnings-URL och endast är tillgänglig för användare med åtkomst till Frigate och den här kameran.",
+ "custom": "Anpassad tidsstämpel",
+ "button": "Dela tidsstämpel-URL",
+ "shareTitle": "Frigate granskning tidsstämpel: {{camera}}"
}
},
"imagePicker": {
diff --git a/web/public/locales/sv/components/filter.json b/web/public/locales/sv/components/filter.json
index efb50d919c..846b2f07c2 100644
--- a/web/public/locales/sv/components/filter.json
+++ b/web/public/locales/sv/components/filter.json
@@ -1,13 +1,13 @@
{
"labels": {
"all": {
- "title": "Alla Etiketter",
+ "title": "Alla etiketter",
"short": "Etiketter"
},
"label": "Etiketter",
"count": "{{count}} Etiketter",
"count_one": "{{count}} Etikett",
- "count_other": "{{count}} Etiketter"
+ "count_other": "{{count}} etiketter"
},
"filter": "Filtrera",
"zones": {
@@ -49,7 +49,7 @@
"defaultView": {
"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.",
+ "desc": "När inga filter är valda, visa en översikt av de senaste spårade objekten per etikett eller visa ett ofiltrerat rutnät.",
"unfilteredGrid": "Ofiltrerat Rutnät"
},
"searchSource": {
@@ -95,7 +95,7 @@
"selectAll": "Välj alla",
"clearAll": "Rensa alla"
},
- "more": "Flera filter",
+ "more": "Fler filter",
"reset": {
"label": "Nollställ filter"
},
@@ -121,7 +121,7 @@
"filterBySeverity": "Filtrera logg på allvarlighetsgrad",
"disableLogStreaming": "Inaktivera strömning av logg",
"allLogs": "Alla loggar",
- "label": "Filter loggnivå"
+ "label": "Filtrera loggnivå"
},
"trackedObjectDelete": {
"title": "Bekräfta Borttagning",
diff --git a/web/public/locales/sv/components/player.json b/web/public/locales/sv/components/player.json
index 7c6301ca11..6e0cd60c35 100644
--- a/web/public/locales/sv/components/player.json
+++ b/web/public/locales/sv/components/player.json
@@ -4,7 +4,8 @@
"noPreviewFoundFor": "Ingen förhandsvisning hittad för {{cameraName}}",
"submitFrigatePlus": {
"title": "Skicka denna bild till Frigate+?",
- "submit": "Skicka"
+ "submit": "Skicka",
+ "previewError": "Det gick inte att ladda förhandsgranskningen av ögonblicksbilden. Inspelningen är kanske inte tillgänglig just nu."
},
"livePlayerRequiredIOSVersion": "iOS 17.1 eller senare krävs för den här typen av livestream.",
"streamOffline": {
@@ -47,5 +48,6 @@
"success": {
"submittedFrigatePlus": "Bildruta har skickats till Frigate+ med lyckat resultat"
}
- }
+ },
+ "cameraOff": "Kameran är avstängd"
}
diff --git a/web/public/locales/sv/config/cameras.json b/web/public/locales/sv/config/cameras.json
index 0967ef424b..6b59c41908 100644
--- a/web/public/locales/sv/config/cameras.json
+++ b/web/public/locales/sv/config/cameras.json
@@ -1 +1,29 @@
-{}
+{
+ "label": "KameraKonfiguration",
+ "name": {
+ "label": "Kameranamn",
+ "description": "Kameranamn krävs"
+ },
+ "friendly_name": {
+ "label": "Visningsnamn",
+ "description": "Visningsnamn för kamera i Frigate UI"
+ },
+ "enabled": {
+ "label": "Aktiverad",
+ "description": "Aktiverad"
+ },
+ "audio": {
+ "label": "Ljuddetektering",
+ "description": "Inställningar för ljudbaserad händelsedetektering för denna kamera.",
+ "enabled": {
+ "label": "Aktivera ljuddetektering",
+ "description": "Aktivera eller avaktivera ljudbaserad detektering för denna kamera."
+ },
+ "max_not_heard": {
+ "description": "Antal sekunder utan den konfigurerade ljudtypen innan en ljudbaserad händelse slutar."
+ },
+ "min_volume": {
+ "label": "Minsta ljudvolym"
+ }
+ }
+}
diff --git a/web/public/locales/sv/config/global.json b/web/public/locales/sv/config/global.json
index 0967ef424b..ae266a8280 100644
--- a/web/public/locales/sv/config/global.json
+++ b/web/public/locales/sv/config/global.json
@@ -1 +1,17 @@
-{}
+{
+ "version": {
+ "label": "Nuvarande konfigurations version"
+ },
+ "audio": {
+ "label": "Ljuddetektering",
+ "enabled": {
+ "label": "Aktivera ljuddetektering"
+ },
+ "max_not_heard": {
+ "description": "Antal sekunder utan den konfigurerade ljudtypen innan en ljudbaserad händelse slutar."
+ },
+ "min_volume": {
+ "label": "Minsta ljudvolym"
+ }
+ }
+}
diff --git a/web/public/locales/sv/config/groups.json b/web/public/locales/sv/config/groups.json
index 0967ef424b..4a81abf8e8 100644
--- a/web/public/locales/sv/config/groups.json
+++ b/web/public/locales/sv/config/groups.json
@@ -1 +1,7 @@
-{}
+{
+ "audio": {
+ "global": {
+ "sensitivity": "Global känslighet"
+ }
+ }
+}
diff --git a/web/public/locales/sv/config/validation.json b/web/public/locales/sv/config/validation.json
index 0967ef424b..23e4d27483 100644
--- a/web/public/locales/sv/config/validation.json
+++ b/web/public/locales/sv/config/validation.json
@@ -1 +1,3 @@
-{}
+{
+ "minimum": "Måste minst vara {{limit}}"
+}
diff --git a/web/public/locales/sv/views/chat.json b/web/public/locales/sv/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/sv/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/sv/views/classificationModel.json b/web/public/locales/sv/views/classificationModel.json
index f4ac4b2efc..5a66ca2246 100644
--- a/web/public/locales/sv/views/classificationModel.json
+++ b/web/public/locales/sv/views/classificationModel.json
@@ -12,10 +12,10 @@
},
"toast": {
"success": {
- "deletedCategory_one": "Borttagen klass",
- "deletedCategory_other": "",
- "deletedImage_one": "Raderade bilder",
- "deletedImage_other": "",
+ "deletedCategory_one": "Tog bort {{count}} klass",
+ "deletedCategory_other": "Tog bort {{count}} klasser",
+ "deletedImage_one": "Tog bort {{count}} bild",
+ "deletedImage_other": "Tog bort {{count}} bilder",
"categorizedImage": "Lyckades klassificera bilden",
"trainedModel": "Modellen har tränats.",
"trainingModel": "Modellträning har startat.",
@@ -28,7 +28,7 @@
"deleteImageFailed": "Misslyckades med att ta bort: {{errorMessage}}",
"deleteCategoryFailed": "Misslyckades med att ta bort klassen: {{errorMessage}}",
"categorizeFailed": "Misslyckades med att kategorisera bilden: {{errorMessage}}",
- "trainingFailed": "Modellträningen misslyckades. Kontrollera Frigate loggarna för mer information.",
+ "trainingFailed": "Modellträningen misslyckades. Kontrollera Frigate-loggarna för mer information.",
"deleteModelFailed": "Misslyckades med att ta bort modellen: {{errorMessage}}",
"updateModelFailed": "Misslyckades med att uppdatera modell: {{errorMessage}}",
"trainingFailedToStart": "Misslyckades med att starta modellträning: {{errorMessage}}",
diff --git a/web/public/locales/sv/views/explore.json b/web/public/locales/sv/views/explore.json
index b6355ea2c1..37ce216681 100644
--- a/web/public/locales/sv/views/explore.json
+++ b/web/public/locales/sv/views/explore.json
@@ -6,11 +6,11 @@
"startingUp": "Startar upp…",
"estimatedTime": "Beräknad återstående tid:",
"finishingShortly": "Snart klar",
- "context": "Utforskaren kan användas efter inbäddade spårade objekt har slutat återindexerat.",
+ "context": "Utforskaren kan användas när inbäddningarna för spårade objekt har indexerats om.",
"step": {
- "thumbnailsEmbedded": "Miniatyrbilder inbäddad: ",
+ "thumbnailsEmbedded": "Inbäddade miniatyrbilder: ",
"descriptionsEmbedded": "Beskrivningar inbäddade: ",
- "trackedObjectsProcessed": "Spårade objekt bearbetad: "
+ "trackedObjectsProcessed": "Bearbetade spårade objekt: "
}
},
"title": "Utforska är inte tillgänglig",
@@ -68,8 +68,8 @@
},
"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"
+ "desc": "Ange ett nytt registreringsnummer för detta {{label}}-objekt",
+ "descNoLabel": "Ange ett nytt registreringsnummer för det här spårade objektet"
},
"snapshotScore": {
"label": "Ögonblicksbildspoäng"
@@ -81,7 +81,7 @@
"score": {
"label": "Poäng"
},
- "recognizedLicensePlate": "Erkänd nummerplåt",
+ "recognizedLicensePlate": "Identifierad registreringsskylt",
"estimatedSpeed": "Uppskattad hastighet",
"objects": "Objekt",
"camera": "Kamera",
@@ -289,7 +289,7 @@
"label": "Annoteringsförskjutning",
"desc": "Denna data kommer från din kameras detekteringsflöde men läggs ovanpå bilder från inspelningsflödet. Det är osannolikt att de två strömmarna är helt synkroniserade. Som ett resultat kommer avgränsningsramen och filmmaterialet inte att radas upp perfekt. Du kan använda den här inställningen för att förskjuta anteckningarna framåt eller bakåt i tiden för att bättre anpassa dem till det inspelade materialet.",
"millisecondsToOffset": "Millisekunder för att förskjuta detektera annoteringar med. Standard: 0 ",
- "tips": "TIPS: Föreställ dig ett händelseklipp med en person som går från vänster till höger. Om tidslinjens avgränsningsram konsekvent är till vänster om personen bör värdet minskas. På samma sätt, om en person går från vänster till höger och avgränsningsramen konsekvent är framför personen bör värdet ökas.",
+ "tips": "Sänk värdet om videouppspelningen sker före rutorna och banpunkterna, och öka värdet om videouppspelningen sker bakom dem. Detta värde kan vara negativt.",
"toast": {
"success": "Annoteringsförskjutningen för {{camera}} har sparats i konfigurationsfilen."
}
diff --git a/web/public/locales/sv/views/faceLibrary.json b/web/public/locales/sv/views/faceLibrary.json
index 76a80ce92d..24c80ebe32 100644
--- a/web/public/locales/sv/views/faceLibrary.json
+++ b/web/public/locales/sv/views/faceLibrary.json
@@ -7,7 +7,7 @@
"faceDesc": "Detaljer om det spårade objektet som genererade detta ansikte",
"unknown": "Okänd",
"subLabelScore": "Underetikettpoäng",
- "scoreInfo": "Underetikettpoängen är den viktade poängen för alla igenkända ansiktskonfidenser, så detta kan skilja sig från poängen som visas på ögonblicksbilden."
+ "scoreInfo": "Poängen är ett viktat genomsnitt av alla ansiktspoäng, viktat efter ansiktets storlek i varje bild."
},
"description": {
"placeholder": "Ange ett namn för denna samling",
@@ -75,7 +75,7 @@
"deletedName_other": "{{count}} ansikten har raderats.",
"renamedFace": "Ansiktet har bytt namn till {{name}}",
"trainedFace": "Ansikte är tränant.",
- "updatedFaceScore": "Ansikts poängen har uppdaterats."
+ "updatedFaceScore": "Ansiktspoängen för {{name}} har uppdaterats till {{score}}."
},
"error": {
"uploadingImageFailed": "Misslyckades med att ladda upp bilden: {{errorMessage}}",
diff --git a/web/public/locales/sv/views/live.json b/web/public/locales/sv/views/live.json
index 750fdd8362..bd2bfbcbc4 100644
--- a/web/public/locales/sv/views/live.json
+++ b/web/public/locales/sv/views/live.json
@@ -12,18 +12,18 @@
"ptz": {
"zoom": {
"in": {
- "label": "Zooma in PTZ kamera"
+ "label": "Zooma in PTZ-kameran"
},
"out": {
- "label": "Zooma ut PTZ kamera"
+ "label": "Zooma ut PTZ-kameran"
}
},
"move": {
"up": {
- "label": "Flytta PTZ kamera uppåt"
+ "label": "Flytta PTZ-kameran uppåt"
},
"right": {
- "label": "Flytta PTZ kamera åt höger"
+ "label": "Flytta PTZ-kameran åt höger"
},
"clickMove": {
"disable": "Inaktivera klick för att flytta",
@@ -42,10 +42,10 @@
"label": "Klicka i bilden för att centrera PTZ kamera"
}
},
- "presets": "PTZ kamera förinställningar",
+ "presets": "PTZ-kamerans förinställningar",
"focus": {
"in": {
- "label": "Fokusera PTZ-kameran in"
+ "label": "Fokusera PTZ-kameran närmare"
},
"out": {
"label": "Fokusera PTZ-kameran ut"
@@ -101,7 +101,7 @@
},
"cameraSettings": {
"audioDetection": "Ljuddetektering",
- "title": "{{kamera}} inställningar",
+ "title": "Inställningar för {{camera}}",
"cameraEnabled": "Kamera Aktiverad",
"objectDetection": "Objektsdetektering",
"recording": "Inspelning",
diff --git a/web/public/locales/sv/views/motionSearch.json b/web/public/locales/sv/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/sv/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/sv/views/replay.json b/web/public/locales/sv/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/sv/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/sv/views/search.json b/web/public/locales/sv/views/search.json
index 2e9f4e0072..c1a01d656c 100644
--- a/web/public/locales/sv/views/search.json
+++ b/web/public/locales/sv/views/search.json
@@ -14,18 +14,18 @@
"desc": {
"step1": "Skriv ett filtreringsnyckelnamn följt av ett kolon (t.ex. \"kameror:\").",
"step2": "Välj ett värde utifrån alternativen eller skriv in ditt egna.",
- "step3": "Använd flera filter genom att addera dom en efter en med ett mellanslag emellan dom.",
+ "step3": "Använd flera filter genom att lägga till dem en efter en med ett mellanslag mellan dem.",
"step4": "Datumfilter (före: och efter:) använd {{DateFormat}} format.",
"step5": "Tidsintervaller använder {{exampleTime}} format.",
"exampleLabel": "Exempel:",
- "step6": "Ta bort filter genom att klicka på 'x\" bredvid dom.",
+ "step6": "Ta bort filter genom att klicka på \"x\" bredvid dem.",
"text": "Filter hjälper dig att begränsa dina sökresultat. Så här använder du dem i inmatningsfältet:"
},
"title": "Hur du använder filter"
},
"header": {
"noFilters": "Filter",
- "activeFilters": "Aktiva Filter",
+ "activeFilters": "Aktiva filter",
"currentFilterType": "Filtervärden"
},
"label": {
diff --git a/web/public/locales/sv/views/settings.json b/web/public/locales/sv/views/settings.json
index 7a256d722e..e09673a38f 100644
--- a/web/public/locales/sv/views/settings.json
+++ b/web/public/locales/sv/views/settings.json
@@ -18,20 +18,20 @@
"title": "UI inställningar",
"liveDashboard": {
"automaticLiveView": {
- "desc": "Automatiskt byte till kamera där aktivitet registreras. Inaktivering av denna inställning gör att en statisk bild visas i Live Panelen som uppdateras en gång per minut.",
- "label": "Automatisk Live Visning"
+ "desc": "Växla automatiskt till kamerans livevy när aktivitet upptäcks. Om det här alternativet inaktiveras uppdateras statiska kamerabilder på livepanelen bara en gång per minut.",
+ "label": "Automatisk livevisning"
},
"playAlertVideos": {
- "desc": "Som standard visas varningar på Live panelen som små loopande klipp. Inaktivera denna inställning för att bara visa en statisk bild av nya varningar på denna enhet/webbläsare.",
+ "desc": "Som standard spelas de senaste varningarna upp som små loopande videor på livepanelen. Inaktivera det här alternativet om du bara vill visa en statisk bild av de senaste varningarna på den här enheten/i den här webbläsaren.",
"label": "Spela upp Varnings videor"
},
- "title": "Live Panel",
+ "title": "Livepanel",
"displayCameraNames": {
"label": "Visa alltid kameranamn",
"desc": "Visa alltid kameranamnen i ett chip i instrumentpanelen för livevisning med flera kameror."
},
"liveFallbackTimeout": {
- "label": "Live spelare reserv timeout",
+ "label": "Reservtimeout för livespelare",
"desc": "När en kameras högkvalitativa liveström inte är tillgänglig, återgå till lågbandbreddsläge efter så här många sekunder. Standard: 3."
}
},
@@ -125,10 +125,10 @@
"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"
+ "title": "Ansiktsigenkänning"
},
"licensePlateRecognition": {
- "title": "Nummerplåt Erkännande",
+ "title": "Igenkänning av registreringsskyltar",
"desc": "Frigate kan känna igen nummerplåt på fordon och automatiskt lägga till de upptäckta tecknen i fältet recognized_license_plate eller ett känt namn som en underetikett till objekt av typen bil. Ett vanligt användningsfall kan vara att läsa nummerplåtor på bilar som kör in på en uppfart eller bilar som passerar på en gata.",
"readTheDocumentation": "Läs dokumentationen"
},
@@ -151,7 +151,8 @@
"triggers": "Utlösare",
"roles": "Roller",
"cameraManagement": "Hantering",
- "cameraReview": "Granska"
+ "cameraReview": "Granska",
+ "profiles": "Profiler"
},
"dialog": {
"unsavedChanges": {
@@ -244,7 +245,7 @@
}
},
"motionMaskLabel": "Rörelsemask {{number}}",
- "objectMaskLabel": "Objektmask {{number}} ({{label}})",
+ "objectMaskLabel": "Objektmask {{number}}",
"form": {
"zoneName": {
"error": {
@@ -407,7 +408,7 @@
},
"motionDetectionTuner": {
"title": "Rörelsedetekteringstuner",
- "unsavedChanges": "Osparade ändringar i Motion Tuner ({{camera}})",
+ "unsavedChanges": "Osparade ändringar i rörelsejusteraren ({{camera}})",
"desc": {
"title": "Frigate använder rörelsedetektering som en första kontroll för att se om det händer något i bilden som är värt att kontrollera med objektdetektering.",
"documentation": "Läs guiden för rörelsejustering"
@@ -430,7 +431,7 @@
},
"debug": {
"title": "Felsök",
- "detectorDesc": "Fregate använder dina detektorer ({{detectors}}) för att upptäcka objekt i din kameras videoström.",
+ "detectorDesc": "Frigate använder dina detektorer ({{detectors}}) för att upptäcka objekt i kamerans videoström.",
"desc": "Felsökningsvyn visar en realtidsvy av spårade objekt och deras statistik. Objektlistan visar en tidsfördröjd sammanfattning av upptäckta objekt.",
"openCameraWebUI": "Öppna {{camera}}s webbgränssnitt",
"debugging": "Felsökning",
@@ -567,7 +568,7 @@
},
"createUser": {
"title": "Skapa ny användare",
- "desc": "Lägg till ett nytt användarkonto och ange en roll för åtkomst till områden i Frigate gränssnittet.",
+ "desc": "Lägg till ett nytt användarkonto och ange en roll för åtkomst till områden i Frigate-gränssnittet.",
"usernameOnlyInclude": "Användarnamnet får endast innehålla bokstäver, siffror, . eller _",
"confirmPassword": "Vänligen bekräfta ditt lösenord"
},
@@ -597,7 +598,7 @@
"admin": "Administratör",
"adminDesc": "Full åtkomst till alla funktioner.",
"viewer": "Åskådare",
- "viewerDesc": "Begränsat till Live-dashboards, Review, Explore, och Exports bara.",
+ "viewerDesc": "Endast åtkomst till Live-paneler, Granskning, Utforska och Exporter.",
"customDesc": "Anpassad roll med specifik kameraåtkomst."
}
}
@@ -684,7 +685,7 @@
"dialog": {
"createRole": {
"title": "Skapa ny roll",
- "desc": "Skapa en ny roll och ange kamera åtkomstbehörigheter."
+ "desc": "Skapa en ny roll och ange behörigheter för kameraåtkomst."
},
"deleteRole": {
"title": "Radera roll",
@@ -714,7 +715,7 @@
},
"management": {
"title": "Hantering av tittarroller",
- "desc": "Hantera anpassade tittarroller och deras kameraåtkomstbehörigheter för den här Frigate instansen."
+ "desc": "Hantera anpassade tittarroller och deras kameraåtkomstbehörigheter för den här Frigate-instansen."
}
},
"frigatePlus": {
@@ -728,8 +729,8 @@
},
"snapshotConfig": {
"title": "Ögonblicksbild konfiguration",
- "desc": "Att skicka till Frigate+ kräver att både snapshots och clean_copy snapshots är aktiverade i din konfiguration.",
- "cleanCopyWarning": "Vissa kameror har aktiverade ögonblicksbilder men har ren kopia inaktiverad. Du måste aktivera clean_copy i din ögonblicksbild konfiguration för att kunna skicka bilder från dessa kameror till Frigate+.",
+ "desc": "För att skicka till Frigate+ måste ögonblicksbilder vara aktiverade i konfigurationen.",
+ "cleanCopyWarning": "Vissa kameror har ögonblicksbilder inaktiverade",
"table": {
"camera": "Kamera",
"snapshots": "Ögonblicksbilder",
@@ -1079,7 +1080,7 @@
"streamsTitle": "Kameraströmmar",
"addStream": "Lägg till ström",
"addAnotherStream": "Lägg till ytterligare en ström",
- "streamUrl": "Stream-URL",
+ "streamUrl": "Ström-URL",
"streamUrlPlaceholder": "rtsp://användarnamn:lösenord@värd:portnummer/plats",
"selectStream": "Välj en ström",
"searchCandidates": "Sök kandidater...",
@@ -1225,7 +1226,7 @@
"noDefinedZones": "Inga zoner är definierade för den här kameran.",
"objectAlertsTips": "Alla {{alertsLabels}}-objekt på {{cameraName}} kommer att visas som Varningar.",
"zoneObjectAlertsTips": "Alla {{alertsLabels}} objekt som upptäcks i {{zone}} på {{cameraName}} kommer att visas som Varningar.",
- "objectDetectionsTips": "Alla {{detectionsLabels}}-objekt som inte kategoriseras på {{cameraName}} kommer att visas som Detektioner oavsett vilken zon de befinner sig i.",
+ "objectDetectionsTips": "Alla {{detectionsLabels}}-objekt som inte kategoriseras på {{cameraName}} visas som detekteringar oavsett vilken zon de befinner sig i.",
"zoneObjectDetectionsTips": {
"text": "Alla {{detectionsLabels}}-objekt som inte kategoriseras i {{zone}} på {{cameraName}} kommer att visas som Detektioner.",
"notSelectDetections": "Alla {{detectionsLabels}} objekt som upptäckts i {{zone}} på {{cameraName}} och som inte kategoriserats som Varningar kommer att visas som Detekteringar oavsett vilken zon de befinner sig i.",
diff --git a/web/public/locales/sv/views/system.json b/web/public/locales/sv/views/system.json
index 0d6eac76ab..8a5320cfd3 100644
--- a/web/public/locales/sv/views/system.json
+++ b/web/public/locales/sv/views/system.json
@@ -126,7 +126,7 @@
"overview": "Översikt",
"info": {
"aspectRatio": "bildförhållande",
- "cameraProbeInfo": "{{camera}} Kamerasondinformation",
+ "cameraProbeInfo": "Kamerainformation för {{camera}}",
"streamDataFromFFPROBE": "Strömdata erhålls med ffprobe.",
"codec": "Codec:",
"resolution": "Upplösning:",
@@ -173,7 +173,7 @@
"detectHighCpuUsage": "{{camera}} har hög CPU-användning vid detektering ({{detectAvg}}%)",
"healthy": "Systemet är hälsosamt",
"reindexingEmbeddings": "Omindexering av inbäddningar ({{processed}}% klar)",
- "cameraIsOffline": "{{camera}} är urkopplad",
+ "cameraIsOffline": "{{camera}} är frånkopplad",
"detectIsSlow": "{{detect}} är långsam ({{speed}} ms)",
"detectIsVerySlow": "{{detect}} är väldigt långsam ({{speed}} ms)",
"shmTooLow": "/dev/shm allokeringen ({{total}} MB) bör ökas till minst {{min}} MB."
@@ -187,11 +187,11 @@
"face_recognition": "Ansiktsigenkänning",
"plate_recognition": "Nummerplåt igenkänning",
"image_embedding_speed": "Bildinbäddningshastighet",
- "face_embedding_speed": "Ansikts inbäddnings hastighet",
+ "face_embedding_speed": "Hastighet för ansiktsinbäddning",
"face_recognition_speed": "Ansiktsigenkänningshastighet",
"plate_recognition_speed": "Hastighet för igenkänning av nummerplåtar",
"text_embedding_speed": "Textinbäddningshastighet",
- "yolov9_plate_detection_speed": "YOLOv9 nummerplåt detekterings hastighet",
+ "yolov9_plate_detection_speed": "Detekteringshastighet för YOLOv9-registreringsskyltar",
"yolov9_plate_detection": "YOLOv9 nummerplåt detektering",
"review_description": "Recensionsbeskrivning",
"review_description_speed": "Recensionsbeskrivning Hastighet",
diff --git a/web/public/locales/te/audio.json b/web/public/locales/te/audio.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/audio.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/common.json b/web/public/locales/te/common.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/common.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/components/auth.json b/web/public/locales/te/components/auth.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/components/auth.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/components/camera.json b/web/public/locales/te/components/camera.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/components/camera.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/components/dialog.json b/web/public/locales/te/components/dialog.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/components/dialog.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/components/filter.json b/web/public/locales/te/components/filter.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/components/filter.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/components/icons.json b/web/public/locales/te/components/icons.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/components/icons.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/components/input.json b/web/public/locales/te/components/input.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/components/input.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/components/player.json b/web/public/locales/te/components/player.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/components/player.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/config/cameras.json b/web/public/locales/te/config/cameras.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/config/cameras.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/config/global.json b/web/public/locales/te/config/global.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/config/global.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/config/groups.json b/web/public/locales/te/config/groups.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/config/groups.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/config/validation.json b/web/public/locales/te/config/validation.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/config/validation.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/objects.json b/web/public/locales/te/objects.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/objects.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/views/chat.json b/web/public/locales/te/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/views/classificationModel.json b/web/public/locales/te/views/classificationModel.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/views/classificationModel.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/views/configEditor.json b/web/public/locales/te/views/configEditor.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/views/configEditor.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/views/events.json b/web/public/locales/te/views/events.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/views/events.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/views/explore.json b/web/public/locales/te/views/explore.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/views/explore.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/views/exports.json b/web/public/locales/te/views/exports.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/views/exports.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/views/faceLibrary.json b/web/public/locales/te/views/faceLibrary.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/views/faceLibrary.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/views/live.json b/web/public/locales/te/views/live.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/views/live.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/views/motionSearch.json b/web/public/locales/te/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/views/recording.json b/web/public/locales/te/views/recording.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/views/recording.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/views/replay.json b/web/public/locales/te/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/views/search.json b/web/public/locales/te/views/search.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/views/search.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/views/settings.json b/web/public/locales/te/views/settings.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/views/settings.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/te/views/system.json b/web/public/locales/te/views/system.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/te/views/system.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/th/common.json b/web/public/locales/th/common.json
index b920787973..75cbef6a07 100644
--- a/web/public/locales/th/common.json
+++ b/web/public/locales/th/common.json
@@ -66,7 +66,8 @@
"12hour": "MM-dd-yy-h-mm-ss-a",
"24hour": "MM-dd-yy-HH-mm-ss"
},
- "formattedTimestampMonthDay": "MMM d"
+ "formattedTimestampMonthDay": "MMM d",
+ "never": "ไม่เคย"
},
"label": {
"back": "ย้อนกลับ"
diff --git a/web/public/locales/th/config/cameras.json b/web/public/locales/th/config/cameras.json
index 0967ef424b..4a4191953a 100644
--- a/web/public/locales/th/config/cameras.json
+++ b/web/public/locales/th/config/cameras.json
@@ -1 +1,16 @@
-{}
+{
+ "label": "ตั้งค่ากล้อง",
+ "name": {
+ "label": "ชื่อกล้อง"
+ },
+ "friendly_name": {
+ "label": "ชื่อแบบจำง่าย"
+ },
+ "enabled": {
+ "label": "ถูกเปิดอยู่",
+ "description": "ถูกเปิดอยู่"
+ },
+ "audio": {
+ "label": "การตรวจจับเสียง"
+ }
+}
diff --git a/web/public/locales/th/config/global.json b/web/public/locales/th/config/global.json
index 0967ef424b..86743dce82 100644
--- a/web/public/locales/th/config/global.json
+++ b/web/public/locales/th/config/global.json
@@ -1 +1,16 @@
-{}
+{
+ "version": {
+ "label": "การตั้งค่าปัจจุบัน"
+ },
+ "environment_vars": {
+ "label": "สภาพแวดล้อมที่หลากหลาย"
+ },
+ "audio": {
+ "label": "การตรวจจับเสียง"
+ },
+ "auth": {
+ "enabled": {
+ "label": "เปิดใช้การยืนยันตัวตน"
+ }
+ }
+}
diff --git a/web/public/locales/th/config/groups.json b/web/public/locales/th/config/groups.json
index 0967ef424b..43ae3e5bc7 100644
--- a/web/public/locales/th/config/groups.json
+++ b/web/public/locales/th/config/groups.json
@@ -1 +1,18 @@
-{}
+{
+ "audio": {
+ "cameras": {
+ "detection": "การตรวจจับ",
+ "sensitivity": "ความอ่อนไหว"
+ }
+ },
+ "snapshots": {
+ "cameras": {
+ "display": "แสดงผล"
+ }
+ },
+ "detect": {
+ "cameras": {
+ "resolution": "ความละเอียด"
+ }
+ }
+}
diff --git a/web/public/locales/th/config/validation.json b/web/public/locales/th/config/validation.json
index 0967ef424b..1e9b0d54d7 100644
--- a/web/public/locales/th/config/validation.json
+++ b/web/public/locales/th/config/validation.json
@@ -1 +1,9 @@
-{}
+{
+ "maximum": "มากที่สุดไม่เกิน {{limit}}",
+ "exclusiveMinimum": "ต้องเกินกว่า {{limit}}",
+ "exclusiveMaximum": "ต้องน้อยกว่า {{limit}}",
+ "minLength": "จำนวนอย่างน้อย {{limit}} อักขระ",
+ "maxLength": "ต้องไม่เกิน {{limit}} อักขระ",
+ "maxItems": "ต้องไม่เกิน {{limit}}",
+ "minimum": "ขั้นต่ำ {{limit}}"
+}
diff --git a/web/public/locales/th/views/chat.json b/web/public/locales/th/views/chat.json
new file mode 100644
index 0000000000..4390d3961c
--- /dev/null
+++ b/web/public/locales/th/views/chat.json
@@ -0,0 +1,10 @@
+{
+ "documentTitle": "สนทนา - Frigate",
+ "subtitle": "AI ผู้ช่วยบริหารจัดการข้อมูลเชิงลึกสำหรับกล้องวงจรปิดของคุณ",
+ "placeholder": "เชิญถาม…",
+ "error": "เกิดข้อขัดข้อง โปรดลองอีกครั้ง",
+ "processing": "กำลังประมวลผล…",
+ "showTools": "แสดงเครื่องมือ ({{count}})",
+ "response": "ตอบกลับ",
+ "attachment_chip_remove": "เอาสิ่งที่แนบออก"
+}
diff --git a/web/public/locales/th/views/classificationModel.json b/web/public/locales/th/views/classificationModel.json
index 5d1307ccf7..81389c3328 100644
--- a/web/public/locales/th/views/classificationModel.json
+++ b/web/public/locales/th/views/classificationModel.json
@@ -2,9 +2,13 @@
"documentTitle": "โมเดลการจำแนกประเภท- Frigate",
"details": {
"scoreInfo": "คะแนน (Score) คือค่าเฉลี่ยของความมั่นใจในการจำแนกประเภท (Classification Confidence) จากการตรวจจับวัตถุชิ้นนี้ในทุกๆ ครั้ง",
- "none": "ไม่มี"
+ "none": "ไม่มี",
+ "unknown": "ไม่ทราบ"
},
"description": {
"invalidName": "ชื่อไม่ถูกต้อง ชื่อสามารถประกอบได้ด้วยตัวอักษร, ตัวเลข, ช่องว่าง, เครื่องหมาย ( ' , _ , - ) เท่านั้น"
+ },
+ "button": {
+ "deleteImages": "ลบภาพ"
}
}
diff --git a/web/public/locales/th/views/events.json b/web/public/locales/th/views/events.json
index f303ea6b3c..9279e9ae33 100644
--- a/web/public/locales/th/views/events.json
+++ b/web/public/locales/th/views/events.json
@@ -34,5 +34,6 @@
"detections": "การตรวจจับ",
"selected_one": "เลือก {{count}} แล้ว",
"timeline.aria": "เลือกไทม์ไลน์",
- "documentTitle": "รีวิว - Frigate"
+ "documentTitle": "รีวิว - Frigate",
+ "zoomIn": "ซูมเข้า"
}
diff --git a/web/public/locales/th/views/explore.json b/web/public/locales/th/views/explore.json
index 030d228994..8c17c33b03 100644
--- a/web/public/locales/th/views/explore.json
+++ b/web/public/locales/th/views/explore.json
@@ -8,7 +8,10 @@
"context": "สํารวจสามารถใช้หลังจากติดตามวัตถุเสร็จ.",
"startingUp": "เริ่มต้น…",
"estimatedTime": "ระยะเวลาโดยประมาณ:",
- "finishingShortly": "เสร็จเร็วๆนี้"
+ "finishingShortly": "เสร็จเร็วๆนี้",
+ "step": {
+ "thumbnailsEmbedded": "รูปภาพย่อที่ฝังไว้: "
+ }
},
"downloadingModels": {
"tips": {
diff --git a/web/public/locales/th/views/exports.json b/web/public/locales/th/views/exports.json
index 698c6f82b8..1c58da8353 100644
--- a/web/public/locales/th/views/exports.json
+++ b/web/public/locales/th/views/exports.json
@@ -13,5 +13,11 @@
"error": {
"renameExportFailed": "ผิดพลาดในการแก้ไขชื่อการส่งออก: {{errorMessage}}"
}
+ },
+ "headings": {
+ "cases": "กรณี"
+ },
+ "tooltip": {
+ "editName": "เปลี่ยนชื่อ"
}
}
diff --git a/web/public/locales/th/views/faceLibrary.json b/web/public/locales/th/views/faceLibrary.json
index d663a7bcf6..4cbefcc7ab 100644
--- a/web/public/locales/th/views/faceLibrary.json
+++ b/web/public/locales/th/views/faceLibrary.json
@@ -2,7 +2,7 @@
"details": {
"person": "คน",
"subLabelScore": "คะแนน Sub Label",
- "unknown": "ไม่รู้",
+ "unknown": "ไม่ทราบ",
"timestamp": "เวลา"
},
"steps": {
@@ -46,7 +46,8 @@
"description": {
"addFace": "เพิ่มคอลเลกชันใหม่ไปยังคลังใบหน้า โดยการอัปโหลดรูปภาพแรก",
"placeholder": "ใส่ชื่อสําหรับคอลเลกชันนี้",
- "invalidName": "ชื่อไม่ถูกต้อง ชื่อสามารถประกอบได้ด้วยตัวอักษร, ตัวเลข, ช่องว่าง, เครื่องหมาย ( ' , _ , - ) เท่านั้น"
+ "invalidName": "ชื่อไม่ถูกต้อง ชื่อสามารถประกอบได้ด้วยตัวอักษร, ตัวเลข, ช่องว่าง, เครื่องหมาย ( ' , _ , - ) เท่านั้น",
+ "nameCannotContainHash": "ชื่อ ห้ามมีเครื่องหมาย #"
},
"toast": {
"success": {
@@ -54,5 +55,6 @@
"deletedName_other": "{{count}} หน้าถูกลบไปเรียบร้อยแล้ว."
}
},
- "readTheDocs": "อ่านเอกสาร"
+ "readTheDocs": "อ่านเอกสาร",
+ "documentTitle": "คลังข้อมูลใบหน้า - Frigate"
}
diff --git a/web/public/locales/th/views/live.json b/web/public/locales/th/views/live.json
index ccf620b84c..3bdd408f89 100644
--- a/web/public/locales/th/views/live.json
+++ b/web/public/locales/th/views/live.json
@@ -12,7 +12,9 @@
"tips.documentation": "อ่านเอกสาร "
}
},
- "documentTitle": "สด - Frigate",
+ "documentTitle": {
+ "default": "ถ่ายทอดสด - Frigate"
+ },
"lowBandwidthMode": "โหมดแบนด์วิดท์ต่ำ",
"twoWayTalk": {
"enable": "เปิดใช้งานการสนทนาสองทาง",
@@ -45,5 +47,12 @@
},
"recording": {
"disable": "ปิดการบันทึก"
+ },
+ "ptz": {
+ "move": {
+ "clickMove": {
+ "label": "คลิกที่ภาพ เพื่อเลือกตำแหน่งที่จะตั้งค่าให้เป็นศูนย์กลางภาพ"
+ }
+ }
}
}
diff --git a/web/public/locales/th/views/motionSearch.json b/web/public/locales/th/views/motionSearch.json
new file mode 100644
index 0000000000..852fb09cb9
--- /dev/null
+++ b/web/public/locales/th/views/motionSearch.json
@@ -0,0 +1,11 @@
+{
+ "documentTitle": "ค้นหาการเคลื่อนไหว - Frigate",
+ "title": "ค้นหาการเคลื่อนไหว",
+ "description": "วาดเส้นกรอบกำหนดขอบเขตที่ต้องการ และระบุช่วงเวลาค้นหาการเคลื่อนไหวในบริเวณนั้น",
+ "startSearch": "เริ่มการค้นหา",
+ "searchStarted": "การค้นหาเริ่มแล้ว",
+ "searchCancelled": "เลิกค้นหาแล้ว",
+ "cancelSearch": "ยกเลิก",
+ "noChangesFound": "ไม่มีการเปลี่ยนแปลงในภาพบริเวณที่เลือก",
+ "jumpToTime": "ข้ามมาที่เวลานี้"
+}
diff --git a/web/public/locales/th/views/replay.json b/web/public/locales/th/views/replay.json
new file mode 100644
index 0000000000..1c8cc9b831
--- /dev/null
+++ b/web/public/locales/th/views/replay.json
@@ -0,0 +1,13 @@
+{
+ "websocket_messages": "ข้อความ",
+ "dialog": {
+ "camera": "ภาพจากกล้อง",
+ "timeRange": "ช่วงเวลา",
+ "preset": {
+ "1m": "1 นาทีสุดท้าย"
+ },
+ "startButton": "เริ่มเล่นภาพย้อนหลัง",
+ "selectFromTimeline": "เลือก",
+ "startLabel": "เริ่ม"
+ }
+}
diff --git a/web/public/locales/th/views/settings.json b/web/public/locales/th/views/settings.json
index b848a4e279..ebd2805da4 100644
--- a/web/public/locales/th/views/settings.json
+++ b/web/public/locales/th/views/settings.json
@@ -109,7 +109,8 @@
"cameraManagement": "จัดการกล้อง - Frigate",
"enrichments": "การตั้งค่าของเพิ่มเติม - Frigate",
"motionTuner": "ปรับแต่งการเคลื่อนไหว - Frigate",
- "object": "ดีบั๊ก - Frigate"
+ "object": "ดีบั๊ก - Frigate",
+ "cameraReview": "แสดงการตั้งค่าของกล้อง - Frigate"
},
"menu": {
"notifications": "การแจ้งเตือน",
diff --git a/web/public/locales/th/views/system.json b/web/public/locales/th/views/system.json
index 4ab0f7361f..90e29a25ef 100644
--- a/web/public/locales/th/views/system.json
+++ b/web/public/locales/th/views/system.json
@@ -64,7 +64,17 @@
"logs": {
"frigate": "Frigate Logs - Frigate",
"go2rtc": "Logs ของ Go2RTC - Frigate",
- "nginx": "Logs ของ Nginx - Frigate"
+ "nginx": "Logs ของ Nginx - Frigate",
+ "websocket": "ประวัติข้อความ - Frigate"
+ }
+ },
+ "logs": {
+ "websocket": {
+ "pause": "พัก",
+ "clear": "ลบล้าง",
+ "filter": {
+ "all": "ทุกหัวข้อ"
+ }
}
}
}
diff --git a/web/public/locales/tr/common.json b/web/public/locales/tr/common.json
index 2a97d8d0f4..50ea2c65f9 100644
--- a/web/public/locales/tr/common.json
+++ b/web/public/locales/tr/common.json
@@ -36,7 +36,7 @@
"h": "{{time}} s",
"yr": "{{time}} yıl",
"mo": "{{time}} ay",
- "untilForTime": "{{time}}'a kadar",
+ "untilForTime": "{{time}} saatine kadar",
"pm": "ÖS",
"am": "ÖÖ",
"d": "{{time}} gün",
@@ -123,7 +123,19 @@
"twoWayTalk": "Çift Yönlü Ses",
"close": "Kapat",
"delete": "Sil",
- "continue": "Devam Et"
+ "continue": "Devam Et",
+ "add": "Ekle",
+ "applying": "Uygulanıyor…",
+ "undo": "Geri al",
+ "copiedToClipboard": "Panoya kopyaladı",
+ "modified": "Değiştirilmiş",
+ "overridden": "Üstüne yazılmış",
+ "resetToGlobal": "Genele sıfırla",
+ "resetToDefault": "Varsayılana sıfırla",
+ "saveAll": "Hepsini Kaydet",
+ "savingAll": "Hepsi Kaydediliyor…",
+ "undoAll": "Hepsini Geri Al",
+ "retry": "Yeniden dene"
},
"menu": {
"systemLogs": "Sistem günlükleri",
@@ -179,7 +191,10 @@
"bg": "Български (Bulgarca)",
"gl": "Galego (Galiçyaca)",
"id": "Bahasa Indonesia (Endonezce)",
- "ur": "اردو (Urduca)"
+ "ur": "اردو (Urduca)",
+ "hr": "Hrvatski (Hırvatça)",
+ "zhHant": "繁體中文 (Geleneksel Çince)",
+ "bs": "Bosanski (Boşnakça)"
},
"withSystem": "Sistem",
"theme": {
@@ -225,7 +240,11 @@
"faceLibrary": "Yüz Veritabanı",
"systemMetrics": "Sistem metrikleri",
"uiPlayground": "UI Deneme Alanı",
- "classification": "Sınıflandırma"
+ "classification": "Sınıflandırma",
+ "profiles": "Profiller",
+ "actions": "Eylemler",
+ "chat": "Sohbet",
+ "features": "Özellikler"
},
"label": {
"back": "Geri",
@@ -283,7 +302,8 @@
"error": {
"noMessage": "Yapılandırma değişiklikleri kaydedilemedi",
"title": "Yapılandırma değişiklikleri kaydedilemedi: {{errorMessage}}"
- }
+ },
+ "success": "Yapılandırma değişiklikleri kaydedildi."
}
},
"selectItem": "{{item}} seçin",
@@ -305,5 +325,10 @@
},
"information": {
"pixels": "{{area}}px"
+ },
+ "no_items": "Öge bulunmuyor",
+ "validation_errors": "Doğrulama Hataları",
+ "credentialField": {
+ "savedPlaceholder": "Kaydedildi — mevcut haliyle kalması için boş bırakın"
}
}
diff --git a/web/public/locales/tr/components/camera.json b/web/public/locales/tr/components/camera.json
index 7885c2653c..2076c0b9f3 100644
--- a/web/public/locales/tr/components/camera.json
+++ b/web/public/locales/tr/components/camera.json
@@ -68,7 +68,10 @@
"cameras": {
"desc": "Bu gruba dahil olacak kameraları seçin.",
"label": "Kameralar"
- }
+ },
+ "showAll": "Bütün Kamera Gruplarını Göster",
+ "showLess": "Daha Az",
+ "editGroups": "Kamera Gruplarını Düzenle"
},
"debug": {
"options": {
@@ -82,6 +85,7 @@
"zones": "Alanlar",
"mask": "Maske",
"motion": "Hareket",
- "regions": "Tespit Bölgeleri"
+ "regions": "Tespit Bölgeleri",
+ "paths": "Yollar"
}
}
diff --git a/web/public/locales/tr/components/dialog.json b/web/public/locales/tr/components/dialog.json
index 35fe451704..323fe5d331 100644
--- a/web/public/locales/tr/components/dialog.json
+++ b/web/public/locales/tr/components/dialog.json
@@ -6,7 +6,8 @@
"content": "Bu sayfa {{countdown}} saniye sonra yeniden yüklenecektir.",
"title": "Frigate Yeniden Başlatılıyor",
"button": "Şimdi Yeniden Yükle"
- }
+ },
+ "description": "Bu Frigate'i kısa süreliğine yeniden başlayana kadar durduracak."
},
"explore": {
"plus": {
@@ -65,14 +66,78 @@
"endTimeMustAfterStartTime": "Bitiş zamanı başlangıç zamanından sonra olmalıdır",
"noVaildTimeSelected": "Geçerli bir zaman aralığı seçilmedi"
},
- "view": "Görüntüle"
+ "view": "Görüntüle",
+ "queued": "Dışa aktarımlar kuyruğa alındı. İlerlemeyi dışa aktarım sayfasından görebilirsiniz.",
+ "batchSuccess_one": "1 Adet dışa aktarım başlatıldı. Durum açılıyor.",
+ "batchSuccess_other": "{{count}} Adet dışa aktarım başlatıldı. Durum açılıyor.",
+ "batchPartial": "{{total}} üzerinden {{successful}} adet dışa aktarım başlatıldı. Başarısız: {{failedCameras}}",
+ "batchFailed": "{{total}} dışa aktarma işlemi başlatılamadı. Başarısız kameralar: {{failedCameras}}",
+ "batchQueuedSuccess_one": "1 dışa aktarım sıraya alındı.",
+ "batchQueuedSuccess_other": "{{count}} dışa aktarım sıraya alındı.",
+ "batchQueuedPartial": "{{total}} dışa aktarımdan {{successful}} tanesi sıraya alındı. Başarısız olan kameralar: {{failedCameras}}",
+ "batchQueueFailed": "{{total}} adet dışa aktarım sıraya alınamadı. Başarısız kameralar: {{failedCameras}}"
},
"fromTimeline": {
"saveExport": "Dışa Aktarımı Kaydet",
- "previewExport": "Dışa Aktarımı Önizle"
+ "previewExport": "Dışa Aktarımı Önizle",
+ "useThisRange": "Use This Range",
+ "queueingExport": "Dışa aktarma sıraya alınıyor..."
},
"name": {
"placeholder": "Dışa Aktarımı Adlandırın"
+ },
+ "case": {
+ "newCaseOption": "Yeni durum oluştur",
+ "newCaseNamePlaceholder": "Yeni durum ismi",
+ "newCaseDescriptionPlaceholder": "Durum açıklaması",
+ "label": "Durum",
+ "nonAdminHelp": "Bu dışa aktarımlar için yeni durumlar oluşturulacak.",
+ "placeholder": "Durum seç"
+ },
+ "queueing": "Dışa aktarımlar kuyruğa alınıyor...",
+ "tabs": {
+ "export": "Tek Kamera",
+ "multiCamera": "Çoklu Kamera"
+ },
+ "multiCamera": {
+ "timeRange": "Zaman Aralığı",
+ "selectFromTimeline": "Zaman çizelgesinden seç",
+ "cameraSelection": "Kameralar",
+ "cameraSelectionHelp": "Bu zaman aralığında ki obje takibi olan kameralar önceden seçildi",
+ "checkingActivity": "Kamera faliyeti kontrol ediliyor...",
+ "noCameras": "Kamera mevcut değil",
+ "detectionCount_one": "1 adet takip edilen obje",
+ "detectionCount_other": "{{count}} adet takip edilen obje",
+ "nameLabel": "Dışa aktarım ismi",
+ "namePlaceholder": "Dışa aktarım için opsiyonel temel isim",
+ "queueingButton": "Dışa aktarımlar kuyruğa alınıyor...",
+ "exportButton_one": "1 Adet Kamera Dışarı Aktarıldı",
+ "exportButton_other": "{{count}} Adet Kamera Dışarı Aktarıldı",
+ "searchOrSelectGroup": "Kamera grubu arayın veya seçin...",
+ "selectAll": "Tüm kameraları seç",
+ "clearSelection": "Seçimi kaldır",
+ "selectWithActivity": "Nesne takibi yapan kameralar",
+ "selectGroup": "Grup seç",
+ "noMatchingCameras": "Arama kriterlerinize uyan kamera bulunamadı",
+ "selectedCount": "{{selected}} / {{total}} seçilen"
+ },
+ "multi": {
+ "title_one": "1 Adet Değerlendirme Dışarı Aktarıldı",
+ "title_other": "{{count}} Adet Değerlendirme Dışarı Aktarıldı",
+ "description": "Seçilmiş değerlendirmeleri tek tek dışa aktarın. Bütün dışa aktarımlar tek bir durum altında toplanacak.",
+ "descriptionNoCase": "Seçilmiş değerlendirmeleri tek tek dışa aktar.",
+ "caseNamePlaceholder": "Değerlendirme dışa aktarımı - {{date}}",
+ "exportButton_one": "1 Adet Değerlendirme'yi dışa akatar",
+ "exportButton_other": "{{count}} Adet Değerlendirme'yi dışa akatar",
+ "exportingButton": "Dışa aktarılıyor...",
+ "toast": {
+ "started_one": "1 Adet dışa aktarın başladı. Durum açılıyor.",
+ "started_other": "{{count}} Adet dışa aktarın başladı. Durum açılıyor.",
+ "startedNoCase_one": "1 Adet dışa aktarım başladı.",
+ "startedNoCase_other": "{{count}} Adet dışa aktarım başladı.",
+ "partial": "{{total}} üzerinden {{successful}} dışa aktarıldı. Başarısız: {{failedItems}}",
+ "failed": "{{total}} Adet dışa aktarım başarısız oldu. Başarısız: {{failedItems}}"
+ }
}
},
"streaming": {
@@ -120,6 +185,14 @@
"markAsReviewed": "İncelendi olarak işaretle",
"deleteNow": "Şimdi Sil",
"markAsUnreviewed": "Gözden geçirilmedi olarak işaretle"
+ },
+ "shareTimestamp": {
+ "label": "Zaman Damgasını Paylaş",
+ "title": "Zaman Damgasını Paylaş",
+ "description": "Mevcut oynatıcı konumuna ait zaman damgalı bir URL paylaşın veya özel bir zaman damgası seçin. Bunun herkese açık bir paylaşım URL'si olmadığını ve yalnızca Frigate ile bu kameraya erişimi olan kullanıcılar tarafından görüntülenebileceğini unutmayın.",
+ "custom": "Özel Zaman Damgası",
+ "button": "Zaman Damgalı URL'yi Paylaş",
+ "shareTitle": "Frigate İnceleme Zaman Damgası: {{camera}}"
}
},
"imagePicker": {
diff --git a/web/public/locales/tr/components/player.json b/web/public/locales/tr/components/player.json
index 6a7950369e..46ff41d464 100644
--- a/web/public/locales/tr/components/player.json
+++ b/web/public/locales/tr/components/player.json
@@ -46,6 +46,8 @@
"livePlayerRequiredIOSVersion": "Bu canlı yayın türü için iOS 17.1 veya daha yeni sürüm gereklidir.",
"submitFrigatePlus": {
"title": "Bu kare Frigate+'ya gönderilsin mi?",
- "submit": "Gönder"
- }
+ "submit": "Gönder",
+ "previewError": "Önizleme şuan aktif edilemiyor. Kayıt şuan mevcut olmayabilir."
+ },
+ "cameraOff": "Kamera kapalı"
}
diff --git a/web/public/locales/tr/config/cameras.json b/web/public/locales/tr/config/cameras.json
index 7bc693e879..13595d4955 100644
--- a/web/public/locales/tr/config/cameras.json
+++ b/web/public/locales/tr/config/cameras.json
@@ -1,5 +1,66 @@
{
"name": {
- "label": "Kamera ismi"
+ "label": "Kamera adı",
+ "description": "Kamera adı gereklidir"
+ },
+ "friendly_name": {
+ "label": "Kolay ad",
+ "description": "Frigate arayüzünde kullanılacak kolay ad"
+ },
+ "enabled": {
+ "label": "Etkin",
+ "description": "Etkin"
+ },
+ "audio": {
+ "label": "Ses olayları",
+ "description": "Bu kamera için ses tabanlı olay algılama ayarları.",
+ "enabled": {
+ "label": "Ses algılamayı etkinleştir",
+ "description": "Bu kamera için ses tabanlı olay algılamayı etkinleştirin veya devre dışı bırakın."
+ },
+ "max_not_heard": {
+ "label": "Zaman aşımını sonlandır",
+ "description": "Ses tabanlı olayın sona ermesi için yapılandırılmış ses türünün algılanmadığı saniye süresi."
+ },
+ "min_volume": {
+ "label": "Minimum ses seviyesi",
+ "description": "Ses algılamayı çalıştırmak için gereken minimum RMS ses seviyesi eşiği; daha düşük değerler hassasiyeti artırır (örneğin: 200 yüksek, 500 orta, 1000 düşük)."
+ },
+ "listen": {
+ "label": "Dinlenecek ses türleri",
+ "description": "Algılanacak ses olayı türlerinin listesi (örneğin: bark (havlama), fire_alarm (yangın alarmı), speech (konuşma), yell (bağırma))."
+ },
+ "filters": {
+ "label": "Ses filtreleri",
+ "description": "Yanlış alarmları azaltmak için kullanılan güven eşikleri gibi ses türüne özel filtre ayarları.",
+ "threshold": {
+ "label": "Minimum ses güven eşiği",
+ "description": "Ses olayının sayılması için gereken minimum güven eşiği."
+ }
+ },
+ "enabled_in_config": {
+ "label": "Orijinal ses durumu",
+ "description": "Ses algılamanın statik yapılandırma dosyasında başlangıçta etkinleştirilip etkinleştirilmediğini belirtir."
+ }
+ },
+ "label": "Kamera Konfigürasyonu",
+ "detect": {
+ "stationary": {
+ "threshold": {
+ "label": "Sabitlik eşiği",
+ "description": "Bir nesneyi sabit olarak işaretlemek için gereken, konum değişikliği olmayan kare sayısı."
+ },
+ "max_frames": {
+ "label": "Maksimum kare sayısı",
+ "description": "Sabit nesnelerin, izlenmesi bırakılmadan önce ne kadar süre takip edileceğini sınırlar.",
+ "default": {
+ "label": "Varsayılan maksimum kare sayısı",
+ "description": "Sabit bir nesneyi durdurmadan önce takip etmek için varsayılan maksimum kare sayısı."
+ },
+ "objects": {
+ "label": "Nesne maksimum kare sayısı"
+ }
+ }
+ }
}
}
diff --git a/web/public/locales/tr/config/global.json b/web/public/locales/tr/config/global.json
index 4b4308cb3c..bcc1260a2d 100644
--- a/web/public/locales/tr/config/global.json
+++ b/web/public/locales/tr/config/global.json
@@ -1,8 +1,65 @@
{
"safe_mode": {
- "label": "Güvenli mod"
+ "label": "Güvenli mod",
+ "description": "Etkinleştirildiğinde, Firagate'i sorun girderme için kısıtlı özelliklere sahip güvenli modda başlat."
},
"environment_vars": {
"label": "Ortam değişkenleri"
+ },
+ "audio": {
+ "label": "Ses olayları",
+ "enabled": {
+ "label": "Ses algılamayı etkinleştir"
+ },
+ "max_not_heard": {
+ "label": "Zaman aşımını sonlandır",
+ "description": "Ses tabanlı olayın sona ermesi için yapılandırılmış ses türünün algılanmadığı saniye süresi."
+ },
+ "min_volume": {
+ "label": "Minimum ses seviyesi",
+ "description": "Ses algılamayı çalıştırmak için gereken minimum RMS ses seviyesi eşiği; daha düşük değerler hassasiyeti artırır (örneğin: 200 yüksek, 500 orta, 1000 düşük)."
+ },
+ "listen": {
+ "label": "Dinlenecek ses türleri",
+ "description": "Algılanacak ses olayı türlerinin listesi (örneğin: bark (havlama), fire_alarm (yangın alarmı), speech (konuşma), yell (bağırma))."
+ },
+ "filters": {
+ "label": "Ses filtreleri",
+ "description": "Yanlış alarmları azaltmak için kullanılan güven eşikleri gibi ses türüne özel filtre ayarları.",
+ "threshold": {
+ "label": "Minimum ses güven eşiği",
+ "description": "Ses olayının sayılması için gereken minimum güven eşiği."
+ }
+ },
+ "enabled_in_config": {
+ "label": "Orijinal ses durumu",
+ "description": "Ses algılamanın statik yapılandırma dosyasında başlangıçta etkinleştirilip etkinleştirilmediğini belirtir."
+ }
+ },
+ "version": {
+ "label": "Mevcut konfigürasyon versiyonu",
+ "description": "Taşıma veya biçimlendirme değişikliklerini tespit etmeye yardımcı olmak için etkin konfigürasyonun sayısal veya metin tabanlı sürümü."
+ },
+ "logger": {
+ "label": "Kayıt"
+ },
+ "detect": {
+ "stationary": {
+ "threshold": {
+ "label": "Sabitlik eşiği",
+ "description": "Bir nesneyi sabit olarak işaretlemek için gereken, konum değişikliği olmayan kare sayısı."
+ },
+ "max_frames": {
+ "label": "Maksimum kare sayısı",
+ "description": "Sabit nesnelerin, izlenmesi bırakılmadan önce ne kadar süre takip edileceğini sınırlar.",
+ "default": {
+ "label": "Varsayılan maksimum kare sayısı",
+ "description": "Sabit bir nesneyi durdurmadan önce takip etmek için varsayılan maksimum kare sayısı."
+ },
+ "objects": {
+ "label": "Nesne maksimum kare sayısı"
+ }
+ }
+ }
}
}
diff --git a/web/public/locales/tr/config/groups.json b/web/public/locales/tr/config/groups.json
index 0967ef424b..c6e643d6b0 100644
--- a/web/public/locales/tr/config/groups.json
+++ b/web/public/locales/tr/config/groups.json
@@ -1 +1,71 @@
-{}
+{
+ "audio": {
+ "global": {
+ "detection": "Genel Tespit",
+ "sensitivity": "Genel Hassasiyet"
+ },
+ "cameras": {
+ "detection": "Tespit",
+ "sensitivity": "Hassasiyet"
+ }
+ },
+ "timestamp_style": {
+ "global": {
+ "appearance": "Genel Görünüm"
+ },
+ "cameras": {
+ "appearance": "Görünüm"
+ }
+ },
+ "detect": {
+ "cameras": {
+ "resolution": "Çözünürlük",
+ "tracking": "Takip"
+ },
+ "global": {
+ "resolution": "Genel Çözünürlük",
+ "tracking": "Genel Takip"
+ }
+ },
+ "objects": {
+ "global": {
+ "tracking": "Genel Takip",
+ "filtering": "Genel Filtreleme"
+ },
+ "cameras": {
+ "tracking": "Takip",
+ "filtering": "Filtreleme"
+ }
+ },
+ "record": {
+ "global": {
+ "events": "Genel Etkinlikler"
+ },
+ "cameras": {
+ "events": "Etkinlikler"
+ }
+ },
+ "ffmpeg": {
+ "cameras": {
+ "cameraFfmpeg": "Kamera özel FFmpeg argümanları"
+ }
+ },
+ "motion": {
+ "global": {
+ "sensitivity": "Genel Hassasiyet",
+ "algorithm": "Genel Algoritma"
+ },
+ "cameras": {
+ "sensitivity": "Hassasiyet",
+ "algorithm": "Algoritma"
+ }
+ },
+ "snapshots": {
+ "global": {
+ "display": "Genel Görüntü"
+ },
+ "cameras": {
+ "display": "Görüntü"
+ }
+ }
+}
diff --git a/web/public/locales/tr/config/validation.json b/web/public/locales/tr/config/validation.json
index 73b68c5150..aaac6f566a 100644
--- a/web/public/locales/tr/config/validation.json
+++ b/web/public/locales/tr/config/validation.json
@@ -2,5 +2,7 @@
"minimum": "En az {{limit}} olmalı",
"maximum": "En fazla {{limit}} olmalı",
"exclusiveMinimum": "{{limit}}’den büyük olmalı",
- "exclusiveMaximum": "{{limit}}’den küçük olmalı"
+ "exclusiveMaximum": "{{limit}}’den küçük olmalı",
+ "minLength": "En az {{limit}} karakter olmalı",
+ "maxLength": "En fazla {{limit}} karakter olmalı"
}
diff --git a/web/public/locales/tr/views/chat.json b/web/public/locales/tr/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/tr/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/tr/views/exports.json b/web/public/locales/tr/views/exports.json
index 0c8fec129f..d6ee3ebfe2 100644
--- a/web/public/locales/tr/views/exports.json
+++ b/web/public/locales/tr/views/exports.json
@@ -1,7 +1,9 @@
{
"search": "Arama",
"documentTitle": "Dışa Aktar - Frigate",
- "deleteExport": "Dışa Aktarımı Sil",
+ "deleteExport": {
+ "label": "Dışa Aktarımı Sil"
+ },
"deleteExport.desc": "{{exportName}} adlı dışa aktarımı silmek istediğinize emin misiniz?",
"editExport": {
"saveExport": "Dışa Aktarımı Kaydet",
@@ -19,5 +21,9 @@
"downloadVideo": "Videoyu İndir",
"editName": "İsmi Düzenle",
"deleteExport": "Dışa Aktarmayı Sil"
+ },
+ "headings": {
+ "uncategorizedExports": "Kategorize Edilmemiş Dışa Aktarım",
+ "cases": "Durumlar"
}
}
diff --git a/web/public/locales/tr/views/motionSearch.json b/web/public/locales/tr/views/motionSearch.json
new file mode 100644
index 0000000000..2ed13c9e4c
--- /dev/null
+++ b/web/public/locales/tr/views/motionSearch.json
@@ -0,0 +1,10 @@
+{
+ "newSearch": "Yeni Arama",
+ "clearResults": "Sonuçları Temizle",
+ "dialog": {
+ "cameraLabel": "Kamera"
+ },
+ "timeRange": {
+ "end": "Bitiş Zamanı"
+ }
+}
diff --git a/web/public/locales/tr/views/replay.json b/web/public/locales/tr/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/tr/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/tr/views/settings.json b/web/public/locales/tr/views/settings.json
index 3d419144f1..72cc56ef85 100644
--- a/web/public/locales/tr/views/settings.json
+++ b/web/public/locales/tr/views/settings.json
@@ -28,7 +28,8 @@
"triggers": "Tetikler",
"cameraManagement": "Yönetim",
"cameraReview": "İncele",
- "roles": "Roller"
+ "roles": "Roller",
+ "profiles": "Profiller"
},
"general": {
"title": "Kullanıcı Arayüzü Ayarları",
diff --git a/web/public/locales/tr/views/system.json b/web/public/locales/tr/views/system.json
index d388118430..8ce1835f41 100644
--- a/web/public/locales/tr/views/system.json
+++ b/web/public/locales/tr/views/system.json
@@ -141,7 +141,11 @@
}
},
"title": "Kameralar",
- "overview": "Genel"
+ "overview": "Genel",
+ "connectionQuality": {
+ "expectedFps": "Beklenen FPS",
+ "fps": "FPS"
+ }
},
"lastRefreshed": "Son güncelleme: ",
"stats": {
diff --git a/web/public/locales/uk/audio.json b/web/public/locales/uk/audio.json
index 773d5e3a79..4aaf0547b7 100644
--- a/web/public/locales/uk/audio.json
+++ b/web/public/locales/uk/audio.json
@@ -21,7 +21,7 @@
"goat": "Коза",
"sheep": "Вівця",
"chicken": "Курка",
- "speech": "Мо́влення",
+ "speech": "Розмова",
"idling": "Холостий хід",
"railroad_car": "Залізничний вагон",
"alarm": "Сигналізація",
@@ -91,8 +91,8 @@
"chatter": "Балаканина",
"animal": "Тварина",
"yip": "Гавкання",
- "babbling": "Бурмотіння",
- "yell": "Кричати",
+ "babbling": "Балаканина",
+ "yell": "Крик",
"bow_wow": "Гав гав",
"growling": "Ревіння",
"purr": "Муркотіти",
diff --git a/web/public/locales/uk/common.json b/web/public/locales/uk/common.json
index 39dff176fc..21002ee0cc 100644
--- a/web/public/locales/uk/common.json
+++ b/web/public/locales/uk/common.json
@@ -120,7 +120,19 @@
"deleteNow": "Видалити негайно",
"next": "Наступне",
"unsuspended": "Відновити дію",
- "continue": "Продовжити"
+ "continue": "Продовжити",
+ "add": "Додати",
+ "applying": "Застосовую…",
+ "undo": "Скасувати",
+ "copiedToClipboard": "Скопійовано в буфер обміну",
+ "modified": "Змінено",
+ "overridden": "Перевизначено",
+ "resetToGlobal": "Скинути до Глобальних",
+ "resetToDefault": "Скинути до По замовчуванню",
+ "saveAll": "Зберігти все",
+ "savingAll": "Зберігаю все…",
+ "undoAll": "Відмінити все",
+ "retry": "Спробувати ще"
},
"menu": {
"language": {
@@ -165,7 +177,10 @@
"bg": "Български (Болгарська)",
"gl": "Galego (Галісійська)",
"id": "Bahasa Indonesia (Індонезійська)",
- "ur": "اردو (Урду)"
+ "ur": "اردو (Урду)",
+ "hr": "Hrvatski (Хорватська)",
+ "bs": "Bosanski (Боснійська)",
+ "zhHant": "繁體中文 (Традиційна китайська)"
},
"system": "Система",
"systemMetrics": "Системна метріка",
@@ -223,7 +238,11 @@
},
"appearance": "Зовнішність",
"withSystem": "Система",
- "classification": "Класифікація"
+ "classification": "Класифікація",
+ "profiles": "Профілі",
+ "actions": "Дії",
+ "features": "Особливості",
+ "chat": "Чат"
},
"unit": {
"speed": {
@@ -258,7 +277,8 @@
"error": {
"title": "Не вдалося зберегти зміни конфігурації: {{errorMessage}}",
"noMessage": "Не вдалося зберегти зміни налаштування"
- }
+ },
+ "success": "Зміни до налаштувань збережені вдало."
},
"copyUrlToClipboard": "Скопійовано URL до буфера обміну."
},
@@ -303,5 +323,10 @@
"field": {
"optional": "Необов'язково",
"internalID": "Внутрішній ідентифікатор, який Frigate використовує в конфігурації та базі даних"
+ },
+ "no_items": "Нема елементів",
+ "validation_errors": "Помилки при перевірці",
+ "credentialField": {
+ "savedPlaceholder": "Збережено - залиште порожнім щоб зберегти поточні"
}
}
diff --git a/web/public/locales/uk/components/camera.json b/web/public/locales/uk/components/camera.json
index 0836510e14..c8ccbca89b 100644
--- a/web/public/locales/uk/components/camera.json
+++ b/web/public/locales/uk/components/camera.json
@@ -68,7 +68,10 @@
"label": "Камери",
"desc": "Виберіть камери для цієї групи."
},
- "icon": "Значок"
+ "icon": "Значок",
+ "showAll": "Відобразити всі групи камер",
+ "showLess": "Показати менше",
+ "editGroups": "Редагувати групи камер"
},
"debug": {
"zones": "Зони",
@@ -82,6 +85,7 @@
"hideOptions": "Приховати параметри"
},
"boundingBox": "Обмежувальна рамка",
- "timestamp": "Позначка часу"
+ "timestamp": "Позначка часу",
+ "paths": "Шляхи"
}
}
diff --git a/web/public/locales/uk/components/dialog.json b/web/public/locales/uk/components/dialog.json
index 7ede7901bf..f1bab4a3fd 100644
--- a/web/public/locales/uk/components/dialog.json
+++ b/web/public/locales/uk/components/dialog.json
@@ -53,16 +53,29 @@
"export": {
"toast": {
"error": {
- "failed": "Не вдалося розпочати експорт: {{error}}",
+ "failed": "Не вдалося додати до черги експорт: {{error}}",
"endTimeMustAfterStartTime": "Час закінчення повинен бути після часу початку",
"noVaildTimeSelected": "Не вибрано допустимий діапазон часу"
},
"success": "Експорт успішно розпочато. Перегляньте файл на сторінці експорту.",
- "view": "Переглянути"
+ "view": "Переглянути",
+ "queued": "Експорт додано до черги. Дивіться прогрес на сторінці експортів.",
+ "batchSuccess_one": "Розпочинаю {{count}} експорт. Відкриваю справу зараз.",
+ "batchSuccess_few": "Розпочинаю {{count}} експортів. Відкриваю справу зараз.",
+ "batchSuccess_many": "Розпочинаю {{count}} експортів. Відкриваю справу зараз.",
+ "batchPartial": "Розпочато {{successful}} з {{total}} експортів. Невдалі камери: {{failedCameras}}",
+ "batchFailed": "Не зміг розпочати {{total}} експортів. Невдалі камери: {{failedCameras}}",
+ "batchQueuedSuccess_one": "Додав до черги {{count}} експорт. Відкриваю справу зараз.",
+ "batchQueuedSuccess_few": "Додав до черги {{count}} експортів. Відкриваю справу зараз.",
+ "batchQueuedSuccess_many": "Додав до черги {{count}} експортів. Відкриваю справу зараз.",
+ "batchQueuedPartial": "Додав до черги {{successful}} з {{total}} експортів. Невдалі камери: {{failedCameras}}",
+ "batchQueueFailed": "Не зміг додати до черги {{total}} експортів. Невдалі камери: {{failedCameras}}"
},
"fromTimeline": {
"saveExport": "Зберегти експорт",
- "previewExport": "Попередній перегляд експорту"
+ "previewExport": "Попередній перегляд експорту",
+ "queueingExport": "Додаю до черги експорт...",
+ "useThisRange": "Використовуй цей діапазон"
},
"time": {
"fromTimeline": "Вибір шкали часу",
@@ -84,7 +97,59 @@
},
"select": "Вибрати",
"export": "Експорт",
- "selectOrExport": "Выбiр або експорт"
+ "selectOrExport": "Выбiр або експорт",
+ "case": {
+ "newCaseOption": "Створити новий випадок",
+ "newCaseNamePlaceholder": "Ім'я для нового випадку",
+ "newCaseDescriptionPlaceholder": "Опис випадку",
+ "label": "Випадок",
+ "nonAdminHelp": "Новий випадок буде створено для цих експортів.",
+ "placeholder": "Оберіть випадок"
+ },
+ "queueing": "Додаю експорт у чергу...",
+ "tabs": {
+ "export": "Одна камера",
+ "multiCamera": "Мульті-камера"
+ },
+ "multiCamera": {
+ "timeRange": "Діапазон часу",
+ "selectFromTimeline": "Оберіть з шкали часу",
+ "cameraSelection": "Камери",
+ "cameraSelectionHelp": "Камери з відстежуваними об'єктами у цьому часовому діапазоні є попередньо обраними",
+ "checkingActivity": "Перевіряю активність камери...",
+ "noCameras": "Доступні камери відсутні",
+ "detectionCount_one": "{{count}} відстежувана камера",
+ "detectionCount_few": "{{count}} відстежувані камери",
+ "detectionCount_many": "{{count}} відстежуваних камер",
+ "nameLabel": "Ім'я для експорту",
+ "namePlaceholder": "Необов'язкове базове ім'я для цих експортів",
+ "queueingButton": "Додаю експорти у чергу...",
+ "exportButton_one": "Експорт {{count}} камери",
+ "exportButton_few": "Експорт {{count}} камер",
+ "exportButton_many": "Експорт {{count}} камер"
+ },
+ "multi": {
+ "title_one": "Експорт {{count}} рецензії",
+ "title_few": "Експорт {{count}} рецензій",
+ "title_many": "Експорт {{count}} рецензій",
+ "description": "Експорт всіх обраних рецензій. Всі експорти будуть згруповані під спільною справою.",
+ "descriptionNoCase": "Експорт всіх обраних рецензій.",
+ "caseNamePlaceholder": "Переглянути експорт - {{date}}",
+ "exportButton_one": "Переглянути {{count}} рецензію",
+ "exportButton_few": "Переглянути {{count}} рецензії",
+ "exportButton_many": "Переглянути {{count}} рецензій",
+ "exportingButton": "Експортую...",
+ "toast": {
+ "started_one": "Розпочинаю {{count}} експорт. Відкриваю справу зараз.",
+ "started_few": "Розпочинаю {{count}} експортів. Відкриваю справу зараз.",
+ "started_many": "Розпочинаю {{count}} експортів. Відкриваю справу зараз.",
+ "startedNoCase_one": "Розпочинаю {{count}} експорт.",
+ "startedNoCase_few": "Розпочинаю {{count}} експортів.",
+ "startedNoCase_many": "Розпочинаю {{count}} експортів.",
+ "partial": "Розпочато {{successful}} з {{total}} експортів. Невдало: {{failedItems}}",
+ "failed": "Не зміг розпочати {{total}} експортів. Невдало: {{failedItems}}"
+ }
+ }
},
"recording": {
"button": {
@@ -96,12 +161,20 @@
"confirmDelete": {
"title": "Підтвердити вилучення",
"desc": {
- "selected": "Ви впевнені, що хочете видалити все записане відео, пов'язане з цим пунктом огляду? Утримуйте клавішу Shift , щоб обійти це діалогове вікно в майбутньому."
+ "selected": "Ви впевнені, що хочете видалити все записане відео, пов'язане з цим пунктом огляду? Утримуйте клавішу Shift , щоб обійти це діалогове вікно в майбутньому."
},
"toast": {
"error": "Не вдалося видалити: {{error}}",
"success": "Відеозаписи, пов’язані з вибраними елементами огляду, успішно видалено."
}
+ },
+ "shareTimestamp": {
+ "label": "Поділитися позначкою часу",
+ "title": "Поділитися позначкою часу",
+ "description": "Поділитися URL з позначкою часу поточної позиції плеєру або оберіть довільну позначку часу. Зверніть увагу, що це не публічна URL і вона доступна тільки для користувачів з доступом до Frigate та цієї камери.",
+ "custom": "Довільна позначка часу",
+ "button": "Поділитися URL з позначкою часу",
+ "shareTitle": "Позначка часу для Frigate рецензії: {{camera}}"
}
},
"restart": {
@@ -111,7 +184,8 @@
"title": "Frigate перезапускається",
"content": "Цю сторінку буде перезавантажено за {{countdown}} секунд.",
"button": "Примусово перезавантажити"
- }
+ },
+ "description": "Це призупинить роботу Frigate на час перезавантаження."
},
"imagePicker": {
"selectImage": "Вибір мініатюри відстежуваного об'єкта",
diff --git a/web/public/locales/uk/components/player.json b/web/public/locales/uk/components/player.json
index 746eba6c12..7577dff25c 100644
--- a/web/public/locales/uk/components/player.json
+++ b/web/public/locales/uk/components/player.json
@@ -37,7 +37,7 @@
},
"streamOffline": {
"title": "Струм офлайн",
- "desc": "Потік detect камера {{cameraName}} не отримувала ніяких кадрів, перевіряйте журнали помилок"
+ "desc": "Потік detect камери {{cameraName}} не отримував ніяких кадрів, перевірте журнал помилок"
},
"cameraDisabled": "Камера вимкнена",
"toast": {
@@ -47,5 +47,6 @@
"error": {
"submitFrigatePlusFailed": "Не вдалося надіслати фрейм Frigate+"
}
- }
+ },
+ "cameraOff": "Камера вимкнена"
}
diff --git a/web/public/locales/uk/config/cameras.json b/web/public/locales/uk/config/cameras.json
index 0967ef424b..4de1a14df5 100644
--- a/web/public/locales/uk/config/cameras.json
+++ b/web/public/locales/uk/config/cameras.json
@@ -1 +1,72 @@
-{}
+{
+ "name": {
+ "label": "Назва камери",
+ "description": "Потрібно вказати назву камери"
+ },
+ "label": "Конфігурація камери",
+ "friendly_name": {
+ "label": "Зрозуміле ім'я",
+ "description": "Зручна назва камери, що використовується в інтерфейсі Frigate"
+ },
+ "enabled": {
+ "label": "Увімкнено",
+ "description": "Увімкнено"
+ },
+ "audio": {
+ "label": "Виявлення звуку",
+ "enabled": {
+ "label": "Увімкнути виявлення звуку",
+ "description": "Увімкнути або вимкнути виявлення звукових подій для цієї камери."
+ },
+ "max_not_heard": {
+ "description": "Кількість секунд без налаштованого типу звуку перед тим, як звукова подія закінчиться.",
+ "label": "Кінець очікування"
+ },
+ "min_volume": {
+ "label": "Мінімальний рівень звуку",
+ "description": "Поріг мінімального середньоквадратичного значення необхідний для запуску розпізнавання звуку; чим нижче значення, тим вища чутливість (наприклад, 200 — висока, 500 — середня, 1000 — низька)."
+ },
+ "num_threads": {
+ "description": "Поріг мінімального середньоквадратичного значення необхідний для запуску розпізнавання звуку; чим нижче значення, тим вища чутливість (наприклад, 200 — висока, 500 — середня, 1000 — низька)."
+ },
+ "listen": {
+ "description": "Список звукових подій для виявлення (наприклад, bark, fire_alarm, speech, yell).",
+ "label": "Типи звукових подій"
+ },
+ "filters": {
+ "label": "Фільтри звуку"
+ },
+ "description": "Налаштування звукових подій для цієї камери."
+ },
+ "audio_transcription": {
+ "label": "Транскрипція аудіо",
+ "enabled": {
+ "label": "Увімкнути транскрипцію"
+ },
+ "live_enabled": {
+ "description": "Увімкнути потокову транскрипцію аудіо в реальному часі коли вона надходить."
+ }
+ },
+ "birdseye": {
+ "label": "Режим Birdseye",
+ "description": "Налаштування для складеного елементу перегляду режиму Birdseye який відображає потоки з декількох камер на єдиному макеті.",
+ "enabled": {
+ "label": "Увімкнути Birdseye",
+ "description": "Увімкнути або вимкнути функцію перегляду у режимі Birdseye."
+ },
+ "mode": {
+ "label": "Режим відстежування",
+ "description": "Режими для додавання камер в Birdseye: 'objects' (об'єкти), 'motion' (рух) або 'continuous' (безперервний)."
+ },
+ "order": {
+ "label": "Розміщення",
+ "description": "Номер позиції, що визначає розміщення камери на макеті Birdseye."
+ }
+ },
+ "detect": {
+ "label": "Виявлення об'єктів",
+ "enabled": {
+ "label": "Увімкнути виявлення об'єктів"
+ }
+ }
+}
diff --git a/web/public/locales/uk/config/global.json b/web/public/locales/uk/config/global.json
index 0967ef424b..6618f81f66 100644
--- a/web/public/locales/uk/config/global.json
+++ b/web/public/locales/uk/config/global.json
@@ -1 +1,54 @@
-{}
+{
+ "audio": {
+ "label": "Виявлення звуку",
+ "enabled": {
+ "label": "Увімкнути виявлення звуку"
+ },
+ "max_not_heard": {
+ "description": "Кількість секунд без налаштованого типу звуку перед тим, як звукова подія закінчиться.",
+ "label": "Кінець очікування"
+ },
+ "min_volume": {
+ "label": "Мінімальний рівень звуку",
+ "description": "Поріг мінімального середньоквадратичного значення необхідний для запуску розпізнавання звуку; чим нижче значення, тим вища чутливість (наприклад, 200 — висока, 500 — середня, 1000 — низька)."
+ },
+ "num_threads": {
+ "description": "Поріг мінімального середньоквадратичного значення необхідний для запуску розпізнавання звуку; чим нижче значення, тим вища чутливість (наприклад, 200 — висока, 500 — середня, 1000 — низька)."
+ },
+ "listen": {
+ "description": "Список звукових подій для виявлення (наприклад, bark, fire_alarm, speech, yell).",
+ "label": "Типи звукових подій"
+ },
+ "filters": {
+ "label": "Фільтри звуку"
+ }
+ },
+ "audio_transcription": {
+ "label": "Транскрипція аудіо",
+ "live_enabled": {
+ "description": "Увімкнути потокову транскрипцію аудіо в реальному часі коли вона надходить."
+ }
+ },
+ "birdseye": {
+ "label": "Режим Birdseye",
+ "description": "Налаштування для складеного елементу перегляду режиму Birdseye який відображає потоки з декількох камер на єдиному макеті.",
+ "enabled": {
+ "label": "Увімкнути Birdseye",
+ "description": "Увімкнути або вимкнути функцію перегляду у режимі Birdseye."
+ },
+ "mode": {
+ "label": "Режим відстежування",
+ "description": "Режими для додавання камер в Birdseye: 'objects' (об'єкти), 'motion' (рух) або 'continuous' (безперервний)."
+ },
+ "order": {
+ "label": "Розміщення",
+ "description": "Номер позиції, що визначає розміщення камери на макеті Birdseye."
+ }
+ },
+ "detect": {
+ "label": "Виявлення об'єктів",
+ "enabled": {
+ "label": "Увімкнути виявлення об'єктів"
+ }
+ }
+}
diff --git a/web/public/locales/uk/views/chat.json b/web/public/locales/uk/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/uk/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/uk/views/classificationModel.json b/web/public/locales/uk/views/classificationModel.json
index faceecd91c..82739b61d1 100644
--- a/web/public/locales/uk/views/classificationModel.json
+++ b/web/public/locales/uk/views/classificationModel.json
@@ -12,12 +12,12 @@
},
"toast": {
"success": {
- "deletedCategory_one": "Видалений клас",
- "deletedCategory_few": "",
- "deletedCategory_many": "",
- "deletedImage_one": "Видалені зображення",
- "deletedImage_few": "",
- "deletedImage_many": "",
+ "deletedCategory_one": "Видалено {{count}} клас",
+ "deletedCategory_few": "Видалено {{count}} класи",
+ "deletedCategory_many": "Видалено {{count}} класів",
+ "deletedImage_one": "Видалено {{count}} зображення",
+ "deletedImage_few": "Видалено {{count}} зображення",
+ "deletedImage_many": "Видалено {{count}} зображень",
"categorizedImage": "Зображення успішно класифіковано",
"trainedModel": "Успішно навчена модель.",
"trainingModel": "Успішно розпочато навчання моделі.",
diff --git a/web/public/locales/uk/views/motionSearch.json b/web/public/locales/uk/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/uk/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/uk/views/replay.json b/web/public/locales/uk/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/uk/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/uk/views/settings.json b/web/public/locales/uk/views/settings.json
index e9bc0dd420..a7816acdfe 100644
--- a/web/public/locales/uk/views/settings.json
+++ b/web/public/locales/uk/views/settings.json
@@ -135,7 +135,7 @@
},
"context": {
"documentation": "Прочитати документацію",
- "title": "Маски руху використовуються для запобігання виявлення небажаних типів руху (наприклад: гілки дерева, часові мітки камери). Слід використовувати маски рухудуже економно , надмірне маскування ускладнить відстеження об'єктів."
+ "title": "Маски руху використовуються для запобігання виявлення небажаних типів руху (наприклад: гілки дерева, часові мітки камери). Слід використовувати маски руху дуже економно , надмірне маскування ускладнить відстеження об'єктів."
},
"clickDrawPolygon": "Клацніть, щоб намалювати багатокутник на зображенні.",
"add": "Нова маска руху",
@@ -300,7 +300,7 @@
"all": "Усі маски та зони"
},
"motionMaskLabel": "Маска руху {{number}}",
- "objectMaskLabel": "Маска об'єкта {{number}} ({{label}})"
+ "objectMaskLabel": "Маска об'єкта {{number}}"
},
"debug": {
"zones": {
@@ -451,7 +451,7 @@
"snapshots": "Знімки",
"cleanCopySnapshots": "clean_copy Знімки"
},
- "cleanCopyWarning": "На деяких камерах увімкнено знімки екрана, але вимкнено чисте копіювання. Щоб мати змогу надсилати зображення з цих камер до Frigate+, потрібно ввімкнути параметр clean_copy у конфігурації знімків екрана."
+ "cleanCopyWarning": "На деяких камерах знімки екрана вимкнені"
},
"apiKey": {
"desc": "Ключ API Frigate+ забезпечує інтеграцію з сервісом Frigate+.",
@@ -529,7 +529,7 @@
"motionDetectionTuner": {
"improveContrast": {
"title": "Покращення контрастності",
- "desc": "Покращення контрастності для темніших сцен. За замовчуванням: УВІМК. "
+ "desc": "Покращення контрастності для темніших сцен. За замовчуванням: УВІМК "
},
"desc": {
"documentation": "Прочитайте посібник з налаштування руху",
@@ -578,7 +578,8 @@
"triggers": "Тригери",
"roles": "Ролі",
"cameraManagement": "Управління",
- "cameraReview": "Огляду"
+ "cameraReview": "Огляду",
+ "profiles": "Профілі"
},
"dialog": {
"unsavedChanges": {
@@ -647,7 +648,7 @@
"hide": "Приховати пароль",
"requirements": {
"title": "Вимоги до пароля:",
- "length": "Принаймні 8 символів",
+ "length": "Принаймні 12 символів",
"uppercase": "Принаймні одна велика літера",
"digit": "Принаймні одна цифра",
"special": "Принаймні один спеціальний символ (!@#$%^&*(),.?\":{}|<>)"
diff --git a/web/public/locales/uk/views/system.json b/web/public/locales/uk/views/system.json
index b65616c60e..4d8412f44f 100644
--- a/web/public/locales/uk/views/system.json
+++ b/web/public/locales/uk/views/system.json
@@ -40,7 +40,10 @@
"tips": {
"title": "Інформація про зонд камери"
},
- "aspectRatio": "співвідношення сторін"
+ "aspectRatio": "співвідношення сторін",
+ "keyframes": {
+ "observedDuration": "Тривалість спостереження:"
+ }
},
"overview": "Огляд",
"framesAndDetections": "Кадри / Виявлення"
@@ -68,7 +71,7 @@
"classification_speed": "Швидкість класифікації {{name}}",
"classification_events_per_second": "{{name}} Подій класифікації за секунду"
},
- "title": "Збагаченням",
+ "title": "Збагачення",
"infPerSecond": "Висновки за секунду",
"averageInf": "Середній час висновування"
},
@@ -178,7 +181,8 @@
"logs": {
"frigate": "Фрегатні журнали - Фрегат",
"go2rtc": "Журнали Go2RTC - Фрегат",
- "nginx": "Журнали Nginx - Фрегат"
+ "nginx": "Журнали Nginx - Фрегат",
+ "websocket": "Журнал повідомлень - Frigate"
}
},
"title": "Система",
@@ -204,6 +208,25 @@
"fetchingLogsFailed": "Помилка отримання журналів: {{errorMessage}}",
"whileStreamingLogs": "Помилка під час потокової передачі журналів: {{errorMessage}}"
}
+ },
+ "websocket": {
+ "label": "Повідомлення",
+ "pause": "Павза",
+ "resume": "Продовжити",
+ "clear": "Очистити",
+ "filter": {
+ "all": "Всі теми (topics)",
+ "topics": "Теми (topics)",
+ "events": "Події",
+ "reviews": "Перевірки",
+ "classification": "Класифікація",
+ "face_recognition": "Розпізнавання обличчя",
+ "lpr": "Розпізнавання номерних знаків (LPR)",
+ "camera_activity": "Активність камери",
+ "system": "Система",
+ "camera": "Камера",
+ "all_cameras": "Всі камери"
+ }
}
}
}
diff --git a/web/public/locales/ur/audio.json b/web/public/locales/ur/audio.json
index b6b532255a..6d7904e2b7 100644
--- a/web/public/locales/ur/audio.json
+++ b/web/public/locales/ur/audio.json
@@ -34,5 +34,20 @@
"train": "ٹرین",
"bicycle": "سائیکل",
"crying": "رونا",
- "sigh": "آہیں"
+ "sigh": "آہیں",
+ "choir": "کوئر",
+ "yodeling": "یوڈیلنگ",
+ "chant": "نعرہ لگانا",
+ "mantra": "منتر",
+ "child_singing": "چائلڈ گانا",
+ "synthetic_singing": "مصنوعی گانا",
+ "rapping": "ریپنگ",
+ "humming": "گنگنانا",
+ "groan": "کراہنا",
+ "grunt": "گرنٹ",
+ "whistling": "سیٹی بجانا",
+ "breathing": "سانس لینا",
+ "gasp": "ہانپنا",
+ "pant": "ہانپنا",
+ "snort": "خراٹے"
}
diff --git a/web/public/locales/ur/components/auth.json b/web/public/locales/ur/components/auth.json
index e7625b65c5..ab19b2e150 100644
--- a/web/public/locales/ur/components/auth.json
+++ b/web/public/locales/ur/components/auth.json
@@ -1,6 +1,6 @@
{
"form": {
- "user": "صارف نام",
+ "user": "اکاؤنٹ کا نام",
"password": "پاسورڈ",
"login": "لاگ ان",
"errors": {
diff --git a/web/public/locales/ur/config/cameras.json b/web/public/locales/ur/config/cameras.json
index 0967ef424b..23c240d85a 100644
--- a/web/public/locales/ur/config/cameras.json
+++ b/web/public/locales/ur/config/cameras.json
@@ -1 +1,3 @@
-{}
+{
+ "label": "کیمرے کی ترتیب"
+}
diff --git a/web/public/locales/ur/config/global.json b/web/public/locales/ur/config/global.json
index 0967ef424b..805c0d3b7d 100644
--- a/web/public/locales/ur/config/global.json
+++ b/web/public/locales/ur/config/global.json
@@ -1 +1,5 @@
-{}
+{
+ "version": {
+ "label": "موجودہ کنفیگریشن ورژن"
+ }
+}
diff --git a/web/public/locales/ur/config/groups.json b/web/public/locales/ur/config/groups.json
index 0967ef424b..13c20a36ec 100644
--- a/web/public/locales/ur/config/groups.json
+++ b/web/public/locales/ur/config/groups.json
@@ -1 +1,7 @@
-{}
+{
+ "audio": {
+ "global": {
+ "detection": "عالمی کھوج"
+ }
+ }
+}
diff --git a/web/public/locales/ur/config/validation.json b/web/public/locales/ur/config/validation.json
index 0967ef424b..b871b9f6ed 100644
--- a/web/public/locales/ur/config/validation.json
+++ b/web/public/locales/ur/config/validation.json
@@ -1 +1,3 @@
-{}
+{
+ "minimum": "کم از کم {{limit}} ہونا چاہیے"
+}
diff --git a/web/public/locales/ur/views/chat.json b/web/public/locales/ur/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/ur/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/ur/views/classificationModel.json b/web/public/locales/ur/views/classificationModel.json
index 0967ef424b..7893f7d013 100644
--- a/web/public/locales/ur/views/classificationModel.json
+++ b/web/public/locales/ur/views/classificationModel.json
@@ -1 +1,3 @@
-{}
+{
+ "documentTitle": "درجہ بندی کے ماڈلز - فریگیٹ"
+}
diff --git a/web/public/locales/ur/views/faceLibrary.json b/web/public/locales/ur/views/faceLibrary.json
index cfb7dce625..9185231539 100644
--- a/web/public/locales/ur/views/faceLibrary.json
+++ b/web/public/locales/ur/views/faceLibrary.json
@@ -1,6 +1,6 @@
{
"description": {
- "addFace": "فیس لائبریری میں نئی کلیکشن شامل کرنے کا طریقہ بتائیں۔",
+ "addFace": "اپنی پہلی تصویر اپ لوڈ کرکے فیس لائبریری میں ایک نیا کلیکشن شامل کریں۔",
"placeholder": "اس مجموعہ کے لیے ایک نام درج کریں",
"invalidName": "غلط نام۔ ناموں میں صرف حروف، اعداد، فاصلے، اپوسٹروف، انڈر اسکور، اور ہائفن شامل ہو سکتے ہیں۔"
},
diff --git a/web/public/locales/ur/views/motionSearch.json b/web/public/locales/ur/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/ur/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/ur/views/replay.json b/web/public/locales/ur/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/ur/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/uz/audio.json b/web/public/locales/uz/audio.json
index ddd93cd741..436bf9121a 100644
--- a/web/public/locales/uz/audio.json
+++ b/web/public/locales/uz/audio.json
@@ -1,3 +1,4 @@
{
- "speech": "So'zlashuv"
+ "speech": "Nutq",
+ "laughter": "Kulgi"
}
diff --git a/web/public/locales/uz/views/chat.json b/web/public/locales/uz/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/uz/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/uz/views/motionSearch.json b/web/public/locales/uz/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/uz/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/uz/views/replay.json b/web/public/locales/uz/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/uz/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/vi/views/chat.json b/web/public/locales/vi/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/vi/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/vi/views/motionSearch.json b/web/public/locales/vi/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/vi/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/vi/views/replay.json b/web/public/locales/vi/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/vi/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/yue-Hant/views/chat.json b/web/public/locales/yue-Hant/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/yue-Hant/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/yue-Hant/views/motionSearch.json b/web/public/locales/yue-Hant/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/yue-Hant/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/yue-Hant/views/replay.json b/web/public/locales/yue-Hant/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/yue-Hant/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zh-CN/common.json b/web/public/locales/zh-CN/common.json
index e9337cfc79..ebb58cecfb 100644
--- a/web/public/locales/zh-CN/common.json
+++ b/web/public/locales/zh-CN/common.json
@@ -221,7 +221,9 @@
"gl": "加利西亚语 (Galego)",
"id": "印度尼西亚语 (Bahasa Indonesia)",
"ur": "乌尔都语 (اردو)",
- "hr": "克罗地亚语 (Hrvatski)"
+ "hr": "克罗地亚语 (Hrvatski)",
+ "bs": "波斯尼亚语(Bosanski)",
+ "zhHant": "繁体中文 (Traditional Chinese)"
},
"appearance": "外观",
"darkMode": {
@@ -273,7 +275,8 @@
"classification": "目标分类",
"actions": "操作",
"chat": "聊天",
- "profiles": "配置模板"
+ "profiles": "配置模板",
+ "features": "功能"
},
"toast": {
"copyUrlToClipboard": "已复制链接到剪贴板。",
@@ -290,7 +293,7 @@
"title": "权限组",
"admin": "管理员",
"viewer": "成员",
- "desc": "管理员可以完全访问Frigate界面上所有功能。成员则仅能查看摄像头、核查项和历史录像。"
+ "desc": "管理员可以完全访问 Frigate 界面上所有功能。成员则仅能查看摄像头、核查项和历史录像。"
},
"accessDenied": {
"documentTitle": "没有权限 - Frigate",
@@ -317,5 +320,8 @@
"internalID": "Frigate 在配置与数据库中使用的内部 ID"
},
"no_items": "没有项目",
- "validation_errors": "验证错误"
+ "validation_errors": "验证错误",
+ "credentialField": {
+ "savedPlaceholder": "已保存 — 留空则保留当前设置"
+ }
}
diff --git a/web/public/locales/zh-CN/components/camera.json b/web/public/locales/zh-CN/components/camera.json
index 9bd70155e4..1e45e408d6 100644
--- a/web/public/locales/zh-CN/components/camera.json
+++ b/web/public/locales/zh-CN/components/camera.json
@@ -68,7 +68,10 @@
"placeholder": "选择视频流"
},
"birdseye": "鸟瞰图"
- }
+ },
+ "showAll": "显示所有的摄像头组摄像头摄像头",
+ "showLess": "显示更少",
+ "editGroups": "编辑摄像头组"
},
"debug": {
"options": {
diff --git a/web/public/locales/zh-CN/components/dialog.json b/web/public/locales/zh-CN/components/dialog.json
index d2013adcdb..35e45b7926 100644
--- a/web/public/locales/zh-CN/components/dialog.json
+++ b/web/public/locales/zh-CN/components/dialog.json
@@ -32,6 +32,9 @@
"ask_a": "这个目标/物体是 {{label}} 吗?",
"ask_an": "这个目标/物体是 {{label}} 吗?",
"ask_full": "这个目标/物体是 {{untranslatedLabel}} ({{translatedLabel}}) 吗?"
+ },
+ "toast": {
+ "error": "提交至 Frigate+ 失败,请检查您的网络连接并重试。"
}
}
},
@@ -62,19 +65,71 @@
"toast": {
"success": "导出成功。进入 导出 页面查看文件。",
"error": {
- "failed": "导出失败:{{error}}",
+ "failed": "未能加入导出队列:{{error}}",
"endTimeMustAfterStartTime": "结束时间必须在开始时间之后",
"noVaildTimeSelected": "未选择有效的时间范围"
},
- "view": "查看"
+ "view": "查看",
+ "queued": "导出已加入队列。请在导出页面查看进度。",
+ "batchSuccess_other": "已开始 {{count}} 个导出,正在打开合集。",
+ "batchPartial": "已开始 {{total}} 个导出中的 {{successful}} 个。失败的摄像头:{{failedCameras}}",
+ "batchFailed": "启动导出失败(共 {{total}} 个)。失败的摄像头:{{failedCameras}}",
+ "batchQueuedSuccess_other": "已排队 {{count}} 个导出,正在打开合集。",
+ "batchQueuedPartial": "已将 {{total}} 个导出中的 {{successful}} 个加入队列。失败的摄像头:{{failedCameras}}",
+ "batchQueueFailed": "未能将 {{total}} 个导出加入队列。失败的摄像头:{{failedCameras}}"
},
"fromTimeline": {
"saveExport": "保存导出",
- "previewExport": "预览导出"
+ "previewExport": "预览导出",
+ "queueingExport": "正在加入导出队列…",
+ "useThisRange": "使用此范围"
},
"case": {
"label": "合集",
- "placeholder": "选择合集"
+ "placeholder": "选择合集",
+ "newCaseOption": "创建新合集",
+ "newCaseNamePlaceholder": "新合集名称",
+ "newCaseDescriptionPlaceholder": "合集描述",
+ "nonAdminHelp": "将为这些导出文件创建一个新的合集。"
+ },
+ "queueing": "正在加入导出队列…",
+ "tabs": {
+ "export": "单个摄像头",
+ "multiCamera": "多个摄像头"
+ },
+ "multiCamera": {
+ "timeRange": "时间范围",
+ "selectFromTimeline": "从时间线选择",
+ "cameraSelection": "摄像头",
+ "cameraSelectionHelp": "在此时间范围内具有追踪目标的摄像头会被预先选中",
+ "checkingActivity": "正在检查摄像头活动…",
+ "noCameras": "没有可用的摄像头",
+ "detectionCount_other": "{{count}} 个追踪目标",
+ "nameLabel": "导出名称",
+ "namePlaceholder": "这些导出文件的可选基础名称",
+ "queueingButton": "正在加入导出队列…",
+ "exportButton_other": "导出 {{count}} 个摄像头",
+ "searchOrSelectGroup": "搜索或选择摄像头组…",
+ "selectAll": "选择所有摄像头",
+ "clearSelection": "清空选择",
+ "selectWithActivity": "包含追踪目标的摄像头",
+ "selectGroup": "选择组",
+ "noMatchingCameras": "没有匹配搜索的摄像头",
+ "selectedCount": "{{selected}} / {{total}} 个"
+ },
+ "multi": {
+ "title_other": "导出 {{count}} 个核查",
+ "description": "导出每个选定的核查项。所有导出文件将归入同一个合集。",
+ "descriptionNoCase": "导出每个选定的核查项。",
+ "caseNamePlaceholder": "核查导出 - {{date}}",
+ "exportButton_other": "导出 {{count}} 个核查",
+ "exportingButton": "导出中…",
+ "toast": {
+ "started_other": "已开始 {{count}} 个导出。正在打开合集。",
+ "startedNoCase_other": "已开始 {{count}} 个导出。",
+ "partial": "已启动 {{total}} 个导出,其中 {{successful}} 个成功。失败项:{{failedItems}}",
+ "failed": "启动导出失败(共 {{total}} 个)。失败项:{{failedItems}}"
+ }
}
},
"streaming": {
@@ -122,6 +177,14 @@
"markAsReviewed": "标记为已核查",
"deleteNow": "立即删除",
"markAsUnreviewed": "标记为未核查"
+ },
+ "shareTimestamp": {
+ "label": "分享该时间片段",
+ "title": "分享该时间片段",
+ "description": "分享带当前录制的播放时间的地址,或选择自定义时间。请注意,这不是公开的分享链接,只有具有 Frigate 和此摄像头访问权限的用户才能访问。",
+ "custom": "自定义时间",
+ "button": "分享时间片段地址",
+ "shareTitle": "Frigate 核查时间:{{camera}}"
}
},
"imagePicker": {
diff --git a/web/public/locales/zh-CN/components/player.json b/web/public/locales/zh-CN/components/player.json
index 0336c32a12..70956ccb35 100644
--- a/web/public/locales/zh-CN/components/player.json
+++ b/web/public/locales/zh-CN/components/player.json
@@ -4,7 +4,8 @@
"noPreviewFoundFor": "没有在 {{cameraName}} 下找到预览",
"submitFrigatePlus": {
"title": "提交此帧到 Frigate+?",
- "submit": "提交"
+ "submit": "提交",
+ "previewError": "无法加载快照预览。该录制当前可能不可用。"
},
"livePlayerRequiredIOSVersion": "此直播流类型需要 iOS 17.1 或更高版本。",
"streamOffline": {
@@ -47,5 +48,6 @@
"error": {
"submitFrigatePlusFailed": "提交帧到 Frigate+ 失败"
}
- }
+ },
+ "cameraOff": "摄像头已关闭"
}
diff --git a/web/public/locales/zh-CN/config/cameras.json b/web/public/locales/zh-CN/config/cameras.json
index aa627f5494..93d82d6825 100644
--- a/web/public/locales/zh-CN/config/cameras.json
+++ b/web/public/locales/zh-CN/config/cameras.json
@@ -13,7 +13,7 @@
"description": "开启"
},
"audio": {
- "label": "音频事件",
+ "label": "音频检测",
"description": "此摄像头的音频事件检测设置。",
"enabled": {
"label": "开启音频检测",
@@ -33,11 +33,15 @@
},
"listen": {
"label": "监听类型",
- "description": "要检测的音频事件类型列表(例如:bark、fire_alarm、scream、speech、yell)。"
+ "description": "要检测的音频事件类型列表(例如:bark、fire_alarm、speech、yell)。"
},
"filters": {
"label": "音频过滤器",
- "description": "按音频类型的过滤器设置,如用于减少误报的置信度阈值。"
+ "description": "按音频类型的过滤器设置,如用于减少误报的置信度阈值。",
+ "threshold": {
+ "label": "最低音频置信度",
+ "description": "设置音频事件所需的最低置信度阈值。"
+ }
},
"enabled_in_config": {
"label": "原始音频状态",
@@ -68,7 +72,7 @@
},
"mode": {
"label": "追踪模式",
- "description": "在鸟瞰视图中包含摄像头的模式:'objects'(目标)、'motion'(动作)或 'continuous'(持续)。"
+ "description": "在鸟瞰视图中包含摄像头的模式有:“基于目标”、“基于画面变动”或“连续”。"
},
"order": {
"label": "排序位置",
@@ -127,7 +131,7 @@
},
"classifier": {
"label": "开启视觉分类器",
- "description": "使用视觉分类器,即使检测框有轻微抖动,也能准确判断物体是真的静止。"
+ "description": "使用视觉分类器,即使检测框有轻微抖动,也能准确判断物体是否为静止。"
}
},
"annotation_offset": {
@@ -148,11 +152,11 @@
}
},
"ffmpeg": {
- "label": "FFmpeg",
- "description": "FFmpeg 编解码相关设置,包含可执行文件路径、命令行参数、硬件加速选项,以及按不同功能划分的输出参数。",
+ "label": "视频流(FFmpeg)",
+ "description": "摄像头视频流和 FFmpeg 编解码相关设置,包含可执行文件路径、命令行参数、硬件加速,以及按不同功能划分的输出参数。",
"path": {
"label": "FFmpeg 路径",
- "description": "要使用的 FFmpeg 可执行文件路径,或版本别名(如 \"5.0\" 或 \"7.0\")。"
+ "description": "要使用的 FFmpeg 可执行文件路径,或版本别名(如 \"7.0\" 或 \"8.0\")。"
},
"global_args": {
"label": "FFmpeg 全局参数",
@@ -268,20 +272,20 @@
"description": "为此摄像头启用和控制通知的设置。"
},
"live": {
- "label": "实时回放",
+ "label": "实时监控观看",
"streams": {
"label": "实时监控流名称",
"description": "配置的流名称到用于实时监控播放的 restream/go2rtc 名称的映射。"
},
"height": {
"label": "实时监控高度",
- "description": "在 Web UI 中渲染 jsmpeg 实时监控流的高度(像素);必须小于等于检测流高度。"
+ "description": "在网页页面中渲染 jsmpeg 实时监控流的高度(像素);必须小于等于检测流高度。"
},
"quality": {
"label": "实时监控质量",
"description": "jsmpeg 流的编码质量(1 最高,31 最低)。"
},
- "description": "用于控制实时流选择、分辨率和质量的 Web UI 设置。"
+ "description": "用于控制实时流选择、分辨率和质量的网页页面设置。"
},
"motion": {
"label": "画面变动检测",
@@ -388,51 +392,51 @@
"label": "原始遮罩"
},
"genai": {
- "label": "GenAI 目标配置",
- "description": "用于描述追踪目标和发送帧进行生成的 GenAI 选项。",
+ "label": "生成式 AI 目标配置",
+ "description": "用于发送画面给生成式 AI 进行生成和描述追踪目标的选项。",
"enabled": {
- "label": "开启 GenAI",
- "description": "默认启用 GenAI 生成追踪目标的描述。"
+ "label": "开启生成式 AI",
+ "description": "默认开启生成式 AI 生成追踪目标的描述。"
},
"use_snapshot": {
"label": "使用快照",
- "description": "使用目标快照而不是缩略图进行 GenAI 描述生成。"
+ "description": "使用目标快照而不是缩略图给生成式 AI 进行描述生成。"
},
"prompt": {
"label": "字幕提示",
- "description": "使用 GenAI 生成描述时使用的默认提示模板。"
+ "description": "使用生成式 AI 生成描述时使用的默认提示模板。"
},
"object_prompts": {
"label": "目标提示",
- "description": "用于自定义特定标签的 GenAI 输出的按目标提示。"
+ "description": "按目标设置提示词,让生成式 AI 对不同标签的输出进行定制。"
},
"objects": {
- "label": "GenAI 目标",
- "description": "默认发送给 GenAI 的目标标签列表。"
+ "label": "生成式 AI 目标",
+ "description": "默认发送给生成式 AI 的目标标签列表。"
},
"required_zones": {
"label": "必需区域",
- "description": "目标必须进入才能符合 GenAI 描述生成条件的区域。"
+ "description": "目标必须进入这些区域,才会触发生成式 AI 描述生成。"
},
"debug_save_thumbnails": {
"label": "保存缩略图",
- "description": "保存发送给 GenAI 的缩略图用于调试和核查。"
+ "description": "保存发送给生成式 AI 的缩略图用于调试和核查。"
},
"send_triggers": {
- "label": "GenAI 触发器",
- "description": "定义何时应将帧发送给 GenAI(结束时、更新后等)。",
+ "label": "生成式 AI 触发器",
+ "description": "定义画面帧应在何时发送给生成式 AI(如检测结束时、更新后等)。",
"tracked_object_end": {
"label": "结束时发送",
- "description": "当追踪目标结束时向 GenAI 发送请求。"
+ "description": "目标追踪结束时向生成式 AI 发送请求。"
},
"after_significant_updates": {
- "label": "早期 GenAI 触发器",
- "description": "在追踪目标进行指定次数的重大更新后向 GenAI 发送请求。"
+ "label": "生成式 AI 提前触发",
+ "description": "在追踪目标发生指定次数的重要变化后,向生成式 AI 发送请求。"
}
},
"enabled_in_config": {
- "label": "原始 GenAI 状态",
- "description": "指示原始静态配置中是否启用了 GenAI。"
+ "label": "原配置生成式 AI 状态",
+ "description": "表示在原始静态配置中是否已启用生成式 AI。"
}
}
},
@@ -516,11 +520,18 @@
"hwaccel_args": {
"label": "导出硬件加速参数",
"description": "用于导出/转码操作的硬件加速参数。"
+ },
+ "max_concurrent": {
+ "label": "最大并发导出数",
+ "description": "同时可处理的最大导出任务数量。"
+ },
+ "chapters": {
+ "label": "导出的录制中嵌入元数据"
}
},
"preview": {
"label": "预览配置",
- "description": "控制 UI 中显示的录像预览质量的设置。",
+ "description": "控制界面中显示的录像预览质量的设置。",
"quality": {
"label": "预览质量",
"description": "预览质量级别(very_low、low、medium、high、very_high)。"
@@ -583,46 +594,46 @@
}
},
"genai": {
- "label": "GenAI 配置",
+ "label": "生成式 AI 配置",
"description": "控制使用生成式 AI 为核查项生成描述和摘要。",
"enabled": {
- "label": "开启 GenAI 描述",
- "description": "为核查项启用或禁用 GenAI 生成的描述和摘要。"
+ "label": "开启生成式 AI 描述",
+ "description": "为核查项开启或关闭使用生成式 AI 生成描述和摘要。"
},
"alerts": {
- "label": "为警报开启 GenAI",
- "description": "使用 GenAI 为警报项生成描述。"
+ "label": "为警报开启生成式 AI",
+ "description": "使用生成式 AI 为警报项生成描述。"
},
"detections": {
- "label": "为检测开启 GenAI",
- "description": "使用 GenAI 为检测项生成描述。"
+ "label": "为检测开启生成式 AI",
+ "description": "使用生成式 AI 为检测项生成描述。"
},
"image_source": {
"label": "核查图像来源",
- "description": "发送给 GenAI 的图像来源('preview' 或 'recordings');'recordings' 使用更高质量的帧但消耗更多 token。"
+ "description": "发送给生成式 AI 的画面来源(“预览” 或 “录制”);“录制”将使用更高质量的画面帧,但会消耗更多的 token。"
},
"additional_concerns": {
"label": "额外关注事项",
- "description": "GenAI 在评估此摄像头活动时应考虑的额外关注事项或备注列表。"
+ "description": "生成式 AI 在分析此摄像头的监控行为时,需要额外注意的事项或说明列表。"
},
"debug_save_thumbnails": {
"label": "保存缩略图",
- "description": "保存发送给 GenAI 提供商的缩略图用于调试和核查。"
+ "description": "保存发送给生成式 AI 提供商的缩略图用于调试和核查。"
},
"enabled_in_config": {
- "label": "原始 GenAI 状态",
- "description": "追踪原始静态配置中是否启用了 GenAI 核查。"
+ "label": "原配置生成式 AI 状态",
+ "description": "记录在静态配置中最初是否已启用生成式 AI 核查功能。"
},
"preferred_language": {
"label": "首选语言",
- "description": "向 GenAI 提供商请求生成响应的首选语言。"
+ "description": "向生成式 AI 提供商请求生成响应的首选语言。"
},
"activity_context_prompt": {
"label": "活动上下文提示",
- "description": "描述什么是和什么不是可疑活动的自定义提示,为 GenAI 摘要提供上下文。"
+ "description": "自定义提示词,用于说明可疑行为与非可疑行为的界定,为生成式 AI 生成摘要提供上下文依据。"
}
},
- "description": "控制此摄像头的警报、检测和 GenAI 核查摘要的设置,用于 UI 和存储。"
+ "description": "控制此摄像头的警报、检测和生成式 AI 核查总结的设置,这些设置会被界面与存储功能使用。"
},
"snapshots": {
"label": "快照",
@@ -710,7 +721,7 @@
"label": "时间戳效果",
"description": "时间戳文本的视觉效果(none、solid、shadow)。"
},
- "description": "应用于录像和快照的实时监控流中时间戳的样式选项。"
+ "description": "快照与调试视图的时间戳样式设置。"
},
"semantic_search": {
"label": "语义搜索",
@@ -719,7 +730,7 @@
"description": "摄像头特定语义搜索触发器的操作和匹配条件。",
"friendly_name": {
"label": "友好名称",
- "description": "在 UI 中为此触发器显示的可选友好名称。"
+ "description": "可选友好名称,用于在界面上为触发器显示此名称。"
},
"enabled": {
"label": "开启此触发器",
@@ -841,15 +852,19 @@
}
},
"ui": {
- "label": "摄像头 UI",
- "description": "此摄像头在 UI 中的显示顺序和可见性。顺序影响默认仪表板。如需更精细的控制,请使用摄像头组。",
+ "label": "摄像头页面",
+ "description": "此摄像头在页面中的显示顺序和可见性。显示顺序仅影响默认仪表板。如需更精细的控制,请使用“摄像头组”。",
"order": {
"label": "UI 顺序",
- "description": "用于在 UI 中排序摄像头的数值顺序(默认仪表板和列表);数值越大出现越晚。"
+ "description": "用于在页面中排序摄像头的顺序(只会影响默认仪表板和列表);数值越大则在越后面。"
},
"dashboard": {
- "label": "在 UI 中显示",
- "description": "切换此摄像头在 Frigate UI 的所有位置是否可见。禁用此项将需要手动编辑配置才能在 UI 中再次查看此摄像头。"
+ "label": "在页面中显示",
+ "description": "切换此摄像头在 Frigate 页面的所有位置是否可见。禁用此项将需要手动编辑配置才能在页面中再次查看此摄像头。"
+ },
+ "review": {
+ "label": "在核查中显示",
+ "description": "切换该摄像头是否在核查页面可见(包含核查页、摄像头筛选栏、画面变动核查与历史视图)。"
}
},
"best_image_timeout": {
@@ -862,14 +877,14 @@
},
"webui_url": {
"label": "摄像头 URL",
- "description": "从系统页面直接访问摄像头的 URL"
+ "description": "从系统页面直接访问摄像头管理后台的 URL"
},
"zones": {
"label": "区域",
"description": "区域允许您定义帧的特定区域,以便确定目标是否在特定区域内。",
"friendly_name": {
"label": "区域名称",
- "description": "区域的友好名称,显示在 Frigate UI 中。如果未设置,将使用区域名称的格式化版本。"
+ "description": "区域的友好名称,显示在 Frigate 页面中。如果未设置,将使用区域名称的格式化版本。"
},
"enabled": {
"label": "开启",
diff --git a/web/public/locales/zh-CN/config/global.json b/web/public/locales/zh-CN/config/global.json
index b14f4acbf1..96f45f9601 100644
--- a/web/public/locales/zh-CN/config/global.json
+++ b/web/public/locales/zh-CN/config/global.json
@@ -24,7 +24,7 @@
}
},
"audio": {
- "label": "音频事件",
+ "label": "音频检测",
"enabled": {
"label": "开启音频检测",
"description": "为所有摄像头启用或禁用音频事件检测;可按摄像头覆盖。"
@@ -44,11 +44,15 @@
},
"listen": {
"label": "监听类型",
- "description": "要检测的音频事件类型列表(例如:bark、fire_alarm、scream、speech、yell)。"
+ "description": "要检测的音频事件类型列表(例如:bark、fire_alarm、speech、yell)。"
},
"filters": {
"label": "音频过滤器",
- "description": "按音频类型的过滤器设置,如用于减少误报的置信度阈值。"
+ "description": "按音频类型的过滤器设置,如用于减少误报的置信度阈值。",
+ "threshold": {
+ "label": "最低音频置信度",
+ "description": "设置音频事件所需的最低置信度阈值。"
+ }
},
"enabled_in_config": {
"label": "原始音频状态",
@@ -136,7 +140,7 @@
},
"mode": {
"label": "追踪模式",
- "description": "在鸟瞰视图中包含摄像头的模式:'objects'(目标)、'motion'(动作)或 'continuous'(持续)。"
+ "description": "在鸟瞰视图中包含摄像头的模式有:“基于目标”、“基于画面变动”或“连续”。"
},
"order": {
"label": "排序位置",
@@ -231,7 +235,7 @@
},
"classifier": {
"label": "开启视觉分类器",
- "description": "使用视觉分类器,即使检测框有轻微抖动,也能准确判断物体是真的静止。"
+ "description": "使用视觉分类器,即使检测框有轻微抖动,也能准确判断物体是否为静止。"
}
},
"annotation_offset": {
@@ -252,7 +256,7 @@
"description": "所有摄像头的人脸检测和识别设置;可按摄像头覆盖。",
"model_size": {
"label": "模型大小",
- "description": "用于人脸嵌入的模型大小(small/large);较大的可能需要 GPU。"
+ "description": "用于人脸嵌入的模型大小(小型/大型);较大的可能需要 GPU。"
},
"unknown_score": {
"label": "未知分数阈值",
@@ -288,7 +292,7 @@
"description": "FFmpeg 编解码相关设置,包含可执行文件路径、命令行参数、硬件加速选项,以及按不同功能划分的输出参数。",
"path": {
"label": "FFmpeg 路径",
- "description": "要使用的 FFmpeg 可执行文件路径,或版本别名(如 \"5.0\" 或 \"7.0\")。"
+ "description": "要使用的 FFmpeg 可执行文件路径,或版本别名(如 \"7.0\" 或 \"8.0\")。"
},
"global_args": {
"label": "FFmpeg 全局参数",
@@ -492,7 +496,7 @@
},
"default_role": {
"label": "默认权限组",
- "description": "当没有权限组映射适用时分配给代理认证用户的默认权限组(admin 或 viewer)。"
+ "description": "当没有权限组映射适用时分配给代理认证用户的默认权限组。"
},
"separator": {
"label": "分隔符",
@@ -522,8 +526,8 @@
"description": "为摄像头 ffmpeg 进程和检测器启用按进程网络带宽监控(需要权限)。"
},
"intel_gpu_device": {
- "label": "SR-IOV 设备",
- "description": "将 Intel GPU 视为 SR-IOV 时使用的设备标识符,用于修复 GPU 统计信息。"
+ "label": "Intel GPU 设备",
+ "description": "当系统存在多个 Intel 显卡时,用于将显卡运行数据绑定到指定设备的 PCI 总线地址或 DRM 设备路径(示例:/dev/dri/card1)。"
}
},
"version_check": {
@@ -536,7 +540,7 @@
"description": "Frigate Web 端点(端口 8971)的 TLS 设置。",
"enabled": {
"label": "开启 TLS",
- "description": "为 Frigate 的 Web 页面和 API 的端口开启 TLS 加密。"
+ "description": "为 Frigate 的网页页面和 API 的端口开启 TLS 加密。"
}
},
"ui": {
@@ -544,19 +548,19 @@
"description": "用户界面偏好设置,如时区、时间/日期格式和单位。",
"timezone": {
"label": "时区",
- "description": "UI 中显示的可选时区(如果未设置,则默认为浏览器本地时间)。"
+ "description": "可选时区,用于整个界面展示时间(如果未设置,则默认为浏览器本地时间的时区)。"
},
"time_format": {
"label": "时间格式",
- "description": "UI 中使用的时间格式(browser、12hour 或 24hour)。"
+ "description": "页面中将使用的时间格式(浏览器、12小时制 或 24小时制)。"
},
"date_style": {
"label": "日期样式",
- "description": "UI 中使用的日期样式(full、long、medium、short)。"
+ "description": "页面中将使用的日期样式(完整、长、中等、短)。"
},
"time_style": {
"label": "时间样式",
- "description": "UI 中使用的时间样式(full、long、medium、short)。"
+ "description": "页面中将使用的时间样式(完整、长、中等、短)。"
},
"unit_system": {
"label": "单位系统",
@@ -1470,15 +1474,15 @@
},
"provider": {
"label": "提供商",
- "description": "要使用的 GenAI 提供商(例如:ollama、gemini、openai)。"
+ "description": "要使用的生成式 AI 提供商(例如:ollama、gemini、openai 等。国产大模型厂商可使用 openai 接口)。"
},
"roles": {
"label": "功能",
- "description": "生成式 AI 功能(工具、视觉、嵌入);每个功能单独一个提供商。"
+ "description": "生成式 AI 功能(对话、描述、嵌入);每个功能单独一个提供商。"
},
"provider_options": {
"label": "提供商选项",
- "description": "传递给 GenAI 客户端的附加提供商特定选项。"
+ "description": "要传递给生成式 AI 客户端的、与服务提供商相关的额外配置项。"
},
"runtime_options": {
"label": "运行时选项",
@@ -1486,7 +1490,7 @@
}
},
"live": {
- "label": "实时回放",
+ "label": "实时监控观看",
"description": "用于控制 JSMPEG 实时流分辨率与画质的设置。此设置不影响使用 go2rtc 进行实时预览的摄像头。",
"streams": {
"label": "实时监控流名称",
@@ -1494,7 +1498,7 @@
},
"height": {
"label": "实时监控高度",
- "description": "在 Web UI 中渲染 jsmpeg 实时监控流的高度(像素);必须小于等于检测流高度。"
+ "description": "在网页页面中渲染 jsmpeg 实时监控流的高度(像素);必须小于等于检测流高度。"
},
"quality": {
"label": "实时监控质量",
@@ -1606,51 +1610,86 @@
"label": "原始遮罩"
},
"genai": {
- "label": "GenAI 目标配置",
- "description": "用于描述追踪目标和发送帧进行生成的 GenAI 选项。",
+ "label": "生成式 AI 目标配置",
+ "description": "用于发送画面给生成式 AI 进行生成和描述追踪目标的选项。",
"enabled": {
- "label": "开启 GenAI",
- "description": "默认启用 GenAI 生成追踪目标的描述。"
+ "label": "开启生成式 AI",
+ "description": "默认开启生成式 AI 生成追踪目标的描述。"
},
"use_snapshot": {
"label": "使用快照",
- "description": "使用目标快照而不是缩略图进行 GenAI 描述生成。"
+ "description": "使用目标快照而不是缩略图给生成式 AI 进行描述生成。"
},
"prompt": {
"label": "字幕提示",
- "description": "使用 GenAI 生成描述时使用的默认提示模板。"
+ "description": "使用生成式 AI 生成描述时使用的默认提示模板。"
},
"object_prompts": {
"label": "目标提示",
- "description": "用于自定义特定标签的 GenAI 输出的按目标提示。"
+ "description": "按目标设置提示词,让生成式 AI 对不同标签的输出进行定制。"
},
"objects": {
- "label": "GenAI 目标",
- "description": "默认发送给 GenAI 的目标标签列表。"
+ "label": "生成式 AI 目标",
+ "description": "默认发送给生成式 AI 的目标标签列表。"
},
"required_zones": {
"label": "必需区域",
- "description": "目标必须进入才能符合 GenAI 描述生成条件的区域。"
+ "description": "目标必须进入这些区域,才会触发生成式 AI 描述生成。"
},
"debug_save_thumbnails": {
"label": "保存缩略图",
- "description": "保存发送给 GenAI 的缩略图用于调试和核查。"
+ "description": "保存发送给生成式 AI 的缩略图用于调试和核查。"
},
"send_triggers": {
- "label": "GenAI 触发器",
- "description": "定义何时应将帧发送给 GenAI(结束时、更新后等)。",
+ "label": "生成式 AI 触发器",
+ "description": "定义画面帧应在何时发送给生成式 AI(如检测结束时、更新后等)。",
"tracked_object_end": {
"label": "结束时发送",
- "description": "当追踪目标结束时向 GenAI 发送请求。"
+ "description": "目标追踪结束时向生成式 AI 发送请求。"
},
"after_significant_updates": {
- "label": "早期 GenAI 触发器",
- "description": "在追踪目标进行指定次数的重大更新后向 GenAI 发送请求。"
+ "label": "生成式 AI 提前触发",
+ "description": "在追踪目标发生指定次数的重要变化后,向生成式 AI 发送请求。"
}
},
"enabled_in_config": {
- "label": "原始 GenAI 状态",
- "description": "指示原始静态配置中是否启用了 GenAI。"
+ "label": "原配置生成式 AI 状态",
+ "description": "表示在原始静态配置中是否已启用生成式 AI。"
+ }
+ },
+ "filters_attribute": {
+ "label": "属性筛选",
+ "description": "对检测到的属性进行筛选,以此减少误报(包含面积、比例、置信度)。",
+ "min_area": {
+ "label": "最小属性区域",
+ "description": "该属性所需的最小边框框面积,支持像素值(整数)或百分比数值(范围 0.000001 至 0.99)。"
+ },
+ "max_area": {
+ "label": "最大属性区域",
+ "description": "该属性所需的最大边框框面积,支持像素值(整数)或百分比数值(范围 0.000001 至 0.99)。"
+ },
+ "min_ratio": {
+ "label": "最小纵横比",
+ "description": "边界框所需的最小宽高比。"
+ },
+ "max_ratio": {
+ "label": "最大纵横比",
+ "description": "边界框允许的最大宽高比。"
+ },
+ "threshold": {
+ "label": "置信度阈值",
+ "description": "判定该属性为有效目标所需的平均检测置信度阈值。"
+ },
+ "min_score": {
+ "label": "最小置信度",
+ "description": "将该属性关联至所属父目标所需的单帧最低检测置信度。"
+ },
+ "mask": {
+ "label": "过滤器遮罩",
+ "description": "定义此过滤器在帧内应用位置的多边形坐标。"
+ },
+ "raw_mask": {
+ "label": "原始遮罩"
}
}
},
@@ -1735,11 +1774,18 @@
"hwaccel_args": {
"label": "导出硬件加速参数",
"description": "用于导出/转码操作的硬件加速参数。"
+ },
+ "max_concurrent": {
+ "label": "最大并发导出数",
+ "description": "同时可处理的最大导出任务数量。"
+ },
+ "chapters": {
+ "label": "导出的录制中嵌入元数据"
}
},
"preview": {
"label": "预览配置",
- "description": "控制 UI 中显示的录像预览质量的设置。",
+ "description": "控制界面中显示的录像预览质量的设置。",
"quality": {
"label": "预览质量",
"description": "预览质量级别(very_low、low、medium、high、very_high)。"
@@ -1752,7 +1798,7 @@
},
"review": {
"label": "核查",
- "description": "控制 UI 和存储使用的警报、检测和 GenAI 核查摘要的设置。",
+ "description": "控制界面与存储所使用的警报、检测和生成式 AI 核查总结的相关设置。",
"alerts": {
"label": "警报配置",
"description": "哪些追踪目标生成警报以及如何保留警报的设置。",
@@ -1802,43 +1848,43 @@
}
},
"genai": {
- "label": "GenAI 配置",
+ "label": "生成式 AI 配置",
"description": "控制使用生成式 AI 为核查项生成描述和摘要。",
"enabled": {
- "label": "开启 GenAI 描述",
- "description": "为核查项启用或禁用 GenAI 生成的描述和摘要。"
+ "label": "开启生成式 AI 描述",
+ "description": "为核查项开启或关闭使用生成式 AI 生成描述和摘要。"
},
"alerts": {
- "label": "为警报开启 GenAI",
- "description": "使用 GenAI 为警报项生成描述。"
+ "label": "为警报开启生成式 AI",
+ "description": "使用生成式 AI 为警报项生成描述。"
},
"detections": {
- "label": "为检测开启 GenAI",
- "description": "使用 GenAI 为检测项生成描述。"
+ "label": "为检测开启生成式 AI",
+ "description": "使用生成式 AI 为检测项生成描述。"
},
"image_source": {
"label": "核查图像来源",
- "description": "发送给 GenAI 的图像来源('preview' 或 'recordings');'recordings' 使用更高质量的帧但消耗更多 token。"
+ "description": "发送给生成式 AI 的画面来源(“预览” 或 “录制”);“录制”将使用更高质量的画面帧,但会消耗更多的 token。"
},
"additional_concerns": {
"label": "额外关注事项",
- "description": "GenAI 在评估此摄像头活动时应考虑的额外关注事项或备注列表。"
+ "description": "生成式 AI 在分析此摄像头的监控行为时,需要额外注意的事项或说明列表。"
},
"debug_save_thumbnails": {
"label": "保存缩略图",
- "description": "保存发送给 GenAI 提供商的缩略图用于调试和核查。"
+ "description": "保存发送给生成式 AI 提供商的缩略图用于调试和核查。"
},
"enabled_in_config": {
- "label": "原始 GenAI 状态",
- "description": "追踪原始静态配置中是否启用了 GenAI 核查。"
+ "label": "原配置生成式 AI 状态",
+ "description": "记录在静态配置中最初是否已启用生成式 AI 核查功能。"
},
"preferred_language": {
"label": "首选语言",
- "description": "向 GenAI 提供商请求生成响应的首选语言。"
+ "description": "向生成式 AI 提供商请求生成响应的首选语言。"
},
"activity_context_prompt": {
"label": "活动上下文提示",
- "description": "描述什么是和什么不是可疑活动的自定义提示,为 GenAI 摘要提供上下文。"
+ "description": "自定义提示词,用于说明可疑行为与非可疑行为的界定,为生成式 AI 生成摘要提供上下文依据。"
}
}
},
@@ -2011,7 +2057,7 @@
},
"model_size": {
"label": "模型大小",
- "description": "选择模型大小;'small' 在 CPU 上运行,'large' 通常需要 GPU。"
+ "description": "选择模型大小;“小型”模型一般在 CPU 上运行,而“大型”模型通常需要 GPU。"
},
"device": {
"label": "设备",
@@ -2022,7 +2068,7 @@
"description": "摄像头特定语义搜索触发器的操作和匹配条件。",
"friendly_name": {
"label": "友好名称",
- "description": "在 UI 中为此触发器显示的可选友好名称。"
+ "description": "可选友好名称,用于在界面上为触发器显示此名称。"
},
"enabled": {
"label": "开启此触发器",
@@ -2055,7 +2101,7 @@
},
"model_size": {
"label": "模型大小",
- "description": "用于文本检测/识别的模型大小,大多数用户应使用 'small',只有'small'模型支持中文。"
+ "description": "用于文本检测/识别的模型大小,大多数用户应使用“小型”模型,而且只有“小型”模型支持中文车牌。"
},
"detection_threshold": {
"label": "检测阈值",
@@ -2114,18 +2160,18 @@
},
"camera_groups": {
"label": "摄像头分组",
- "description": "用于在 UI 中组织摄像头的命名摄像头分组配置。",
+ "description": "用于在页面中组织摄像头的命名摄像头分组配置。",
"cameras": {
"label": "摄像头列表",
"description": "此分组中包含的摄像头名称数组。"
},
"icon": {
"label": "分组图标",
- "description": "在 UI 中代表摄像头分组的图标。"
+ "description": "在页面中代表摄像头分组的图标。"
},
"order": {
"label": "排序顺序",
- "description": "用于在 UI 中对摄像头分组进行排序的数字顺序;数值越大越靠后。"
+ "description": "用于在页面中对摄像头分组进行排序的数字顺序;数值越大越靠后。"
}
},
"camera_mqtt": {
@@ -2161,15 +2207,19 @@
}
},
"camera_ui": {
- "label": "摄像头 UI",
- "description": "此摄像头在 UI 中的显示顺序和可见性。顺序影响默认仪表板。如需更精细的控制,请使用摄像头分组。",
+ "label": "摄像头页面",
+ "description": "此摄像头在页面中的显示顺序和可见性。显示顺序仅影响默认仪表板。如需更精细的控制,请使用“摄像头组”。",
"order": {
"label": "UI 顺序",
- "description": "用于在 UI 中对摄像头进行排序的数字顺序(默认仪表板和列表);数值越大越靠后。"
+ "description": "用于在页面中排序摄像头的顺序(只会影响默认仪表板和列表);数值越大则在越后面。"
},
"dashboard": {
- "label": "在 UI 中显示",
- "description": "切换此摄像头在 Frigate UI 中是否可见。禁用后需要手动编辑配置才能再次在 UI 中查看此摄像头。"
+ "label": "在页面中显示",
+ "description": "切换此摄像头在 Frigate 页面中是否可见。禁用后需要手动编辑配置才能再次在页面中查看此摄像头。"
+ },
+ "review": {
+ "label": "在核查中显示",
+ "description": "切换该摄像头是否在核查页面可见(包含核查页、摄像头筛选栏、画面变动核查与历史视图)。"
}
},
"onvif": {
diff --git a/web/public/locales/zh-CN/config/validation.json b/web/public/locales/zh-CN/config/validation.json
index a926f2cce8..ba275a8cd9 100644
--- a/web/public/locales/zh-CN/config/validation.json
+++ b/web/public/locales/zh-CN/config/validation.json
@@ -28,5 +28,8 @@
"header_map": {
"roleHeaderRequired": "配置权限组映射时需要的 role 请求头。"
}
+ },
+ "detect": {
+ "dimensionMustBeEven": "必须是偶数。"
}
}
diff --git a/web/public/locales/zh-CN/objects.json b/web/public/locales/zh-CN/objects.json
index f8d07bc23b..59058f332d 100644
--- a/web/public/locales/zh-CN/objects.json
+++ b/web/public/locales/zh-CN/objects.json
@@ -121,5 +121,10 @@
"royal_mail": "英国皇家邮政",
"school_bus": "校车",
"skunk": "臭鼬",
- "kangaroo": "袋鼠"
+ "kangaroo": "袋鼠",
+ "baby": "婴儿",
+ "baby_stroller": "婴儿车",
+ "rickshaw": "三轮车",
+ "Rodent": "啮齿动物",
+ "rodent": "鼠类动物"
}
diff --git a/web/public/locales/zh-CN/views/chat.json b/web/public/locales/zh-CN/views/chat.json
new file mode 100644
index 0000000000..dee13463e5
--- /dev/null
+++ b/web/public/locales/zh-CN/views/chat.json
@@ -0,0 +1,72 @@
+{
+ "documentTitle": "聊天 - Frigate",
+ "title": "Frigate 聊天",
+ "subtitle": "你的摄像头管理与智能分析 AI 助手",
+ "placeholder": "尝试问我任何事儿…",
+ "error": "出现错误,请稍后重试。",
+ "processing": "进行中…",
+ "toolsUsed": "使用:{{tools}}",
+ "showTools": "显示工具({{count}})",
+ "hideTools": "隐藏工具",
+ "call": "调用",
+ "result": "结果",
+ "arguments": "参数:",
+ "response": "响应:",
+ "attachment_chip_label": "在 {{camera}} 的 {{label}}",
+ "attachment_chip_remove": "移除附件",
+ "open_in_explore": "从浏览中打开",
+ "attach_event_aria": "关联事件 {{eventId}}",
+ "attachment_picker_paste_label": "或粘贴事件 ID",
+ "attachment_picker_attach": "关联",
+ "attachment_picker_placeholder": "关联一个事件",
+ "quick_reply_find_similar": "查找相似抓拍事件",
+ "quick_reply_tell_me_more": "了解更多详情",
+ "quick_reply_when_else": "还在哪些时段出现过?",
+ "quick_reply_find_similar_text": "查找与此相似的抓拍记录。",
+ "quick_reply_tell_me_more_text": "了解此条更多详情。",
+ "starting_requests": {
+ "show_recent_events": "查看近期事件",
+ "show_camera_status": "显示摄像头状态",
+ "recap": "我不在的时候发生了什么?",
+ "watch_camera": "监控摄像头活动"
+ },
+ "quick_reply_when_else_text": "还在哪些时间出现过?",
+ "anchor": "来源",
+ "similarity_score": "相似度",
+ "no_similar_objects_found": "未找到相似目标。",
+ "semantic_search_required": "必须启用语义搜索才能查找相似目标。",
+ "send": "发送",
+ "suggested_requests": "尝试问问:",
+ "starting_requests_prompts": {
+ "show_recent_events": "显示最近一小时的事件",
+ "show_camera_status": "我的摄像头当前状态如何?",
+ "recap": "我不在的时候发生了什么事?",
+ "watch_camera": "监控前门,有人出现就通知我"
+ },
+ "new_chat": "新对话",
+ "settings": {
+ "title": "对话设置",
+ "show_stats": {
+ "title": "显示统计数据",
+ "desc": "显示对话回复的生成速率和上下文大小。",
+ "while_generating": "生成过程中",
+ "always": "始终"
+ },
+ "auto_scroll": {
+ "title": "自动滚动",
+ "desc": "自动滚动到最新消息。"
+ }
+ },
+ "stats": {
+ "context": "{{tokens}} 词元(tokens)",
+ "tokens_per_second": "{{rate}} 词元/秒"
+ },
+ "reasoning": {
+ "active": "思考中…",
+ "show": "显示推理过程",
+ "hide": "隐藏推理过程"
+ },
+ "thinking": {
+ "toggle": "切换思考"
+ }
+}
diff --git a/web/public/locales/zh-CN/views/events.json b/web/public/locales/zh-CN/views/events.json
index f02a839076..6035051c8f 100644
--- a/web/public/locales/zh-CN/views/events.json
+++ b/web/public/locales/zh-CN/views/events.json
@@ -26,7 +26,9 @@
},
"documentTitle": "核查 - Frigate",
"recordings": {
- "documentTitle": "回放 - Frigate"
+ "documentTitle": "回放 - Frigate",
+ "invalidSharedLink": "由于解析错误,无法打开带时间戳的录制链接。",
+ "invalidSharedCamera": "由于摄像头未知或未获授权,无法打开带时间戳的录制链接。"
},
"calendarFilter": {
"last24Hours": "过去24小时"
diff --git a/web/public/locales/zh-CN/views/explore.json b/web/public/locales/zh-CN/views/explore.json
index db062d4556..b5860d18fb 100644
--- a/web/public/locales/zh-CN/views/explore.json
+++ b/web/public/locales/zh-CN/views/explore.json
@@ -285,7 +285,10 @@
"zones": "区",
"ratio": "比例",
"area": "大小",
- "score": "分数"
+ "score": "分数",
+ "computedScore": "计算得分",
+ "topScore": "最高得分",
+ "toggleAdvancedScores": "切换高级分数"
}
},
"annotationSettings": {
diff --git a/web/public/locales/zh-CN/views/exports.json b/web/public/locales/zh-CN/views/exports.json
index b57b1a1c69..6eaed055cf 100644
--- a/web/public/locales/zh-CN/views/exports.json
+++ b/web/public/locales/zh-CN/views/exports.json
@@ -14,7 +14,9 @@
"toast": {
"error": {
"renameExportFailed": "重命名导出失败:{{errorMessage}}",
- "assignCaseFailed": "更新合集分配失败:{{errorMessage}}"
+ "assignCaseFailed": "更新合集分配失败:{{errorMessage}}",
+ "caseSaveFailed": "保存合集失败:{{errorMessage}}",
+ "caseDeleteFailed": "删除合集失败:{{errorMessage}}"
}
},
"tooltip": {
@@ -22,7 +24,8 @@
"downloadVideo": "下载视频",
"editName": "编辑名称",
"deleteExport": "删除导出",
- "assignToCase": "加入合集"
+ "assignToCase": "加入合集",
+ "removeFromCase": "从合集中移除"
},
"headings": {
"uncategorizedExports": "未分类导出项",
@@ -35,5 +38,91 @@
"selectLabel": "合集",
"newCaseOption": "创建新合集",
"descriptionLabel": "描述"
+ },
+ "toolbar": {
+ "newCase": "新合集",
+ "addExport": "新导出",
+ "editCase": "编辑合集",
+ "deleteCase": "删除合集"
+ },
+ "deleteCase": {
+ "label": "删除合集",
+ "desc": "你确定要删除 {{caseName}} 吗?",
+ "descKeepExports": "导出文件将继续保留为未分类导出。",
+ "descDeleteExports": "此合集中的所有导出项都将被永久删除。",
+ "deleteExports": "同时删除导出文件"
+ },
+ "caseCard": {
+ "emptyCase": "暂无导出文件"
+ },
+ "jobCard": {
+ "defaultName": "{{camera}} 导出",
+ "queued": "队列中",
+ "running": "运行中",
+ "preparing": "准备中",
+ "copying": "复制中",
+ "encoding": "编码中",
+ "encodingRetry": "重试编码中",
+ "finalizing": "正在完成"
+ },
+ "caseView": {
+ "noDescription": "没有描述",
+ "createdAt": "已创建 {{value}}",
+ "exportCount_one": "1 个导出",
+ "exportCount_other": "{{count}} 个导出",
+ "cameraCount_one": "1 个摄像头",
+ "cameraCount_other": "{{count}} 个摄像头",
+ "showMore": "显示更多",
+ "showLess": "显示更少",
+ "emptyTitle": "该合集为空",
+ "emptyDescription": "将现有未分类的导出添加进来,以便整理该条目。",
+ "emptyDescriptionNoExports": "目前没有可添加的未分类导出项。"
+ },
+ "caseEditor": {
+ "createTitle": "创建合集",
+ "editTitle": "编辑合集",
+ "namePlaceholder": "合集名称",
+ "descriptionPlaceholder": "为该合集添加备注或相关说明"
+ },
+ "addExportDialog": {
+ "title": "将导出添加到 {{caseName}}",
+ "searchPlaceholder": "搜索未分类的导出项",
+ "empty": "未找到匹配的未分类导出。",
+ "addButton_one": "添加 1 个导出",
+ "addButton_other": "添加 {{count}} 个导出",
+ "adding": "添加中…"
+ },
+ "selected_one": "已选择 {{count}} 个",
+ "selected_other": "已选择 {{count}} 个",
+ "bulkActions": {
+ "addToCase": "添加至合集",
+ "moveToCase": "移动至合集",
+ "removeFromCase": "从合集中移除",
+ "delete": "删除",
+ "deleteNow": "立即删除"
+ },
+ "bulkDelete": {
+ "title": "删除导出",
+ "desc_one": "你确定要删除 {{count}} 个导出吗?",
+ "desc_other": "确定要删除 {{count}} 个导出吗?"
+ },
+ "bulkRemoveFromCase": {
+ "title": "从合集中移除",
+ "desc_one": "你确定要从该合集中移除这 {{count}} 个导出吗?",
+ "desc_other": "你确定要从该合集中移除这 {{count}} 个导出吗?",
+ "descKeepExports": "导出将被移至未分类。",
+ "descDeleteExports": "导出将被永久删除。",
+ "deleteExports": "选择删除导出"
+ },
+ "bulkToast": {
+ "success": {
+ "delete": "已删除导出",
+ "reassign": "已更新合集分配",
+ "remove": "已从合集中移除导出"
+ },
+ "error": {
+ "deleteFailed": "删除导出失败:{{errorMessage}}",
+ "reassignFailed": "更新合集分配失败:{{errorMessage}}"
+ }
}
}
diff --git a/web/public/locales/zh-CN/views/faceLibrary.json b/web/public/locales/zh-CN/views/faceLibrary.json
index d383fb348a..59aedc9f1d 100644
--- a/web/public/locales/zh-CN/views/faceLibrary.json
+++ b/web/public/locales/zh-CN/views/faceLibrary.json
@@ -30,7 +30,11 @@
"title": "近期识别记录",
"aria": "选择近期识别记录",
"empty": "近期未检测到人脸识别操作",
- "titleShort": "近期"
+ "titleShort": "近期",
+ "emptyNoLibrary": {
+ "title": "更新人脸",
+ "description": "你必须向库中添加至少一张人脸,人脸识别功能才能正常工作。"
+ }
},
"selectItem": "选择 {{item}}",
"selectFace": "选择人脸",
diff --git a/web/public/locales/zh-CN/views/live.json b/web/public/locales/zh-CN/views/live.json
index 10b8641d3f..de900ba8bd 100644
--- a/web/public/locales/zh-CN/views/live.json
+++ b/web/public/locales/zh-CN/views/live.json
@@ -58,7 +58,9 @@
},
"camera": {
"enable": "开启摄像头",
- "disable": "关闭摄像头"
+ "disable": "关闭摄像头",
+ "turnOn": "开启摄像头",
+ "turnOff": "关闭摄像头"
},
"muteCameras": {
"enable": "屏蔽所有摄像头",
@@ -70,7 +72,8 @@
},
"recording": {
"enable": "开启录制",
- "disable": "关闭录制"
+ "disable": "关闭录制",
+ "disabledInConfig": "必须先在该摄像头的设置中开启录制功能。"
},
"snapshots": {
"enable": "开启快照",
@@ -150,7 +153,8 @@
"snapshots": "快照",
"audioDetection": "音频检测",
"autotracking": "自动追踪",
- "transcription": "音频转录"
+ "transcription": "音频转录",
+ "camera": "摄像头"
},
"history": {
"label": "显示历史录像"
diff --git a/web/public/locales/zh-CN/views/motionSearch.json b/web/public/locales/zh-CN/views/motionSearch.json
new file mode 100644
index 0000000000..11dafbeea8
--- /dev/null
+++ b/web/public/locales/zh-CN/views/motionSearch.json
@@ -0,0 +1,78 @@
+{
+ "documentTitle": "变动搜索 - Frigate",
+ "title": "画面变动搜索",
+ "description": "绘制一个多边形以划定感兴趣区域,并指定时间范围,检索该区域内的异动变化。",
+ "selectCamera": "画面变动搜索正在加载中",
+ "startSearch": "开始搜索",
+ "searchStarted": "搜索已开始",
+ "searchCancelled": "搜索已取消",
+ "cancelSearch": "取消",
+ "searching": "搜索进行中。",
+ "searchComplete": "搜索完成",
+ "noResultsYet": "在所选区域内执行搜索,查找异常变化",
+ "noChangesFound": "所选区域未检测到像素变化",
+ "changesFound_other": "检测到 {{count}} 处画面变化",
+ "framesProcessed": "已处理 {{count}} 帧画面",
+ "jumpToTime": "跳转到该时间",
+ "results": "结果",
+ "showSegmentHeatmap": "热力图",
+ "newSearch": "新的搜索",
+ "clearResults": "清除结果",
+ "clearROI": "清除多边形选区",
+ "polygonControls": {
+ "points_other": "{{count}} 个点位",
+ "undo": "撤销上一个点位",
+ "reset": "重置多边形",
+ "drawMode": "绘制",
+ "moveMode": "移动"
+ },
+ "motionHeatmapLabel": "画面变动热力图",
+ "dialog": {
+ "title": "画面变动搜索",
+ "cameraLabel": "摄像头",
+ "previewAlt": "{{camera}} 摄像头实时预览"
+ },
+ "timeRange": {
+ "title": "搜索范围",
+ "start": "开始时间",
+ "end": "结束时间"
+ },
+ "settings": {
+ "title": "搜索设置",
+ "parallelMode": "并行模式",
+ "parallelModeDesc": "同时扫描多个录制片段(速度更快,将使用更多解码资源)",
+ "threshold": "灵敏度阈值",
+ "thresholdDesc": "数值越低,可检测到越小的变化(取值范围 1-255)",
+ "minArea": "最小变化区域",
+ "minAreaDesc": "单个移动区域的最小尺寸,占目标区域的百分比",
+ "frameSkip": "帧跳过",
+ "frameSkipDesc": "每隔 N 帧进行一次处理。将该值设置为摄像头的帧率,即可实现每秒处理一帧画面(例如:5 帧 / 秒的摄像头设为 5,30 帧 / 秒的摄像头设为 30)。数值越高处理速度越快,但有可能遗漏短时移动侦测事件。",
+ "maxResults": "最大结果数",
+ "maxResultsDesc": "匹配到设定条数的录像事件后,就自动停止检索"
+ },
+ "errors": {
+ "noCamera": "请选择摄像头",
+ "noROI": "请绘制感兴趣的区域",
+ "noTimeRange": "请选择时间范围",
+ "invalidTimeRange": "结束时间必须在开始时间之后",
+ "searchFailed": "搜索失败:{{message}}",
+ "polygonTooSmall": "多边形至少需要 3 个顶点",
+ "unknown": "未知错误"
+ },
+ "changePercentage": "{{percentage}}% 已变化",
+ "metrics": {
+ "title": "搜索指标",
+ "segmentsScanned": "已扫描片段数",
+ "segmentsProcessed": "已处理",
+ "segmentsSkippedInactive": "已跳过(无活动)",
+ "segmentsSkippedHeatmap": "已跳过(不在感兴趣区域)",
+ "fallbackFullRange": "备用全范围扫描",
+ "framesDecoded": "画面已解码",
+ "wallTime": "搜索时间",
+ "segmentErrors": "片段异常",
+ "seconds": "{{seconds}} 秒",
+ "minutesSeconds": "{{minutes}}分 {{seconds}}秒",
+ "scanSummary": "{{segments}} 分段 · {{time}}"
+ },
+ "scanning": "扫描中 {{time}}"
+}
diff --git a/web/public/locales/zh-CN/views/replay.json b/web/public/locales/zh-CN/views/replay.json
new file mode 100644
index 0000000000..00634af5d7
--- /dev/null
+++ b/web/public/locales/zh-CN/views/replay.json
@@ -0,0 +1,59 @@
+{
+ "title": "调试回放",
+ "description": "回放摄像头录像以供调试。目标列表会延时展示已检测目标的汇总信息,消息标签页则实时展示回放录像对应的 Frigate 内部日志信息流。",
+ "websocket_messages": "消息",
+ "dialog": {
+ "title": "开始调试回放",
+ "description": "创建临时回放摄像头,循环播放历史录制视频,用于调试目标检测与追踪相关问题。临时回放的摄像头将沿用原摄像头的检测配置。请选择一个时间范围开始。",
+ "camera": "原摄像头",
+ "timeRange": "时间范围",
+ "preset": {
+ "1m": "最后 1 分钟",
+ "5m": "最后 5 分钟",
+ "timeline": "从时间线",
+ "custom": "自定义"
+ },
+ "startButton": "开始回放",
+ "selectFromTimeline": "选择",
+ "starting": "开始回放…",
+ "startLabel": "开始",
+ "endLabel": "结束",
+ "toast": {
+ "error": "调试回放启动失败:{{error}}",
+ "alreadyActive": "已有回放会话正在运行",
+ "stopError": "调试回放停止失败:{{error}}",
+ "goToReplay": "进入回放"
+ }
+ },
+ "page": {
+ "noSession": "没有正在进行的调试回放会话",
+ "noSessionDesc": "从历史回放页面启动调试回放:点击工具栏中的操作按钮,选择调试回放即可。",
+ "goToRecordings": "查看历史记录",
+ "preparingClip": "正在准备片段…",
+ "preparingClipDesc": "Frigate 正在拼接所选时间范围的录像片段。时间跨度较大时,该过程可能需要一分钟左右。",
+ "startingCamera": "开始调试回放中…",
+ "startError": {
+ "title": "调试回放启动失败",
+ "back": "返回历史记录"
+ },
+ "sourceCamera": "源摄像头",
+ "replayCamera": "回放摄像头",
+ "initializingReplay": "初始化调试回放中…",
+ "stoppingReplay": "正在停止调试回放…",
+ "stopReplay": "停止回放",
+ "confirmStop": {
+ "title": "要停止调试回放吗?",
+ "description": "这将终止会话并清除所有临时数据。是否确定?",
+ "confirm": "停止回放",
+ "cancel": "取消"
+ },
+ "activity": "活动",
+ "objects": "目标列表",
+ "audioDetections": "音频检测",
+ "noActivity": "未检测到活动",
+ "activeTracking": "活动追踪中",
+ "noActiveTracking": "没有活动追踪",
+ "configuration": "配置",
+ "configurationDesc": "微调调试回放摄像头的移动侦测与目标追踪参数。本次调整不会保存到你的 Frigate 配置文件中。"
+ }
+}
diff --git a/web/public/locales/zh-CN/views/search.json b/web/public/locales/zh-CN/views/search.json
index 51fe47c8ef..ffb770839c 100644
--- a/web/public/locales/zh-CN/views/search.json
+++ b/web/public/locales/zh-CN/views/search.json
@@ -46,7 +46,7 @@
"tips": {
"title": "如何使用文本筛选器",
"desc": {
- "text": "筛选器可帮助您缩小搜索范围。注意,目前还暂不支持中文搜索。以下是在输入字段中使用筛选器的方法:",
+ "text": "筛选器可帮助您缩小搜索范围。注意,Jina v1 不支持中文搜索。以下是在输入字段中使用筛选器的方法:",
"step": "输入筛选器名称后跟一个冒号(例如:“cameras:”)。 从建议中选择一个值或输入您自己的值。 使用多个筛选器时,可以在它们之间用空格分隔。 日期筛选器(before: 和 after:)使用 {{DateFormat}} 格式。 时间范围筛选器使用 {{exampleTime}} 格式。 点击筛选器旁边的“x”即可移除筛选条件。 ",
"example": "示例:cameras:front_door label:person before:01012024 time_range:3:00PM-4:00PM",
"step2": "选择给出的建议值或自行输入;",
diff --git a/web/public/locales/zh-CN/views/settings.json b/web/public/locales/zh-CN/views/settings.json
index 55190e53bd..9a6dbc592c 100644
--- a/web/public/locales/zh-CN/views/settings.json
+++ b/web/public/locales/zh-CN/views/settings.json
@@ -16,7 +16,8 @@
"globalConfig": "全局配置 - Frigate",
"cameraConfig": "摄像头配置 - Frigate",
"maintenance": "维护 - Frigate",
- "profiles": "配置模板 - Frigate"
+ "profiles": "配置模板 - Frigate",
+ "detectorsAndModel": "检测器和模型 - Frigate"
},
"menu": {
"ui": "界面设置",
@@ -31,7 +32,7 @@
"enrichments": "增强功能",
"triggers": "触发器",
"roles": "权限组",
- "cameraManagement": "管理",
+ "cameraManagement": "摄像头管理",
"cameraReview": "核查",
"globalDetect": "目标检测",
"general": "常规",
@@ -45,14 +46,14 @@
"globalMotion": "画面变动检测",
"globalObjects": "目标",
"globalReview": "核查",
- "globalAudioEvents": "音频事件",
- "globalLivePlayback": "实时回放",
+ "globalAudioEvents": "音频检测",
+ "globalLivePlayback": "实时监控观看",
"globalTimestampStyle": "时间戳样式",
"systemDatabase": "数据库",
"systemTls": "TLS加密链接",
"systemAuthentication": "验证",
"systemNetworking": "网络",
- "systemProxy": "代理",
+ "systemProxy": "反向代理",
"systemUi": "界面",
"systemLogging": "日志",
"systemEnvironmentVariables": "环境变量",
@@ -69,22 +70,22 @@
"integrationObjectClassification": "目标分类",
"integrationAudioTranscription": "音频转录",
"cameraDetect": "目标检测",
- "cameraFfmpeg": "FFmpeg",
+ "cameraFfmpeg": "视频流(FFmpeg)",
"cameraRecording": "录制",
"cameraSnapshots": "快照",
"cameraMotion": "画面变动检测",
"cameraObjects": "目标",
"cameraConfigReview": "核查",
- "cameraAudioEvents": "音频事件",
+ "cameraAudioEvents": "音频检测",
"cameraAudioTranscription": "音频转录",
"cameraNotifications": "通知",
- "cameraLivePlayback": "实时回放",
+ "cameraLivePlayback": "实时监控观看",
"cameraBirdseye": "鸟瞰图",
"cameraFaceRecognition": "人脸识别",
"cameraLpr": "车牌识别",
"cameraMqttConfig": "MQTT",
"cameraOnvif": "ONVIF",
- "cameraUi": "摄像头管理页面",
+ "cameraUi": "摄像头页面",
"cameraTimestampStyle": "时间戳样式",
"cameraMqtt": "摄像头 MQTT",
"mediaSync": "媒体同步",
@@ -92,7 +93,8 @@
"uiSettings": "界面设置",
"profiles": "配置模板",
"systemGo2rtcStreams": "go2rtc 视频流",
- "maintenance": "维护"
+ "maintenance": "维护",
+ "systemDetectorsAndModel": "检测器和模型"
},
"dialog": {
"unsavedChanges": {
@@ -358,7 +360,8 @@
"object_mask": "目标遮罩"
},
"revertOverride": {
- "title": "恢复为默认配置"
+ "title": "恢复为默认配置",
+ "desc": "这将移除针对 {{type}} {{name}} 的配置覆盖,并恢复为基础配置。"
}
},
"speed": {
@@ -723,7 +726,8 @@
"notificationUnavailable": {
"title": "通知功能不可用",
"desc": "网页推送通知需要安全连接(https://…)。这是浏览器的限制。请通过安全方式访问 Frigate 以使用通知功能。",
- "documentation": "阅读文档"
+ "documentation": "阅读文档",
+ "descPwa": "在 iOS 设备上,只有将 Frigate 安装到主屏幕后,才能使用网页推送通知。请打开分享 菜单,选择添加到主屏幕 ,然后从新生成的图标打开 Frigate,即可注册此设备以接收通知。"
},
"email": {
"title": "电子邮箱",
@@ -794,12 +798,20 @@
"cameras": "摄像头",
"loading": "正在加载模型信息…",
"error": "加载模型信息失败",
- "availableModels": "可用模型",
+ "availableModels": "可用 Frigate+ 模型",
"loadingAvailableModels": "正在加载可用模型…",
"modelSelect": "您可以在Frigate+上选择可用的模型。请注意,只能选择与当前检测器配置兼容的模型。",
"plusModelType": {
"baseModel": "基础模型",
"userModel": "定向调优"
+ },
+ "noModelLoaded": "当前未加载任何Frigate+模型。",
+ "selectModel": "选择一个模型",
+ "noModelsAvailable": "无可用模型",
+ "filter": {
+ "ariaLabel": "按类型筛选模型",
+ "baseModels": "基础模型",
+ "fineTunedModels": "微调过的模型"
}
},
"toast": {
@@ -814,7 +826,8 @@
"currentModel": "当前模型",
"otherModels": "其他模型",
"configuration": "配置"
- }
+ },
+ "changeInDetectorsAndModel": "改变模型"
},
"enrichments": {
"title": "增强功能设置",
@@ -845,7 +858,7 @@
"desc": "将使用 大 模型。该选项使用了完整的 Jina 模型,条件允许的情况下将自动使用 GPU 运行。"
}
},
- "title": "分类搜索",
+ "title": "语义搜索",
"desc": "Frigate 中的语义搜索功能将能够让你通过图片、用户自定义的文本描述,或自动生成的文本描述等方式在核查项目中查找目标/物体。",
"readTheDocumentation": "阅读文档"
},
@@ -1113,7 +1126,7 @@
"noSnapshot": "无法从配置的视频流中获取快照。"
},
"errors": {
- "brandOrCustomUrlRequired": "请选择摄像头品牌并配置主机/IP地址,或选择“其他”后手动配置视频流地址",
+ "brandOrCustomUrlRequired": "请选择摄像头品牌并配置主机/ IP 地址,或选择“其他”后手动配置视频流地址",
"nameRequired": "摄像头名称为必填项",
"nameLength": "摄像头名称要少于64个字符",
"invalidCharacters": "摄像头名称内有不允许使用的字符",
@@ -1121,7 +1134,7 @@
"brands": {
"reolink-rtsp": "不建议使用萤石 RTSP 协议。建议在摄像头设置中启用 HTTP 协议,并重新运行摄像头添加向导。"
},
- "customUrlRtspRequired": "自定义URL必须以“rtsp://”开头;对于非 RTSP 协议的摄像头流,需手动添加至配置文件。"
+ "customUrlRtspRequired": "自定义 URL 必须以“rtsp://”或“rtsps://”开头;对于非 RTSP 协议的摄像头流,需手动添加至配置文件。"
},
"docs": {
"reolink": "https://docs.frigate-cn.video/configuration/camera_specific.html#reolink-cameras"
@@ -1138,7 +1151,7 @@
"detectionMethodDescription": "如果摄像头支持 ONVIF 协议,将使用该协议探测摄像头,以自动获取摄像头视频流地址;若不支持,也可手动选择摄像头品牌来使用预设地址。如需输入自定义RTSP地址,请选择“手动选择”并选择“其他”选项。",
"onvifPortDescription": "对于支持ONVIF协议的摄像头,该端口通常为80或8080。",
"useDigestAuth": "使用摘要认证",
- "useDigestAuthDescription": "为ONVIF协议启用HTTP摘要认证。部分摄像头可能需要专用的 ONVIF 用户名/密码,而非默认的admin账户。"
+ "useDigestAuthDescription": "为 ONVIF 协议启用 HTTP 摘要认证。部分摄像头可能需要专用的 ONVIF 用户名/密码,而非默认的 admin 账户。"
},
"step2": {
"description": "将根据你选择的检测方式,将会自动查找摄像头可用流配置,或进行手动配置。",
@@ -1161,7 +1174,7 @@
},
"testStream": "测试连接",
"testSuccess": "视频流测试成功!",
- "testFailed": "视频流测试失败",
+ "testFailed": "连接测试失败,请检查您的输入后重试。",
"testFailedTitle": "测试失败",
"connected": "已连接",
"notConnected": "未连接",
@@ -1337,7 +1350,8 @@
},
"hikvision": {
"substreamWarning": "子码流1当前被锁定为低分辨率。多数海康威视的摄像头都支持额外的子码流,这些子码流需要在摄像头设置中手动启用。如果你的设备支持,建议你检查并使用这些高分辨率子码流。"
- }
+ },
+ "resolutionUnknown": "无法检测此视频流的分辨率。你需要在设置或配置文件中手动指定检测分辨率。"
}
}
},
@@ -1348,13 +1362,45 @@
"selectCamera": "选择摄像头",
"backToSettings": "返回摄像头设置",
"streams": {
- "title": "开启或关闭摄像头",
+ "title": "摄像头状态和详细信息",
"desc": "将临时禁用摄像头,直到 Frigate 重启。禁用摄像头将完全停止 Frigate 对该摄像头视频流的处理,届时检测、录制及调试功能均不可用。注意:go2rtc 的转流服务不受影响。 ",
"enableLabel": "开启摄像头",
- "enableDesc": "暂时禁用已开启的摄像头,直到 Frigate 重启。禁用摄像头会完全停止 Frigate 对该摄像头视频流的处理。检测、录像和调试功能将不可用。 注意:这不会禁用 go2rtc 的转推流。 ",
+ "enableDesc": "暂时禁用已开启的摄像头,直到 Frigate 重启。禁用摄像头会完全停止 Frigate 对该摄像头视频流的处理。检测、录像和调试功能将不可用。 注意:这不会禁用 go2rtc 的转推流。 拖动滑块以重新排序摄像头,使其在用户界面中按顺序显示。启用的摄像头的顺序将在整个用户界面中反映,包括实时监控仪表板和摄像头选择下拉菜单。",
"disableLabel": "关闭摄像头",
"disableDesc": "开启在当前在界面中不可见且在配置中被禁用的摄像头。启用后需要重启 Frigate 才能生效。",
- "enableSuccess": "已在配置中启用 {{cameraName}}。请重启 Frigate 以应用更改。"
+ "enableSuccess": "已启用 {{cameraName}}。请重启 Frigate 以应用。",
+ "friendlyName": {
+ "edit": "修改摄像头显示名称",
+ "title": "修改显示名称",
+ "description": "设置该摄像机在 Frigate 用户界面中显示的名称。若留空,则使用摄像机 ID。",
+ "rename": "重命名"
+ },
+ "reorderHandle": "拖动以重新排序",
+ "saving": "保存中…",
+ "saved": "已保存",
+ "details": {
+ "edit": "编辑摄像头细节",
+ "title": "编辑摄像头细节",
+ "description": "更新此摄像头在 Frigate 页面中使用的显示名称、外部设置 URL 地址和是否可见。",
+ "friendlyNameLabel": "显示名称",
+ "friendlyNameHelp": "在 Frigate 页面中显示此摄像头的友好名称。留空以使用摄像头 ID。",
+ "webuiUrlLabel": "摄像头管理后台 URL",
+ "webuiUrlHelp": "从调试页面中直接访问摄像头管理网页界面。留空以禁用链接。",
+ "webuiUrlInvalid": "必须是有效的 URL(例如:https://example.com)。",
+ "dashboardLabel": "在实时监控面板上显示",
+ "dashboardHelp": "在实时监控面板显示该摄像头。",
+ "reviewLabel": "在核查中显示",
+ "reviewHelp": "在核查页面展示该摄像头,包括在筛选列表、画面变动核查以及历史视图。"
+ },
+ "label": "摄像头状态",
+ "description": "设置各摄像头运行状态开启 :正常处理视频流关闭 :临时暂停处理,重启后状态不保留停用 :停止处理并保存配置,重新启用需重启程序备注:停用操作不会影响 go2rtc 转流功能 拖动控件调整摄像头界面显示顺序,排序效果同步应用于实时面板及摄像头选择下拉栏。",
+ "disabledSubheading": "配置中禁用",
+ "status": {
+ "on": "开",
+ "off": "关",
+ "disabled": "关闭"
+ },
+ "disableSuccess": "已停用 {{cameraName}} 并保存至配置。"
},
"cameraConfig": {
"add": "添加摄像头",
@@ -1400,10 +1446,103 @@
"profiles": {
"title": "配置模板的摄像头覆盖项",
"selectLabel": "选择配置模板",
- "description": "配置在启用某个配置模板时,哪些摄像头应被开启或关闭。设置为“继承”的摄像头会沿用它原本的启用/禁用状态。",
+ "description": "配置启用配置文件时开启或关闭的摄像头。设置为“继承”的摄像头会沿用它原本的默认状态。",
"inherit": "继承",
"enabled": "开启",
- "disabled": "关闭"
+ "disabled": "关闭",
+ "on": "开",
+ "off": "关"
+ },
+ "cameraType": {
+ "title": "摄像头类型",
+ "label": "摄像头类型",
+ "description": "为每路摄像头设置类型。专用车牌识别(LPR)摄像头为单用途设备,配备高倍光学变焦,可抓拍远处车辆的车牌。绝大多数摄像头应选用“通用”类型;只有专为车牌识别部署、且画面聚焦对准车牌的摄像头,才需选择“专用车牌识别”。",
+ "normal": "通用",
+ "dedicatedLpr": "车牌识别专用",
+ "saveSuccess": "已更新 {{cameraName}} 的摄像头类型,请重启 Frigate 以使更改生效。"
+ },
+ "description": "添加、编辑和删除摄像头,控制每个摄像头的状态,并配置每个配置文件和摄像头类型的覆盖设置。要配置视频流、检测、画面变动和其他特定于摄像头的设置,请在“摄像头配置”下选择相关功能。",
+ "clone": {
+ "sectionTitle": "复制设置",
+ "sectionDescription": "将摄像头的配置复制到另一台摄像头或新摄像头上。",
+ "button": "复制设置",
+ "title": "复制摄像头设置",
+ "description": "将摄像头的配置复制到其他一个或多个摄像头,或者一个新摄像头上。但摄像头标识(包括名称、别名、Web UI 网址、显示顺序)不会被复制。",
+ "source": {
+ "label": "源摄像头",
+ "placeholder": "选择源摄像头",
+ "required": "选择源摄像头"
+ },
+ "target": {
+ "legend": "目标",
+ "newRadio": "新摄像头",
+ "newNameLabel": "摄像头名称",
+ "newNamePlaceholder": "例如:后门",
+ "newNameRequired": "摄像头名称为必填项",
+ "newNameInvalid": "摄像头名称无效",
+ "newNameCollision": "已存在同名摄像头",
+ "newStreamsForced": "在创建新摄像头时,始终会复制视频流配置。",
+ "existingCamerasRadio": "现有摄像头",
+ "allCameras": "所有摄像头",
+ "existingPlaceholder": "至少选择一个摄像头",
+ "existingDisabled": "没有其他摄像头可供复制"
+ },
+ "categories": {
+ "legend": "要复制的设置",
+ "description": "选择要从源摄像头复制哪些设置。",
+ "selectAll": "选择所有",
+ "selectNone": "取消全选",
+ "resetDefaults": "恢复默认设置",
+ "general": "常规",
+ "spatial": "空间设置",
+ "streams": "视频流",
+ "spatialWarningTitle": "分辨率不一致",
+ "spatialWarning": "源摄像头 {{srcCamera}} 的检测分辨率 ({{srcWidth}}×{{srcHeight}}) 与以下摄像头存在差异:{{cameras}}。在这些摄像头上,多边形(检测区域)可能无法准确对齐。这些默认选项当前处于关闭状态;如果启用,将按原样进行复制。",
+ "restartHint": "需要重启",
+ "items": {
+ "record": "录制",
+ "snapshots": "快照",
+ "review": "核查",
+ "motion": "画面变动检测",
+ "objects": "目标",
+ "audio": "音频检测",
+ "audio_transcription": "音频转录",
+ "notifications": "通知",
+ "birdseye": "鸟瞰图",
+ "mqtt": "MQTT",
+ "timestamp_style": "时间戳样式",
+ "onvif": "ONVIF",
+ "lpr": "车牌识别",
+ "face_recognition": "人脸识别",
+ "semantic_search": "语义搜索",
+ "genai": "生成式 AI",
+ "type": "摄像头类型(通用或车牌识别专用)",
+ "profiles": "配置模板",
+ "detect": "检测维度",
+ "zones": "区域",
+ "motion_mask": "画面变动遮罩",
+ "object_masks": "目标遮罩",
+ "ffmpeg_live": "视频流地址和功能"
+ }
+ },
+ "footer": {
+ "changeCount_other": "将应用 {{count}} 项更改",
+ "restartNeeded": "部分更改需要重启才能生效。",
+ "liveOnly": "所有更改将立即生效,无需重启。",
+ "submit": "复制",
+ "submitting": "复制中…"
+ },
+ "toast": {
+ "success": "设置已复制到 {{cameraName}}",
+ "successWithRestart": "设置已复制到 {{cameraName}}。请重启 Frigate 以应用所有更改。",
+ "successMulti_other": "设置已复制到 {{count}} 个摄像头",
+ "successMultiWithRestart_other": "设置已复制到 {{count}} 个摄像头。请重启 Frigate 以应用所有更改。",
+ "partialFailure": "{{successCount}} 个部分已应用;'{{failedSection}}' 失败:{{errorMessage}}",
+ "partialFailureMulti": "已复制到 {{successCount}} 个摄像头;{{failed}} 个失败:{{errorMessage}}",
+ "newCameraPartialFailure": "摄像头 {{cameraName}} 已创建,但部分设置复制失败:{{errorMessage}}",
+ "sourceMissing": "源摄像头已不存在",
+ "submitError": "复制摄像头失败:{{errorMessage}}"
+ }
}
},
"cameraReview": {
@@ -1493,7 +1632,7 @@
"tls": "TLS",
"proxy": "代理",
"go2rtc": "go2rtc",
- "ffmpeg": "FFmpeg",
+ "ffmpeg": "FFmpeg 编解码",
"detectors": "检测器",
"genai": "生成式 AI",
"face_recognition": "人脸识别",
@@ -1519,7 +1658,7 @@
"remove": "移除"
},
"roleMap": {
- "empty": "未配置角色映射。",
+ "empty": "未配置权限组映射",
"addMapping": "添加角色映射",
"roleLabel": "角色",
"groupsLabel": "用户组",
@@ -1561,7 +1700,17 @@
}
},
"cameraInputs": {
- "itemTitle": "视频流 {{index}}"
+ "itemTitle": "视频流 {{index}}",
+ "sourceMode": {
+ "restream": "转流传输(go2rtc)",
+ "manual": "手动输入路径",
+ "go2rtcStreamPlaceholder": "选择 go2rtc 视频流",
+ "noGo2rtcStreams": "没有配置 go2rtc 视频流",
+ "go2rtcStreamSearch": "搜索视频流…",
+ "go2rtcStreamLabel": "go2rtc 视频流",
+ "availableStreams": "可用的视频流",
+ "noMatchingStreams": "没有匹配的视频流"
+ }
},
"restartRequiredField": "需要重启",
"restartRequiredFooter": "配置已更改 - 需要重启",
@@ -1643,7 +1792,9 @@
"options": {
"embeddings": "嵌入(Embedding)",
"vision": "视觉(Vision)",
- "tools": "工具(Tools)"
+ "tools": "工具(Tools)",
+ "descriptions": "描述生成",
+ "chat": "聊天对话"
}
},
"semanticSearchModel": {
@@ -1655,7 +1806,46 @@
"summary": "已选择 {{count}} 个标签",
"empty": "暂无可用标签"
},
- "addCustomLabel": "添加自定义标签…"
+ "addCustomLabel": "添加自定义标签…",
+ "genaiModel": {
+ "placeholder": "选择或输入模型…",
+ "search": "搜索或输入模型…",
+ "noModels": "暂无模型",
+ "available": "可用模型",
+ "useCustom": "使用 “{{value}}”",
+ "refresh": "刷新模型列表",
+ "probeFailed": "无法获取模型列表",
+ "fetchedModels": "成功获取模型列表"
+ },
+ "knownPlates": {
+ "namePlaceholder": "例如:老婆的车",
+ "platePlaceholder": "车牌号或正则表达式"
+ },
+ "semanticSearchModelSize": {
+ "notApplicable": "不适用于生成式 AI 提供者"
+ },
+ "liveStreams": {
+ "streamNameLabel": "视频流名称",
+ "streamNamePlaceholder": "例如:高清流",
+ "go2rtcStreamLabel": "go2rtc 视频流",
+ "go2rtcStreamPlaceholder": "选择 go2rtc 视频流",
+ "go2rtcStreamSearch": "搜索或输入视频流名称…",
+ "noGo2rtcStreams": "没有 go2rtc 视频流配置",
+ "availableStreams": "可用的视频流",
+ "useCustom": "使用“{{value}}”",
+ "addStream": "添加视频流"
+ },
+ "ptzPresets": {
+ "placeholder": "选择或输入预设…",
+ "search": "搜索或输入预设…",
+ "noPresets": "没有可用的预设",
+ "available": "摄像头预设",
+ "useCustom": "使用 “{{value}}”"
+ },
+ "defaultRole": {
+ "admin": "管理员",
+ "viewer": "成员"
+ }
},
"cameraConfig": {
"title": "摄像头配置",
@@ -1721,7 +1911,7 @@
"desc": "区域网格是一种优化功能,它会学习不同大小的目标通常出现在每个摄像头视野中的位置。Frigate 利用这些数据来高效地确定检测区域的大小。该网格会根据追踪目标数据自动构建。",
"clear": "清除区域网格",
"clearConfirmTitle": "清除区域网格",
- "clearConfirmDesc": "除非您最近更改了检测器模型大小或摄像头的物理位置,并且遇到了目标追踪问题,否则不建议清除区域网格。网格会随着目标的追踪自动重建。更改需要重启 Frigate 才能生效。",
+ "clearConfirmDesc": "除非你最近更改了检测器模型大小或摄像头的物理位置,并且遇到了目标追踪问题,否则不建议清除区域网格。网格会随着目标的追踪自动重建。更改需要重启 Frigate 才能生效。",
"clearSuccess": "区域网格清除成功",
"clearError": "清除区域网格失败",
"restartRequired": "需要重启以使区域网格更改生效"
@@ -1746,7 +1936,8 @@
"resetError": "重置设置失败",
"saveAllSuccess_other": "所有 {{count}} 个部分保存成功。",
"saveAllPartial_other": "已保存 {{successCount}} / {{totalCount}} 个部分。{{failCount}} 个失败。",
- "saveAllFailure": "保存所有部分失败。"
+ "saveAllFailure": "保存所有部分失败。",
+ "saveAllSuccessRestartRequired_other": "成功保存 {{count}} 个部分。重启 Frigate 以生效。"
},
"unsavedChanges": "您有未保存的更改",
"confirmReset": "确认重置",
@@ -1756,7 +1947,18 @@
"overriddenGlobal": "已覆盖全局通用配置",
"overriddenGlobalTooltip": "当前摄像头配置,将优先覆盖全局通用设置",
"overriddenBaseConfigTooltip": "当前 {{profile}} 配置模板会覆盖本节所有设置",
- "overriddenBaseConfig": "已覆盖默认配置"
+ "overriddenBaseConfig": "已覆盖默认配置",
+ "overriddenInCameras": {
+ "label_other": "已在 {{count}} 个摄像头中单独配置",
+ "tooltip_other": "{{count}} 个摄像头在此项中存在单独配置,点击查看详情。",
+ "heading_other": "此全局设置项下有 {{count}} 个摄像头存在自定义单独配置。",
+ "othersField_other": "其余 {{count}} 个",
+ "profilePrefix": "{{profile}} 配置方案:{{fields}}"
+ },
+ "overriddenGlobalHeading_other": "该摄像头已覆盖全局配置中的 {{count}} 项设置:",
+ "overriddenGlobalNoDeltas": "该摄像头已覆盖全局配置,但所有配置项数值均无差异。",
+ "overriddenBaseConfigHeading_other": "{{profile}} 配置模板已覆盖基础配置中的 {{count}} 项设置:",
+ "overriddenBaseConfigNoDeltas": "{{profile}} 配置模板已覆盖该板块,但各项参数与基础配置完全一致无差异。"
},
"profiles": {
"title": "配置模板",
@@ -1795,7 +1997,7 @@
"deleteSection": "删除节点覆盖",
"deleteSectionConfirm": "是否要移除摄像机 {{camera}} 上针对配置文件 {{profile}} 的 {{section}} 覆盖设置?",
"deleteSectionSuccess": "已移除 {{profile}} 的 {{section}} 覆盖设置",
- "enableSwitch": "开启配置文件",
+ "enableSwitch": "开启配置模板",
"enabledDescription": "配置文件功能已启用。请在下方创建新的配置文件,进入摄像头配置页面进行修改并保存,修改即可生效。",
"disabledDescription": "配置文件功能可以让你创建一组带名称的摄像头自定义参数(比如布防、离家、夜间模式),并随时切换启用。"
},
@@ -1844,22 +2046,41 @@
"audioMp3": "转码为 MP3",
"audioExclude": "排除",
"hardwareNone": "无硬件加速",
- "hardwareAuto": "自动选择硬件加速"
- }
+ "hardwareAuto": "自动模式(推荐)",
+ "hardwareVaapi": "VAAPI",
+ "hardwareCuda": "CUDA",
+ "hardwareV4l2m2m": "V4L2 M2M",
+ "hardwareDxva2": "DXVA2",
+ "hardwareVideotoolbox": "VideoToolbox",
+ "addVideoCodec": "添加视频编码器",
+ "addAudioCodec": "添加音频编码器",
+ "removeCodec": "移除编码器"
+ },
+ "streamNumber": "视频流 {{index}}",
+ "sourceNumber": "源 {{index}}"
},
"onvif": {
"profileAuto": "自动",
- "profileLoading": "正在加载配置文件…"
+ "profileLoading": "正在加载配置文件…",
+ "autotracking": {
+ "zooming": {
+ "disabled": "关闭",
+ "absolute": "绝对",
+ "relative": "相对"
+ }
+ }
},
"configMessages": {
"review": {
"recordDisabled": "录制已禁用,不会生成核查记录项。",
"detectDisabled": "目标检测已禁用。核查记录需要依靠检测到的目标来对警报和检测事件进行分类。",
- "allNonAlertDetections": "所有非警报类活动都将被记录为检测事件。"
+ "allNonAlertDetections": "所有非警报类活动都将被记录为检测事件。",
+ "genaiImageSourceRecordingsRecordDisabled": "图像源虽然设置为“录制”,但录制功能已关闭。Frigate 将自动降级使用预览图片。"
},
"lpr": {
- "vehicleNotTracked": "车牌识别需要先开启对 “汽车” 或 “摩托车” 的目标追踪。",
- "globalDisabled": "车牌识别未在全局开启。请在全局设置中开启该功能,才能在摄像头下单独配置车牌识别是否开启。"
+ "vehicleNotTracked": "车牌识别需要先开启对 “汽车” 或 “摩托车” 的目标追踪。请在该摄像头的检测目标中添加“汽车”或“摩托车”。",
+ "globalDisabled": "要让该摄像头的车牌识别功能正常使用,必须先开启车牌识别增强功能。",
+ "modelSizeLarge": "大型模型针对多行格式车牌做了优化。小型模型的性能优于大型模型,而且只有小型模型才能支持中文车牌。除非你所在地区使用多行车牌格式,否则建议使用小型模型。"
},
"audio": {
"noAudioRole": "暂无任何流已开启音频(audio)功能(role)。必须在视频流上启用音频功能,音频检测才能正常工作。"
@@ -1868,11 +2089,19 @@
"audioDetectionDisabled": "该摄像头未开启音频检测功能。音频转录需要先开启音频检测。"
},
"detect": {
- "fpsGreaterThanFive": "不建议设置检测帧率高于 5。"
+ "fpsGreaterThanFive": "不建议设置检测帧率高于 5,数值设置过高可能引发性能问题,且不会带来任何增益。",
+ "disabled": "目标检测已禁用。快照、回放条目以及人脸识别、车牌识别、生成式 AI 等增强功能都将无法使用。",
+ "resolutionShouldBeMultipleOfFour": "为了获得最佳效果,检测的宽度和高度应该是4的倍数。其他偶数值可能会在检测流中产生视觉伪影或轻微失真。",
+ "aspectRatioMismatch": "你输入的宽度和高度与当前检测分辨率的不匹配,这可能会导致图像被拉伸或变形。",
+ "maxFramesSet": "设置最大帧数将会覆盖默认行为,并且禁用静态物体追踪。只有很少的情况下需要这个功能,请谨慎使用。",
+ "squareResolution": "方形的检测分辨率比较少见。检测宽度和高度应该与你摄像头的画面比例一致(例如 16:9),而不是跟目标检测模型的输入尺寸一致。画面比例不匹配可能会拉伸图像,从而降低检测准确率。",
+ "resolutionHigh": "这个检测分辨率高于推荐值,可能会增加资源占用,但不一定提升检测准确率。对于大多数摄像头,建议使用 1080p 或更低的检测分辨率。",
+ "globalResolutionMultipleCameras": "在配置了多个摄像头同时还设置了全局检测分辨率。除非所有摄像头都使用相同的分辨率和画面比例,否则应该为每个摄像头单独设置检测宽度和高度,使其匹配各自摄像头的原生画面比例。"
},
"faceRecognition": {
- "globalDisabled": "人脸识别未在全局开启。请在全局设置中开启该功能,才能在摄像头下单独配置人脸识别是否开启。",
- "personNotTracked": "人脸识别需要检测到 “人”(person) 后才能工作。请确保 “person” 已添加到目标追踪列表中。"
+ "globalDisabled": "必须开启人脸识别增强功能,此摄像头的人脸识别相关功能才能正常使用。",
+ "personNotTracked": "人脸识别需要检测到 “人”(person) 后才能工作。请在该摄像头的检测目标设置中添加“人”。",
+ "modelSizeLarge": "大型模型需要 GPU 或 NPU 才能运行正常。仅使用 CPU 的设备请选用小型模型。"
},
"record": {
"noRecordRole": "暂无任何视频流已配置录制功能,录制功能将无法正常工作。"
@@ -1884,7 +2113,124 @@
"detectDisabled": "目标检测已禁用。快照是根据追踪到的目标生成的,因此将不会创建快照。"
},
"detectors": {
- "mixedTypes": "所有检测器必须为同一类型。若要更换为其他类型,请先移除现有的检测器。"
+ "mixedTypes": "所有检测器必须为同一类型。若要更换为其他类型,请先移除现有的检测器。",
+ "mixedTypesSuggestion": "所有检测器必须使用相同类型。请移除现有检测器,或选择 {{type}}。"
+ },
+ "objects": {
+ "genaiNoDescriptionsProvider": "必须配置具备“描述”功能的生成式 AI 服务商,才能自动生成事件描述。"
+ },
+ "semanticSearch": {
+ "jinav2SmallModelSize": "Jina V2 的大型模型版本内存占用与推理开销较高,建议搭配独立显卡使用大型模型。",
+ "modelSizeIgnoredForProvider": "模型大小仅适用于内置的 Jina 模型。当使用生成式 AI 作为嵌入提供者时,此值将被忽略。"
+ },
+ "onvif": {
+ "autotrackingNoZones": "自动追踪至少需要一个区域。请先在“遮罩 / 区域”中为此摄像头定义一个区域,然后在下方将其设置为必需区域。"
+ },
+ "ffmpeg": {
+ "hwaccelManualNotRecommended": "不建议手动硬件加速参数。除非存在特定需求,否则选择与你的硬件匹配的预设。"
+ },
+ "model": {
+ "optimizedFor320": "Frigate 针对了 320x320 的模型进行优化,这是大多数设置的最佳选择。640x640 模型速度较慢,仅在特定场景下有帮助。",
+ "inputDimensionsNotDetectResolution": "模型输入的宽度和高度是目标检测模型的输入尺寸,而不是你摄像头的检测分辨率。它们应该与你正在使用的模型的尺寸相匹配,通常是一个方形尺寸,如320x320或640x640。"
}
+ },
+ "birdseye": {
+ "trackingMode": {
+ "objects": "基于目标",
+ "motion": "基于画面变动",
+ "continuous": "连续"
+ },
+ "cameraOrder": {
+ "label": "摄像头排序",
+ "description": "拖动摄像头以在鸟瞰布局中设置它们的顺序。",
+ "reorderHandle": "拖动以重新排序",
+ "saving": "保存中…",
+ "saved": "已保存"
+ }
+ },
+ "snapshot": {
+ "retainMode": {
+ "all": "所有",
+ "motion": "画面变动",
+ "active_objects": "活动目标"
+ }
+ },
+ "ui": {
+ "timeFormat": {
+ "browser": "基于浏览器",
+ "12hour": "12 小时制",
+ "24hour": "24 小时制"
+ },
+ "TimeOrDateStyle": {
+ "full": "完整",
+ "long": "长",
+ "medium": "中等",
+ "short": "段"
+ },
+ "unitSystem": {
+ "metric": "公制单位",
+ "imperial": "英制单位"
+ }
+ },
+ "review": {
+ "imageSource": {
+ "recordings": "录制文件",
+ "previews": "预览"
+ }
+ },
+ "logger": {
+ "logLevel": {
+ "debug": "调试",
+ "info": "信息",
+ "warning": "警告",
+ "error": "错误",
+ "critical": "关键"
+ }
+ },
+ "modelSize": {
+ "small": "小型",
+ "large": "大型"
+ },
+ "retainMode": {
+ "all": "全部",
+ "motion": "画面变动",
+ "active_objects": "活动目标"
+ },
+ "previewQuality": {
+ "very_high": "非常高",
+ "high": "高",
+ "medium": "中等",
+ "low": "低",
+ "very_low": "非常低"
+ },
+ "detectorsAndModel": {
+ "title": "检测器和模型",
+ "description": "配置用于运行目标检测的检测器后端及对应模型,配置将统一保存,确保检测器与模型保持匹配一致。",
+ "cardTitles": {
+ "detector": "检测器硬件",
+ "model": "检测器模型"
+ },
+ "tabs": {
+ "plus": "Frigate+",
+ "custom": "自定义模型"
+ },
+ "mismatch": {
+ "warning": "当前 Frigate+ 模型“{{model}}”需搭配 {{required}} 检测器使用。请在下方选择兼容的模型,或切换为自定义模型后再保存。"
+ },
+ "plusModel": {
+ "requiresDetector": "需要检测器:{{detector}}",
+ "noModelSelected": "选择 Frigate+ 模型"
+ },
+ "toast": {
+ "saveSuccess": "检测器与模型设置已保存,请重启 Frigate 以生效配置。",
+ "saveError": "保存检测器及模型设置失败"
+ },
+ "unsavedChanges": "检测器与模型配置存在未保存修改",
+ "restartRequired": "需要重启(检测器 或 模型 的设置已变更)"
+ },
+ "menuDot": {
+ "overrideGlobal": "这一部分覆盖了全局配置",
+ "overrideProfile": "本节被 {{profile}} 配置文件覆盖",
+ "unsaved": "这一部分有未保存的更改"
}
}
diff --git a/web/public/locales/zh-CN/views/system.json b/web/public/locales/zh-CN/views/system.json
index 6e406674a7..c14f062916 100644
--- a/web/public/locales/zh-CN/views/system.json
+++ b/web/public/locales/zh-CN/views/system.json
@@ -175,7 +175,22 @@
"tips": {
"title": "摄像头信息"
},
- "aspectRatio": "宽高比"
+ "aspectRatio": "宽高比",
+ "keyframes": {
+ "title": "关键帧分析",
+ "analyzing": "正在分析关键帧... 剩余 {{seconds}} 秒",
+ "stillAnalyzing": "仍在分析关键帧...",
+ "recordStream": "录制视频流:",
+ "keyframeCount": "观察到的关键帧:",
+ "observedDuration": "观测持续时间:",
+ "gap": "关键帧间隔(最小值 / 平均值 / 最大值):",
+ "segmentLength": "录制片段长度:",
+ "ok": "每 {{seconds}} 秒取一帧,适用于录制和回放。",
+ "warning": "稀疏或不均匀的关键帧(最长间隔约{{seconds}}秒),可能是使用了智能编码器(例如H.264+/H.265+),不建议开启该功能。",
+ "error": "关键帧间隔(~{{seconds}}秒)超过了录制片段长度({{segmentTime}}秒)。某些片段可能没有关键帧,这会导致播放中断。请尝试禁用摄像头的智能或 + 编解码器或缩短其关键帧间隔。",
+ "unknown": "无法确定关键帧间隔。",
+ "recordDisabled": "此摄像头的录制功能已禁用。"
+ }
},
"framesAndDetections": "帧数/检测次数",
"label": {
@@ -213,6 +228,9 @@
"expectedFps": "预期帧率",
"reconnectsLastHour": "最近一小时重连次数",
"stallsLastHour": "最近一小时卡顿次数"
+ },
+ "noCameras": {
+ "title": "没有找到摄像头"
}
},
"lastRefreshed": "最后刷新时间: ",
diff --git a/web/public/locales/zh-Hant/audio.json b/web/public/locales/zh-Hant/audio.json
index 9a458ce9c3..f5dd289f88 100644
--- a/web/public/locales/zh-Hant/audio.json
+++ b/web/public/locales/zh-Hant/audio.json
@@ -77,5 +77,427 @@
"chatter": "嘈雜聲",
"crowd": "人群聲",
"children_playing": "兒童嬉鬧聲",
- "pets": "寵物"
+ "pets": "寵物",
+ "yip": "吠叫",
+ "howl": "嚎叫",
+ "bow_wow": "汪汪",
+ "growling": "咆哮",
+ "whimper_dog": "狗嗚咽",
+ "purr": "咕嚕",
+ "meow": "喵喵",
+ "hiss": "嘶嘶聲",
+ "caterwaul": "貓叫春",
+ "livestock": "牲畜",
+ "clip_clop": "蹄聲",
+ "neigh": "嘶鳴",
+ "cattle": "牛",
+ "moo": "哞哞",
+ "cowbell": "牛鈴",
+ "pig": "豬",
+ "oink": "哼哼",
+ "bleat": "咩咩",
+ "fowl": "家禽",
+ "chicken": "雞",
+ "cluck": "咯咯",
+ "cock_a_doodle_doo": "喔喔",
+ "turkey": "火雞",
+ "gobble": "咯咯",
+ "duck": "鴨子",
+ "quack": "嘎嘎",
+ "goose": "鵝",
+ "honk": "鳴笛/鵝叫聲",
+ "wild_animals": "野生動物",
+ "roaring_cats": "吼叫的貓科動物",
+ "roar": "吼叫",
+ "chirp": "啾啾",
+ "squawk": "啼叫",
+ "pigeon": "鴿子",
+ "coo": "咕咕",
+ "crow": "烏鴉",
+ "caw": "呱呱",
+ "owl": "貓頭鷹",
+ "hoot": "嗚嗚",
+ "flapping_wings": "翅膀拍打",
+ "dogs": "狗群",
+ "rats": "老鼠",
+ "patter": "啪嗒聲",
+ "insect": "昆蟲",
+ "cricket": "蟋蟀",
+ "mosquito": "蚊子",
+ "fly": "蒼蠅",
+ "buzz": "嗡嗡",
+ "frog": "青蛙",
+ "croak": "呱呱",
+ "snake": "蛇",
+ "rattle": "響尾",
+ "whale_vocalization": "鯨魚叫聲",
+ "music": "音樂",
+ "musical_instrument": "樂器",
+ "plucked_string_instrument": "彈撥樂器",
+ "guitar": "吉他",
+ "electric_guitar": "電吉他",
+ "bass_guitar": "貝斯",
+ "acoustic_guitar": "原聲吉他",
+ "steel_guitar": "鋼弦吉他",
+ "tapping": "敲擊",
+ "strum": "掃弦",
+ "banjo": "班卓琴",
+ "sitar": "西塔琴",
+ "mandolin": "曼陀林",
+ "zither": "古箏",
+ "ukulele": "尤克里裡",
+ "piano": "鋼琴",
+ "electric_piano": "電鋼琴",
+ "organ": "風琴",
+ "electronic_organ": "電子琴",
+ "hammond_organ": "哈蒙德風琴",
+ "synthesizer": "合成器",
+ "sampler": "取樣器",
+ "harpsichord": "大鍵琴",
+ "percussion": "打擊樂器",
+ "drum_kit": "架子鼓",
+ "drum_machine": "鼓機",
+ "drum": "鼓",
+ "snare_drum": "軍鼓",
+ "rimshot": "鼓邊擊",
+ "drum_roll": "滾鼓",
+ "bass_drum": "大鼓",
+ "timpani": "定音鼓",
+ "tabla": "塔布拉鼓",
+ "cymbal": "鈸",
+ "hi_hat": "踩鑔",
+ "wood_block": "木魚",
+ "tambourine": "鈴鼓",
+ "maraca": "沙錘",
+ "gong": "鑼",
+ "tubular_bells": "管鍾",
+ "mallet_percussion": "槌擊打擊樂器",
+ "marimba": "馬林巴",
+ "glockenspiel": "鐘琴",
+ "vibraphone": "顫音琴",
+ "steelpan": "鋼鼓",
+ "orchestra": "管絃樂隊",
+ "brass_instrument": "銅管樂器",
+ "french_horn": "圓號",
+ "trumpet": "小號",
+ "trombone": "長號",
+ "bowed_string_instrument": "弓弦樂器",
+ "string_section": "絃樂組",
+ "violin": "小提琴",
+ "pizzicato": "撥絃",
+ "cello": "大提琴",
+ "double_bass": "低音提琴",
+ "wind_instrument": "管樂器",
+ "flute": "長笛",
+ "saxophone": "薩克斯",
+ "clarinet": "單簧管",
+ "harp": "豎琴",
+ "bell": "鈴",
+ "church_bell": "教堂鍾",
+ "jingle_bell": "鈴鐺",
+ "bicycle_bell": "腳踏車鈴",
+ "tuning_fork": "音叉",
+ "chime": "風鈴",
+ "wind_chime": "風鈴",
+ "harmonica": "口琴",
+ "accordion": "手風琴",
+ "bagpipes": "風笛",
+ "didgeridoo": "迪吉里杜管",
+ "theremin": "特雷門琴",
+ "singing_bowl": "頌缽",
+ "scratching": "刮擦聲",
+ "pop_music": "流行音樂",
+ "hip_hop_music": "嘻哈音樂",
+ "beatboxing": "人聲節拍",
+ "rock_music": "搖滾音樂",
+ "heavy_metal": "重金屬",
+ "punk_rock": "朋克搖滾",
+ "grunge": "垃圾搖滾",
+ "progressive_rock": "前衛搖滾",
+ "rock_and_roll": "搖滾樂",
+ "psychedelic_rock": "迷幻搖滾",
+ "rhythm_and_blues": "節奏布魯斯",
+ "soul_music": "靈魂樂",
+ "reggae": "雷鬼",
+ "country": "鄉村音樂",
+ "swing_music": "搖擺樂",
+ "bluegrass": "藍草音樂",
+ "funk": "放克",
+ "folk_music": "民謠",
+ "middle_eastern_music": "中東音樂",
+ "jazz": "爵士樂",
+ "disco": "迪斯科",
+ "classical_music": "古典音樂",
+ "opera": "歌劇",
+ "electronic_music": "電子音樂",
+ "house_music": "浩室音樂",
+ "techno": "科技舞曲",
+ "dubstep": "迴響貝斯",
+ "drum_and_bass": "鼓打貝斯",
+ "electronica": "電子樂",
+ "electronic_dance_music": "電子舞曲",
+ "ambient_music": "環境音樂",
+ "trance_music": "迷幻舞曲",
+ "music_of_latin_america": "拉丁美洲音樂",
+ "salsa_music": "薩爾薩",
+ "flamenco": "弗拉門戈",
+ "blues": "藍調",
+ "music_for_children": "兒童音樂",
+ "new-age_music": "新世紀音樂",
+ "vocal_music": "聲樂",
+ "a_capella": "無伴奏合唱",
+ "music_of_africa": "非洲音樂",
+ "afrobeat": "非洲節拍",
+ "christian_music": "基督教音樂",
+ "gospel_music": "福音音樂",
+ "music_of_asia": "亞洲音樂",
+ "carnatic_music": "卡納提克音樂",
+ "music_of_bollywood": "寶萊塢音樂",
+ "ska": "斯卡",
+ "traditional_music": "傳統音樂",
+ "independent_music": "獨立音樂",
+ "song": "歌曲",
+ "background_music": "背景音樂",
+ "theme_music": "主題音樂",
+ "jingle": "廣告歌",
+ "soundtrack_music": "配樂",
+ "lullaby": "搖籃曲",
+ "video_game_music": "電子遊戲音樂",
+ "christmas_music": "聖誕音樂",
+ "dance_music": "舞曲",
+ "wedding_music": "婚禮音樂",
+ "happy_music": "歡快音樂",
+ "sad_music": "悲傷音樂",
+ "tender_music": "溫柔音樂",
+ "exciting_music": "激動音樂",
+ "angry_music": "憤怒音樂",
+ "scary_music": "恐怖音樂",
+ "wind": "風",
+ "rustling_leaves": "樹葉沙沙聲",
+ "wind_noise": "風聲",
+ "thunderstorm": "雷暴",
+ "thunder": "雷聲",
+ "water": "水",
+ "rain": "雨",
+ "raindrop": "雨滴",
+ "rain_on_surface": "雨打表面",
+ "stream": "溪流",
+ "waterfall": "瀑布",
+ "ocean": "海洋",
+ "waves": "波浪",
+ "steam": "蒸汽",
+ "gurgling": "汩汩聲",
+ "fire": "火",
+ "crackle": "噼啪聲",
+ "sailboat": "帆船",
+ "rowboat": "划艇",
+ "motorboat": "摩托艇",
+ "ship": "輪船",
+ "motor_vehicle": "機動車",
+ "toot": "鳴笛",
+ "car_alarm": "汽車警報",
+ "power_windows": "電動車窗",
+ "skidding": "輪胎打滑",
+ "tire_squeal": "輪胎尖叫",
+ "car_passing_by": "汽車駛過",
+ "race_car": "賽車",
+ "truck": "卡車",
+ "air_brake": "氣閘",
+ "air_horn": "氣笛",
+ "reversing_beeps": "倒車提示音",
+ "ice_cream_truck": "冰淇淋車",
+ "emergency_vehicle": "應急車輛",
+ "police_car": "警車",
+ "ambulance": "救護車",
+ "fire_engine": "消防車",
+ "traffic_noise": "交通噪音",
+ "rail_transport": "鐵路運輸",
+ "train_whistle": "火車汽笛",
+ "train_horn": "火車鳴笛",
+ "railroad_car": "鐵路車廂",
+ "train_wheels_squealing": "火車輪子尖叫",
+ "subway": "地鐵",
+ "aircraft": "飛行器",
+ "aircraft_engine": "飛機引擎",
+ "jet_engine": "噴氣引擎",
+ "propeller": "螺旋槳",
+ "helicopter": "直升機",
+ "fixed-wing_aircraft": "固定翼飛機",
+ "engine": "引擎",
+ "light_engine": "輕型引擎",
+ "dental_drill's_drill": "牙科鑽",
+ "lawn_mower": "割草機",
+ "chainsaw": "電鋸",
+ "medium_engine": "中型引擎",
+ "heavy_engine": "重型引擎",
+ "engine_knocking": "引擎敲擊",
+ "engine_starting": "引擎啟動",
+ "idling": "怠速",
+ "accelerating": "加速",
+ "doorbell": "門鈴",
+ "ding-dong": "叮咚",
+ "sliding_door": "滑動門",
+ "slam": "猛關",
+ "knock": "敲門",
+ "tap": "輕敲",
+ "squeak": "吱吱聲",
+ "cupboard_open_or_close": "櫥櫃開關",
+ "drawer_open_or_close": "抽屜開關",
+ "dishes": "餐具",
+ "cutlery": "刀叉",
+ "chopping": "切菜",
+ "frying": "煎炸",
+ "microwave_oven": "微波爐",
+ "water_tap": "水龍頭",
+ "bathtub": "浴缸",
+ "toilet_flush": "馬桶沖水",
+ "electric_toothbrush": "電動牙刷",
+ "vacuum_cleaner": "吸塵器",
+ "zipper": "拉鍊",
+ "keys_jangling": "鑰匙叮噹",
+ "coin": "硬幣",
+ "electric_shaver": "電動剃鬚刀",
+ "shuffling_cards": "洗牌",
+ "typing": "打字",
+ "typewriter": "打字機",
+ "computer_keyboard": "電腦鍵盤",
+ "writing": "書寫",
+ "alarm": "警報",
+ "telephone": "電話",
+ "telephone_bell_ringing": "電話鈴聲",
+ "ringtone": "手機鈴聲",
+ "telephone_dialing": "電話撥號",
+ "dial_tone": "撥號音",
+ "busy_signal": "忙音",
+ "alarm_clock": "鬧鐘",
+ "siren": "警笛",
+ "civil_defense_siren": "防空警報",
+ "buzzer": "蜂鳴器",
+ "smoke_detector": "煙霧檢測器",
+ "fire_alarm": "火災警報器",
+ "foghorn": "霧笛",
+ "whistle": "哨子",
+ "steam_whistle": "蒸汽汽笛",
+ "mechanisms": "機械裝置",
+ "ratchet": "棘輪",
+ "tick": "滴答",
+ "tick-tock": "滴答滴答",
+ "gears": "齒輪",
+ "pulleys": "滑輪",
+ "sewing_machine": "縫紉機",
+ "mechanical_fan": "機械風扇",
+ "air_conditioning": "空調",
+ "cash_register": "收銀機",
+ "printer": "印表機",
+ "single-lens_reflex_camera": "單反相機",
+ "tools": "工具",
+ "hammer": "錘子",
+ "jackhammer": "風鎬",
+ "sawing": "鋸",
+ "filing": "銼",
+ "sanding": "砂磨",
+ "power_tool": "電動工具",
+ "drill": "電鑽",
+ "explosion": "爆炸",
+ "gunshot": "槍聲",
+ "machine_gun": "機關槍",
+ "fusillade": "齊射",
+ "artillery_fire": "炮火",
+ "cap_gun": "玩具槍",
+ "fireworks": "煙花",
+ "firecracker": "鞭炮",
+ "burst": "爆裂",
+ "eruption": "爆發",
+ "boom": "轟隆",
+ "wood": "木頭",
+ "chop": "砍",
+ "splinter": "碎裂",
+ "crack": "破裂",
+ "glass": "玻璃",
+ "chink": "叮噹",
+ "shatter": "粉碎",
+ "silence": "寂靜",
+ "sound_effect": "音效",
+ "environmental_noise": "環境噪音",
+ "static": "靜電噪音",
+ "white_noise": "白噪音",
+ "pink_noise": "粉紅噪音",
+ "television": "電視",
+ "radio": "收音機",
+ "field_recording": "實地錄音",
+ "scream": "尖叫",
+ "sodeling": "索德鈴",
+ "chird": "啾鳴",
+ "change_ringing": "變奏鐘聲",
+ "shofar": "羊角號",
+ "liquid": "液體",
+ "splash": "液體飛濺",
+ "slosh": "液體晃動",
+ "squish": "擠壓",
+ "drip": "水滴聲",
+ "pour": "倒水聲",
+ "trickle": "細流水聲",
+ "gush": "液體噴湧",
+ "fill": "注水聲",
+ "spray": "噴灑",
+ "pump": "泵送",
+ "stir": "攪拌聲",
+ "boiling": "沸騰聲",
+ "sonar": "聲吶聲",
+ "arrow": "箭矢聲",
+ "whoosh": "呼嘯聲",
+ "thump": "砰擊聲",
+ "thunk": "沉悶聲",
+ "electronic_tuner": "電子調音器",
+ "effects_unit": "效果器",
+ "chorus_effect": "合唱效果",
+ "basketball_bounce": "籃球反彈聲",
+ "bang": "砰聲",
+ "slap": "拍擊聲",
+ "whack": "重擊聲",
+ "smash": "猛擊聲",
+ "breaking": "破碎聲",
+ "bouncing": "彈跳聲",
+ "whip": "鞭打聲",
+ "flap": "撲動聲",
+ "scratch": "刮擦聲",
+ "scrape": "刮擦聲",
+ "rub": "摩擦聲",
+ "roll": "捲動聲",
+ "crushing": "壓碎聲",
+ "crumpling": "揉皺聲",
+ "tearing": "撕裂聲",
+ "beep": "嗶聲",
+ "ping": "嘀聲",
+ "ding": "叮聲",
+ "clang": "鐺聲",
+ "squeal": "尖銳聲",
+ "creak": "嘎吱聲",
+ "rustle": "沙沙聲",
+ "whir": "嗡聲",
+ "clatter": "哐啷聲",
+ "sizzle": "滋滋聲",
+ "clicking": "點選聲",
+ "clickety_clack": "咔嗒聲",
+ "rumble": "隆隆聲",
+ "plop": "撲通聲",
+ "hum": "嗡鳴聲",
+ "zing": "嗖聲",
+ "boing": "嘣聲",
+ "crunch": "咔嚓聲",
+ "sine_wave": "正弦波聲",
+ "harmonic": "諧波聲",
+ "chirp_tone": "啾聲",
+ "pulse": "脈衝",
+ "inside": "室內聲",
+ "outside": "室外聲",
+ "reverberation": "混響",
+ "echo": "回聲",
+ "noise": "噪聲",
+ "mains_hum": "電流嗡聲",
+ "distortion": "失真聲",
+ "sidetone": "旁音",
+ "cacophony": "刺耳噪聲",
+ "throbbing": "脈動聲",
+ "vibration": "振動聲"
}
diff --git a/web/public/locales/zh-Hant/common.json b/web/public/locales/zh-Hant/common.json
index 17a60efaa6..08ae3d639f 100644
--- a/web/public/locales/zh-Hant/common.json
+++ b/web/public/locales/zh-Hant/common.json
@@ -69,7 +69,8 @@
},
"inProgress": "處理中",
"invalidStartTime": "無效的起始時間",
- "invalidEndTime": "無效的結束時間"
+ "invalidEndTime": "無效的結束時間",
+ "never": "永不"
},
"unit": {
"speed": {
@@ -95,7 +96,8 @@
"show": "顯示{{item}}",
"ID": "ID",
"none": "無",
- "all": "全部"
+ "all": "全部",
+ "other": "其他"
},
"button": {
"apply": "套用",
@@ -133,7 +135,19 @@
"export": "匯出",
"deleteNow": "立即刪除",
"next": "繼續",
- "continue": "繼續"
+ "continue": "繼續",
+ "add": "新增",
+ "applying": "應用中…",
+ "undo": "撤銷",
+ "copiedToClipboard": "已複製到剪貼簿",
+ "modified": "已修改",
+ "overridden": "已覆蓋",
+ "resetToGlobal": "重設為全域性",
+ "resetToDefault": "重設為預設",
+ "saveAll": "儲存全部",
+ "savingAll": "儲存全部中…",
+ "undoAll": "撤銷全部",
+ "retry": "重試"
},
"menu": {
"system": "系統",
@@ -185,7 +199,10 @@
"bg": "Български (保加利亞文)",
"gl": "Galego (加利西亞文)",
"id": "Bahasa Indonesia (印尼文)",
- "ur": "اردو (烏爾都文)"
+ "ur": "اردو (烏爾都文)",
+ "hr": "Hrvatski(克羅地亞語)",
+ "bs": "Bosanski (波士尼亞語)",
+ "zhHant": "繁體中文 (繁體中文)"
},
"appearance": "外觀",
"darkMode": {
@@ -233,7 +250,11 @@
"logout": "登出",
"setPassword": "設定密碼"
},
- "classification": "標籤分類"
+ "classification": "標籤分類",
+ "profiles": "設定檔",
+ "actions": "操作",
+ "features": "功能",
+ "chat": "聊天"
},
"toast": {
"copyUrlToClipboard": "已複製連結至剪貼簿。",
@@ -242,7 +263,8 @@
"error": {
"title": "保存設定變更失敗:{{errorMessage}}",
"noMessage": "保存設定變更失敗"
- }
+ },
+ "success": "成功儲存設定檔。"
}
},
"role": {
@@ -285,6 +307,11 @@
"internalID": "在Frigate 設定檔和資料庫使用的內部ID"
},
"information": {
- "pixels": "{{area}}px"
+ "pixels": "{{area}} 像素"
+ },
+ "no_items": "沒有項目",
+ "validation_errors": "驗證錯誤",
+ "credentialField": {
+ "savedPlaceholder": "已儲存 — 留空以保留當前設定"
}
}
diff --git a/web/public/locales/zh-Hant/components/camera.json b/web/public/locales/zh-Hant/components/camera.json
index 3bace4d9de..f56923d7fe 100644
--- a/web/public/locales/zh-Hant/components/camera.json
+++ b/web/public/locales/zh-Hant/components/camera.json
@@ -68,7 +68,10 @@
}
},
"birdseye": "鳥瞰"
- }
+ },
+ "showAll": "顯示所有鏡頭群組",
+ "showLess": "顯示更少",
+ "editGroups": "編輯鏡頭群組"
},
"debug": {
"options": {
@@ -82,6 +85,7 @@
"zones": "區域",
"mask": "遮罩",
"motion": "移動",
- "regions": "區塊"
+ "regions": "區塊",
+ "paths": "行動軌跡"
}
}
diff --git a/web/public/locales/zh-Hant/components/dialog.json b/web/public/locales/zh-Hant/components/dialog.json
index b28ccca480..4d294718c2 100644
--- a/web/public/locales/zh-Hant/components/dialog.json
+++ b/web/public/locales/zh-Hant/components/dialog.json
@@ -6,7 +6,8 @@
"title": "Frigate 正在重新啟動",
"content": "此頁面將在 {{countdown}} 秒後重新載入。",
"button": "立即重新載入"
- }
+ },
+ "description": "Frigate 在重啟期間將短暫停止執行。"
},
"explore": {
"plus": {
@@ -19,6 +20,9 @@
},
"state": {
"submitted": "已提交"
+ },
+ "toast": {
+ "error": "無法提交至 Frigate+,請檢查您的網路連線並再試一次。"
}
},
"submitToPlus": {
@@ -57,11 +61,67 @@
"endTimeMustAfterStartTime": "結束時間必須要在開始時間之後",
"noVaildTimeSelected": "沒有選取有效的時間範圍"
},
- "view": "查看"
+ "view": "查看",
+ "queued": "匯出已加入佇列。請在匯出頁面檢視進度。",
+ "batchSuccess_other": "已開始 {{count}} 個匯出,正在開啟案件。",
+ "batchPartial": "已開始 {{total}} 個匯出中的 {{successful}} 個。失敗的攝影機:{{failedCameras}}",
+ "batchFailed": "啟動匯出失敗(共 {{total}} 個)。失敗的攝影機:{{failedCameras}}",
+ "batchQueuedSuccess_other": "已排隊 {{count}} 個匯出,正在開啟案件。",
+ "batchQueuedPartial": "已將 {{total}} 個匯出中的 {{successful}} 個加入佇列。失敗的攝影機:{{failedCameras}}",
+ "batchQueueFailed": "未能將 {{total}} 個匯出加入佇列。失敗的攝影機:{{failedCameras}}"
},
"fromTimeline": {
"saveExport": "保存匯出資料",
- "previewExport": "預覽匯出資料"
+ "previewExport": "預覽匯出資料",
+ "queueingExport": "正在加入匯出佇列…",
+ "useThisRange": "使用此範圍"
+ },
+ "case": {
+ "newCaseOption": "建立新案件",
+ "newCaseNamePlaceholder": "新案件名稱",
+ "newCaseDescriptionPlaceholder": "案件描述",
+ "label": "案件",
+ "nonAdminHelp": "將為這些匯出檔案建立一個新的案件。",
+ "placeholder": "選擇案件"
+ },
+ "queueing": "正在加入匯出佇列…",
+ "tabs": {
+ "export": "單個攝影機",
+ "multiCamera": "多個攝影機"
+ },
+ "multiCamera": {
+ "timeRange": "時間範圍",
+ "selectFromTimeline": "從時間線選擇",
+ "cameraSelection": "攝影機",
+ "cameraSelectionHelp": "在此時間範圍內具有追蹤目標的攝影機會被預先選中",
+ "checkingActivity": "正在檢查攝影機活動…",
+ "noCameras": "沒有可用的攝影機",
+ "detectionCount_other": "{{count}} 個追蹤目標",
+ "nameLabel": "匯出名稱",
+ "namePlaceholder": "這些匯出檔案的可選基礎名稱",
+ "queueingButton": "正在加入匯出佇列…",
+ "exportButton_other": "匯出 {{count}} 個攝影機",
+ "searchOrSelectGroup": "搜尋或選擇一個攝影機群組…",
+ "selectAll": "選擇所有攝影機",
+ "clearSelection": "清除選擇",
+ "selectWithActivity": "有追蹤物件的攝影機",
+ "selectGroup": "選擇群組",
+ "noMatchingCameras": "找不到符合您搜尋條件的攝影機",
+ "selectedCount": "已選取 {{selected}} / {{total}}"
+ },
+ "multi": {
+ "title_other": "匯出 {{count}} 個審閱",
+ "description": "匯出每個選定的審閱項。所有匯出檔案將歸入同一個案件。",
+ "descriptionNoCase": "匯出每個選定的審閱項。",
+ "caseNamePlaceholder": "審閱匯出 - {{date}}",
+ "exportButton_other": "匯出 {{count}} 個審閱",
+ "exportingButton": "匯出中…",
+ "toast": {
+ "started_other": "已開始 {{count}} 個匯出。正在開啟案件。",
+ "startedNoCase_other": "已開始 {{count}} 個匯出。",
+ "partial": "已啟動 {{total}} 個匯出,其中 {{successful}} 個成功。失敗項:{{failedItems}}",
+ "failed": "啟動匯出失敗(共 {{total}} 個)。失敗項:{{failedItems}}"
+ }
}
},
"streaming": {
@@ -97,7 +157,7 @@
"confirmDelete": {
"title": "確認刪除",
"desc": {
- "selected": "你確定要刪除所有與此查核項目有關的錄影檔案嗎? 按住Shift 以在未來跳過此確認步驟。"
+ "selected": "您確定要刪除所有與此審查項目相關的錄影檔案嗎? 按住 Shift 鍵可在未來跳過此確認步驟。"
},
"toast": {
"success": "已成功刪除與選擇的審核物件有關的影片片段。",
@@ -109,6 +169,14 @@
"markAsReviewed": "標記為已審核",
"deleteNow": "立即刪除",
"markAsUnreviewed": "標記為未審核"
+ },
+ "shareTimestamp": {
+ "label": "分享該時間片段",
+ "title": "分享該時間片段",
+ "description": "分享帶當前錄製播放時間的網址,或選擇自訂時間。請注意這不是公開的分享連結,只有具備 Frigate 及此攝影機存取權限的使用者才能存取。",
+ "custom": "自訂時間",
+ "button": "分享時間片段網址",
+ "shareTitle": "Frigate 審閱時間:{{camera}}"
}
},
"imagePicker": {
diff --git a/web/public/locales/zh-Hant/components/filter.json b/web/public/locales/zh-Hant/components/filter.json
index 1cbef2fd31..1b3cbe0dc7 100644
--- a/web/public/locales/zh-Hant/components/filter.json
+++ b/web/public/locales/zh-Hant/components/filter.json
@@ -106,7 +106,7 @@
},
"trackedObjectDelete": {
"title": "確認刪除",
- "desc": "刪除這 {{objectLength}} 個追蹤物件將會刪除截圖、儲存的嵌入資料,以及相關的物件生命週期紀錄。歷史記錄中的錄影檔案不會 被刪除。 你確定要繼續嗎? 按住 Shift 可在未來跳過這個確認內容。",
+ "desc": "刪除這 {{objectLength}} 個追蹤物件將會移除快照、已儲存的嵌入向量資料(embeddings),以及相關的物件生命週期紀錄。歷史檢視中的錄影檔案 不會 被刪除。 確定要繼續嗎? 按住 Shift 鍵可在未來跳過此對話框。",
"toast": {
"success": "成功刪除追蹤物件。",
"error": "刪除追蹤物件失敗:{{errorMessage}}"
diff --git a/web/public/locales/zh-Hant/components/player.json b/web/public/locales/zh-Hant/components/player.json
index dbecdb2beb..7818615c5d 100644
--- a/web/public/locales/zh-Hant/components/player.json
+++ b/web/public/locales/zh-Hant/components/player.json
@@ -4,7 +4,8 @@
"noPreviewFoundFor": "找不到 {{cameraName}} 的預覽",
"submitFrigatePlus": {
"title": "提交此畫面至 Frigate+?",
- "submit": "提交"
+ "submit": "提交",
+ "previewError": "無法載入快照預覽。該錄製當前可能不可用。"
},
"streamOffline": {
"desc": "{{cameraName}} 的 detect 串流未接收到任何畫面,請檢查錯誤日誌",
@@ -47,5 +48,6 @@
"error": {
"submitFrigatePlusFailed": "提交畫面至 Frigate+ 失敗"
}
- }
+ },
+ "cameraOff": "攝影機關機"
}
diff --git a/web/public/locales/zh-Hant/config/cameras.json b/web/public/locales/zh-Hant/config/cameras.json
index 8602044aa0..b9c9244212 100644
--- a/web/public/locales/zh-Hant/config/cameras.json
+++ b/web/public/locales/zh-Hant/config/cameras.json
@@ -30,6 +30,928 @@
"listen": {
"label": "監聽的音訊類型",
"description": "要偵測的音訊事件類型清單(例如:狗吠、火警、尖叫、說話、大叫)。"
+ },
+ "filters": {
+ "label": "音訊過濾器",
+ "description": "按音訊型別的過濾器設定,如用於減少誤報的置信度閾值。",
+ "threshold": {
+ "label": "最低音訊置信度",
+ "description": "音訊事件被計入的最低置信度閾值。"
+ }
+ },
+ "enabled_in_config": {
+ "label": "原始音訊狀態",
+ "description": "指示原始靜態設定檔中是否開啟了音訊偵測。"
+ },
+ "num_threads": {
+ "label": "偵測執行緒",
+ "description": "用於音訊偵測處理的執行緒數量。"
}
+ },
+ "mqtt": {
+ "label": "MQTT",
+ "description": "MQTT 影像釋出設定。",
+ "enabled": {
+ "label": "傳送影像",
+ "description": "為此攝影機啟用向 MQTT 主題釋出目標影像快照。"
+ },
+ "timestamp": {
+ "label": "新增時間戳",
+ "description": "在釋出到 MQTT 的影像上疊加時間戳。"
+ },
+ "bounding_box": {
+ "label": "新增邊界框",
+ "description": "在透過 MQTT 釋出的影像上繪製邊界框。"
+ },
+ "crop": {
+ "label": "裁剪影像",
+ "description": "將釋出到 MQTT 的影像裁剪到偵測到的目標邊界框。"
+ },
+ "height": {
+ "label": "影像高度",
+ "description": "透過 MQTT 釋出的影像的調整高度(像素)。"
+ },
+ "required_zones": {
+ "label": "必需區域",
+ "description": "目標必須進入才能釋出 MQTT 影像的區域。"
+ },
+ "quality": {
+ "label": "JPEG 品質",
+ "description": "釋出到 MQTT 的影像的 JPEG 品質(0-100)。"
+ }
+ },
+ "notifications": {
+ "label": "通知",
+ "enabled": {
+ "label": "開啟通知",
+ "description": "為此攝影機啟用或停用通知。"
+ },
+ "email": {
+ "label": "通知郵箱",
+ "description": "用於推送通知或某些通知提供商要求的郵箱地址。"
+ },
+ "cooldown": {
+ "label": "冷卻時間",
+ "description": "通知之間的冷卻時間(秒),以避免向收件人傳送垃圾資訊。"
+ },
+ "enabled_in_config": {
+ "label": "原始通知狀態",
+ "description": "指示原始靜態配置中是否啟用了通知。"
+ },
+ "description": "為此攝影機啟用和控制通知的設定。"
+ },
+ "birdseye": {
+ "label": "鳥瞰圖",
+ "description": "將多路攝影機畫面合併為統一佈局的鳥瞰合成檢視設定。",
+ "enabled": {
+ "label": "開啟鳥瞰圖",
+ "description": "開啟或關閉鳥瞰圖功能。"
+ },
+ "mode": {
+ "label": "追蹤模式",
+ "description": "在鳥瞰檢視中包含攝影機的模式:'objects'(目標)、'motion'(動作)或 'continuous'(持續)。"
+ },
+ "order": {
+ "label": "排序位置",
+ "description": "用於控制攝影機在鳥瞰檢視佈局中排序位置的數值。"
+ }
+ },
+ "detect": {
+ "label": "目標偵測",
+ "description": "用於執行目標偵測、初始化追蹤器的偵測模組設定。",
+ "enabled": {
+ "label": "開啟目標偵測",
+ "description": "開啟或關閉該攝影機的目標偵測。"
+ },
+ "height": {
+ "label": "偵測畫面高度",
+ "description": "用於配置偵測流的畫面高度(像素);留空則使用原始影片流解析度。"
+ },
+ "width": {
+ "label": "偵測畫面寬度",
+ "description": "用於配置偵測流的畫面寬度(像素);留空則使用原始影片流解析度。"
+ },
+ "fps": {
+ "label": "偵測幀率",
+ "description": "偵測時希望使用的幀率;數值越低,CPU 佔用越小(推薦值為 5,僅在追蹤極高速運動的目標時才設定更高數值,最高不建議超過 10)。"
+ },
+ "min_initialized": {
+ "label": "最小初始化幀數",
+ "description": "建立追蹤目標前,需要連續偵測到目標的次數。數值越大,錯誤觸發的追蹤越少。預設值為幀率除以 2。"
+ },
+ "max_disappeared": {
+ "label": "最大消失幀數",
+ "description": "追蹤目標在連續多少幀未被偵測到時,將被判定為已消失。"
+ },
+ "stationary": {
+ "label": "靜止目標配置",
+ "description": "用於偵測和管理長時間靜止目標的相關設定。",
+ "interval": {
+ "label": "靜止間隔",
+ "description": "設定每隔多少幀執行一次偵測,用於確認目標是否處於靜止狀態。"
+ },
+ "threshold": {
+ "label": "靜止閾值",
+ "description": "目標需要連續多少幀位置不變,才會被標記為靜止狀態。"
+ },
+ "max_frames": {
+ "label": "最大幀數",
+ "description": "限制靜止目標最大追蹤時長(以幀數為單位),超過將會停止追蹤。",
+ "default": {
+ "label": "預設最大幀數",
+ "description": "停止追蹤前,用於追蹤靜止目標的預設最大幀數。"
+ },
+ "objects": {
+ "label": "目標最大幀數",
+ "description": "可對不同型別目標分別設定靜止追蹤的最大幀數(覆蓋全域性設定)。"
+ }
+ },
+ "classifier": {
+ "label": "開啟視覺分類器",
+ "description": "使用視覺分類器,即使偵測框有輕微抖動,也能準確判斷物體是否為靜止。"
+ }
+ },
+ "annotation_offset": {
+ "label": "標記偏移量",
+ "description": "偵測標記的時間偏移量(毫秒),用於讓時間軸上的偵測框與錄影畫面更精準對齊;可設定為正數或負數。"
+ }
+ },
+ "ffmpeg": {
+ "label": "FFmpeg",
+ "description": "FFmpeg 編解碼相關設定,包含可執行檔案路徑、命令列引數、硬體加速選項,以及按不同功能劃分的輸出引數。",
+ "path": {
+ "label": "FFmpeg 路徑",
+ "description": "要使用的 FFmpeg 可執行檔案路徑,或版本別名(如 \"5.0\" 或 \"7.0\")。"
+ },
+ "global_args": {
+ "label": "FFmpeg 全域性引數",
+ "description": "傳遞給 FFmpeg 程序的全域性引數。"
+ },
+ "hwaccel_args": {
+ "label": "硬體加速引數",
+ "description": "用於 FFmpeg 的硬體加速引數。建議使用對應硬體廠商的預設配置。"
+ },
+ "input_args": {
+ "label": "輸入引數",
+ "description": "應用於 FFmpeg 輸入影片流的輸入引數。"
+ },
+ "output_args": {
+ "label": "輸出引數",
+ "description": "用於不同 FFmpeg 功能(如偵測、錄製)的預設輸出引數。",
+ "detect": {
+ "label": "偵測輸出引數",
+ "description": "偵測功能影片流的預設輸出引數。"
+ },
+ "record": {
+ "label": "錄製輸出引數",
+ "description": "錄製功能影片流的預設輸出引數。"
+ }
+ },
+ "retry_interval": {
+ "label": "FFmpeg 重試時間",
+ "description": "攝影機影片流異常斷開後,重新連線前的等待時間。預設為 10 秒。"
+ },
+ "apple_compatibility": {
+ "label": "Apple 相容性",
+ "description": "錄製 H.265 影片時啟用 HEVC 標記,以提升對 Apple 裝置播放的相容性。"
+ },
+ "gpu": {
+ "label": "GPU 索引",
+ "description": "在啟用硬體加速時,預設使用的 GPU 索引。"
+ },
+ "inputs": {
+ "label": "攝影機輸入影片流",
+ "description": "該攝影機的所有輸入流配置清單(包含路徑和功能)。",
+ "path": {
+ "label": "輸入路徑",
+ "description": "攝影機輸入影片流的地址或路徑。"
+ },
+ "roles": {
+ "label": "輸入流功能",
+ "description": "定義該影片流的功能。"
+ },
+ "global_args": {
+ "label": "FFmpeg 全域性引數",
+ "description": "該輸入影片流使用的 FFmpeg 全域性通用引數。"
+ },
+ "hwaccel_args": {
+ "label": "硬體加速引數",
+ "description": "該輸入影片流的硬體加速引數。"
+ },
+ "input_args": {
+ "label": "輸入引數",
+ "description": "該影片流特定的輸入引數。"
+ }
+ }
+ },
+ "live": {
+ "label": "即時監控觀看",
+ "streams": {
+ "label": "即時監控流名稱",
+ "description": "配置的流名稱到用於即時監控播放的 restream/go2rtc 名稱的對映。"
+ },
+ "height": {
+ "label": "即時監控高度",
+ "description": "在網頁頁面中渲染 jsmpeg 即時監控流的高度(像素);必須小於等於偵測流高度。"
+ },
+ "quality": {
+ "label": "即時監控品質",
+ "description": "jsmpeg 流的編碼品質(1 最高,31 最低)。"
+ },
+ "description": "用於控制即時流選擇、解析度和品質的網頁頁面設定。"
+ },
+ "motion": {
+ "label": "畫面變動偵測",
+ "enabled": {
+ "label": "開啟畫面變動偵測",
+ "description": "開啟或關閉此攝影機的畫面變動偵測。"
+ },
+ "threshold": {
+ "label": "畫面變動閾值",
+ "description": "畫面變動偵測器使用的像素差異閾值;數值越高靈敏度越低(範圍 1-255)。"
+ },
+ "lightning_threshold": {
+ "label": "閃電閾值",
+ "description": "用於偵測和忽略短暫閃電閃爍的閾值(數值越低越敏感,範圍 0.3 到 1.0)。這不會完全阻止畫面變動偵測;只是當超過閾值時偵測器會停止分析額外的幀。在此類事件期間仍會建立基於畫面變動的錄影。"
+ },
+ "skip_motion_threshold": {
+ "label": "跳過畫面變動閾值",
+ "description": "如果單幀中畫面變化超過此比例,偵測器將判定為無畫面變動並立即重新校準。這可以節省 CPU 並減少閃電、風暴等情況下的誤報,但也可能會錯過真正的事件,如 PTZ 攝影機自動追蹤目標。你需要權衡取捨:是否犧牲少量錄製片段,換取更少無效影片與更低的誤檢。保持為空即可關閉該功能。"
+ },
+ "improve_contrast": {
+ "label": "改善對比度",
+ "description": "在畫面變動分析之前對幀應用對比度改善以幫助偵測。"
+ },
+ "contour_area": {
+ "label": "輪廓區域",
+ "description": "畫面變動輪廓被計入所需的最小輪廓區域(像素)。"
+ },
+ "delta_alpha": {
+ "label": "Delta alpha",
+ "description": "用於畫面變動計算的幀差異中使用的 alpha 混合因子。"
+ },
+ "frame_alpha": {
+ "label": "畫面 alpha 通道",
+ "description": "畫面變動預處理時混合畫面所使用的 alpha 值。"
+ },
+ "frame_height": {
+ "label": "畫面高度",
+ "description": "計算畫面變動時縮放畫面的高度(像素)。"
+ },
+ "mask": {
+ "label": "遮罩座標",
+ "description": "定義用於包含/排除區域的畫面變動遮罩多邊形的有序 x,y 座標。"
+ },
+ "mqtt_off_delay": {
+ "label": "MQTT 關閉延遲",
+ "description": "在釋出 MQTT 'off' 狀態之前,最後一次畫面變動後等待的秒數。"
+ },
+ "enabled_in_config": {
+ "label": "原始畫面變動狀態",
+ "description": "指示原始靜態配置中是否啟用了畫面變動偵測。"
+ },
+ "raw_mask": {
+ "label": "原始遮罩"
+ },
+ "description": "此攝影機的預設畫面變動偵測設定。"
+ },
+ "objects": {
+ "label": "目標",
+ "description": "目標追蹤預設設定,包括要追蹤的標籤和按目標的過濾器。",
+ "track": {
+ "label": "要追蹤的目標",
+ "description": "此攝影機要追蹤的目標標籤清單。"
+ },
+ "filters": {
+ "label": "目標過濾器",
+ "description": "應用於偵測到的目標以減少誤報的過濾器(區域、比例、置信度)。",
+ "min_area": {
+ "label": "最小目標區域",
+ "description": "此目標型別所需的最小邊界框區域(像素或百分比)。可以是像素(整數)或百分比(0.000001 到 0.99 之間的浮點數)。"
+ },
+ "max_area": {
+ "label": "最大目標區域",
+ "description": "此目標型別允許的最大邊界框區域(像素或百分比)。可以是像素(整數)或百分比(0.000001 到 0.99 之間的浮點數)。"
+ },
+ "min_ratio": {
+ "label": "最小縱橫比",
+ "description": "邊界框所需的最小寬高比。"
+ },
+ "max_ratio": {
+ "label": "最大縱橫比",
+ "description": "邊界框允許的最大寬高比。"
+ },
+ "threshold": {
+ "label": "置信度閾值",
+ "description": "目標被視為真正陽性所需的平均偵測置信度閾值。"
+ },
+ "min_score": {
+ "label": "最小置信度",
+ "description": "目標被計入所需的最小單幀偵測置信度。"
+ },
+ "mask": {
+ "label": "過濾器遮罩",
+ "description": "定義此過濾器在幀內應用位置的多邊形座標。"
+ },
+ "raw_mask": {
+ "label": "原始遮罩"
+ }
+ },
+ "mask": {
+ "label": "目標遮罩",
+ "description": "用於防止在指定區域進行目標偵測的遮罩多邊形。"
+ },
+ "raw_mask": {
+ "label": "原始遮罩"
+ },
+ "genai": {
+ "label": "生成式 AI 目標配置",
+ "description": "用於傳送畫面給生成式 AI 進行生成和描述追蹤目標的選項。",
+ "enabled": {
+ "label": "開啟生成式 AI",
+ "description": "預設開啟生成式 AI 生成追蹤目標的描述。"
+ },
+ "use_snapshot": {
+ "label": "使用快照",
+ "description": "使用目標快照而不是縮圖給生成式 AI 進行描述生成。"
+ },
+ "prompt": {
+ "label": "字幕提示",
+ "description": "使用生成式 AI 生成描述時使用的預設提示模板。"
+ },
+ "object_prompts": {
+ "label": "目標提示",
+ "description": "按目標設定提示詞,讓生成式 AI 對不同標籤的輸出進行定製。"
+ },
+ "objects": {
+ "label": "生成式 AI 目標",
+ "description": "預設傳送給生成式 AI 的目標標籤清單。"
+ },
+ "required_zones": {
+ "label": "必需區域",
+ "description": "目標必須進入這些區域,才會觸發生成式 AI 描述生成。"
+ },
+ "debug_save_thumbnails": {
+ "label": "儲存縮圖",
+ "description": "儲存傳送給生成式 AI 的縮圖用於除錯和審閱。"
+ },
+ "send_triggers": {
+ "label": "生成式 AI 觸發器",
+ "description": "定義畫面幀應在何時傳送給生成式 AI(如偵測結束時、更新後等)。",
+ "tracked_object_end": {
+ "label": "結束時傳送",
+ "description": "目標追蹤結束時向生成式 AI 傳送請求。"
+ },
+ "after_significant_updates": {
+ "label": "生成式 AI 提前觸發",
+ "description": "在追蹤目標發生指定次數的重要變化後,向生成式 AI 傳送請求。"
+ }
+ },
+ "enabled_in_config": {
+ "label": "原配置生成式 AI 狀態",
+ "description": "表示在原始靜態配置中是否已啟用生成式 AI。"
+ }
+ }
+ },
+ "record": {
+ "label": "錄影",
+ "enabled": {
+ "label": "開啟錄影",
+ "description": "開啟或關閉此攝影機的錄影。"
+ },
+ "expire_interval": {
+ "label": "錄影清理間隔",
+ "description": "清理過期錄影片段的間隔分鐘數。"
+ },
+ "continuous": {
+ "label": "持續保留",
+ "description": "無論是否有追蹤目標或動作,保留錄影的天數。如果只想保留警報和偵測的錄影,請設定為 0。",
+ "days": {
+ "label": "保留天數",
+ "description": "保留錄影的天數。"
+ }
+ },
+ "motion": {
+ "label": "動作保留",
+ "description": "無論是否有追蹤目標,由動作觸發的錄影保留天數。如果只想保留警報和偵測的錄影,請設定為 0。",
+ "days": {
+ "label": "保留天數",
+ "description": "保留錄影的天數。"
+ }
+ },
+ "detections": {
+ "label": "偵測保留",
+ "description": "偵測事件的錄影保留設定,包括前後捕獲時長。",
+ "pre_capture": {
+ "label": "前捕獲秒數",
+ "description": "偵測事件之前包含在錄影中的秒數。"
+ },
+ "post_capture": {
+ "label": "後捕獲秒數",
+ "description": "偵測事件之後包含在錄影中的秒數。"
+ },
+ "retain": {
+ "label": "事件保留",
+ "description": "偵測事件錄影的保留設定。",
+ "days": {
+ "label": "保留天數",
+ "description": "保留偵測事件錄影的天數。"
+ },
+ "mode": {
+ "label": "保留模式",
+ "description": "保留模式:all(儲存所有片段)、motion(儲存有動作的片段)或 active_objects(儲存有活動目標的片段)。"
+ }
+ }
+ },
+ "alerts": {
+ "label": "警報保留",
+ "description": "警報事件的錄影保留設定,包括前後捕獲時長。",
+ "pre_capture": {
+ "label": "前捕獲秒數",
+ "description": "偵測事件之前包含在錄影中的秒數。"
+ },
+ "post_capture": {
+ "label": "後捕獲秒數",
+ "description": "偵測事件之後包含在錄影中的秒數。"
+ },
+ "retain": {
+ "label": "事件保留",
+ "description": "偵測事件錄影的保留設定。",
+ "days": {
+ "label": "保留天數",
+ "description": "保留偵測事件錄影的天數。"
+ },
+ "mode": {
+ "label": "保留模式",
+ "description": "保留模式:all(儲存所有片段)、motion(儲存有動作的片段)或 active_objects(儲存有活動目標的片段)。"
+ }
+ }
+ },
+ "export": {
+ "label": "匯出配置",
+ "description": "匯出錄影時使用的設定,如延時攝影和硬體加速。",
+ "hwaccel_args": {
+ "label": "匯出硬體加速引數",
+ "description": "用於匯出/轉碼操作的硬體加速引數。"
+ },
+ "max_concurrent": {
+ "label": "最大併發匯出數",
+ "description": "同時可處理的最大匯出任務數量。"
+ }
+ },
+ "preview": {
+ "label": "預覽配置",
+ "description": "控制介面中顯示的錄影預覽品質的設定。",
+ "quality": {
+ "label": "預覽品質",
+ "description": "預覽品質級別(very_low、low、medium、high、very_high)。"
+ }
+ },
+ "enabled_in_config": {
+ "label": "原始錄影狀態",
+ "description": "指示原始靜態配置中是否啟用了錄影。"
+ },
+ "description": "此攝影機的錄影和保留設定。"
+ },
+ "review": {
+ "label": "審閱",
+ "alerts": {
+ "label": "警報配置",
+ "description": "哪些追蹤目標生成警報以及如何保留警報的設定。",
+ "enabled": {
+ "label": "開啟警報",
+ "description": "開啟或關閉此攝影機的警報生成。"
+ },
+ "labels": {
+ "label": "警報標籤",
+ "description": "符合警報條件的目標標籤清單(例如:car、person)。"
+ },
+ "required_zones": {
+ "label": "必需區域",
+ "description": "目標必須進入才能被視為警報的區域;留空則允許任何區域。"
+ },
+ "enabled_in_config": {
+ "label": "原始警報狀態",
+ "description": "追蹤原始靜態配置中是否啟用了警報。"
+ },
+ "cutoff_time": {
+ "label": "警報截止時間",
+ "description": "在沒有引起警報的活動後等待多少秒後截止警報。"
+ }
+ },
+ "detections": {
+ "label": "偵測配置",
+ "description": "用於設定哪些追蹤目標會生成偵測記錄(非警報類),以及偵測記錄的保留方式。",
+ "enabled": {
+ "label": "開啟偵測",
+ "description": "開啟或關閉此攝影機的偵測事件。"
+ },
+ "labels": {
+ "label": "偵測標籤",
+ "description": "符合偵測事件條件的目標標籤清單。"
+ },
+ "required_zones": {
+ "label": "必需區域",
+ "description": "目標必須進入才能被視為偵測的區域;留空則允許任何區域。"
+ },
+ "cutoff_time": {
+ "label": "偵測截止時間",
+ "description": "在沒有引起偵測的活動後等待多少秒後截止偵測。"
+ },
+ "enabled_in_config": {
+ "label": "原始偵測狀態",
+ "description": "追蹤原始靜態配置中是否啟用了偵測。"
+ }
+ },
+ "genai": {
+ "label": "生成式 AI 配置",
+ "description": "控制使用生成式 AI 為審閱項生成描述和摘要。",
+ "enabled": {
+ "label": "開啟生成式 AI 描述",
+ "description": "為審閱項開啟或關閉使用生成式 AI 生成描述和摘要。"
+ },
+ "alerts": {
+ "label": "為警報開啟生成式 AI",
+ "description": "使用生成式 AI 為警報項生成描述。"
+ },
+ "detections": {
+ "label": "為偵測開啟生成式 AI",
+ "description": "使用生成式 AI 為偵測項生成描述。"
+ },
+ "image_source": {
+ "label": "審閱影像來源",
+ "description": "傳送給生成式 AI 的畫面來源('preview' 或 'recordings');'recordings' 使用更高品質的畫面幀,但會消耗更多的 token。"
+ },
+ "additional_concerns": {
+ "label": "額外關注事項",
+ "description": "生成式 AI 在分析此攝影機的監控行為時,需要額外注意的事項或說明清單。"
+ },
+ "debug_save_thumbnails": {
+ "label": "儲存縮圖",
+ "description": "儲存傳送給生成式 AI 提供商的縮圖用於除錯和審閱。"
+ },
+ "enabled_in_config": {
+ "label": "原配置生成式 AI 狀態",
+ "description": "記錄在靜態配置中最初是否已啟用生成式 AI 審閱功能。"
+ },
+ "preferred_language": {
+ "label": "首選語言",
+ "description": "向生成式 AI 提供商請求生成回應的首選語言。"
+ },
+ "activity_context_prompt": {
+ "label": "活動上下文提示",
+ "description": "自訂提示詞,用於說明可疑行為與非可疑行為的界定,為生成式 AI 生成摘要提供上下文依據。"
+ }
+ },
+ "description": "控制此攝影機的警報、偵測和生成式 AI 審閱總結的設定,這些設定會被介面與儲存功能使用。"
+ },
+ "snapshots": {
+ "label": "快照",
+ "enabled": {
+ "label": "開啟快照",
+ "description": "開啟或關閉此攝影機的快照儲存。"
+ },
+ "timestamp": {
+ "label": "時間戳疊加",
+ "description": "在 API 生成的快照上疊加時間戳。"
+ },
+ "bounding_box": {
+ "label": "邊界框疊加",
+ "description": "在 API 生成的快照上繪製追蹤目標的邊界框。"
+ },
+ "crop": {
+ "label": "裁剪快照",
+ "description": "在 API 生成的快照裁剪到偵測到的目標邊界框。"
+ },
+ "required_zones": {
+ "label": "必需區域",
+ "description": "目標必須進入才能儲存快照的區域。"
+ },
+ "height": {
+ "label": "快照高度",
+ "description": "將 API 生成的快照調整到的目標高度(像素);留空則保持原始大小。"
+ },
+ "retain": {
+ "label": "快照保留",
+ "description": "快照的保留設定,包括預設天數和按目標覆蓋。",
+ "default": {
+ "label": "預設保留",
+ "description": "保留快照的預設天數。"
+ },
+ "mode": {
+ "label": "保留模式",
+ "description": "保留模式:all(儲存所有片段)、motion(儲存有動作的片段)或 active_objects(儲存有活動目標的片段)。"
+ },
+ "objects": {
+ "label": "目標保留",
+ "description": "按目標覆蓋的快照保留天數。"
+ }
+ },
+ "quality": {
+ "label": "快照品質",
+ "description": "儲存快照的編碼品質(0-100)。"
+ },
+ "description": "此攝影機的追蹤目標 API 快照設定。"
+ },
+ "timestamp_style": {
+ "label": "時間戳樣式",
+ "position": {
+ "label": "時間戳位置",
+ "description": "時間戳在影像上的位置(tl/tr/bl/br)。"
+ },
+ "format": {
+ "label": "時間戳格式",
+ "description": "用於時間戳的日期時間格式字串(Python 日期時間格式程式碼)。"
+ },
+ "color": {
+ "label": "時間戳顏色",
+ "description": "時間戳文字的 RGB 顏色值(所有值 0-255)。",
+ "red": {
+ "label": "紅色",
+ "description": "時間戳顏色的紅色分量(0-255)。"
+ },
+ "green": {
+ "label": "綠色",
+ "description": "時間戳顏色的綠色分量(0-255)。"
+ },
+ "blue": {
+ "label": "藍色",
+ "description": "時間戳顏色的藍色分量(0-255)。"
+ }
+ },
+ "thickness": {
+ "label": "時間戳粗細",
+ "description": "時間戳文字的線條粗細。"
+ },
+ "effect": {
+ "label": "時間戳效果",
+ "description": "時間戳文字的視覺效果(none、solid、shadow)。"
+ },
+ "description": "應用於錄影和快照的即時監控流中時間戳的樣式選項。"
+ },
+ "audio_transcription": {
+ "label": "音訊轉錄",
+ "description": "用於事件和即時字幕的即時和語音音訊轉錄設定。",
+ "live_enabled": {
+ "label": "即時監控轉寫",
+ "description": "在接收到音訊時開啟即時監控持續轉寫。"
+ },
+ "enabled": {
+ "label": "啟用語音轉文字",
+ "description": "開啟或關閉手動觸發的音訊事件轉寫。"
+ },
+ "enabled_in_config": {
+ "label": "原始轉寫狀態"
+ }
+ },
+ "semantic_search": {
+ "label": "語意搜尋",
+ "triggers": {
+ "label": "觸發器",
+ "description": "攝影機特定語意搜尋觸發器的操作和匹配條件。",
+ "friendly_name": {
+ "label": "友好名稱",
+ "description": "在 UI 中為此觸發器顯示的可選友好名稱。"
+ },
+ "enabled": {
+ "label": "開啟此觸發器",
+ "description": "啟用或停用此語意搜尋觸發器。"
+ },
+ "type": {
+ "label": "觸發器型別",
+ "description": "觸發器型別:'thumbnail'(與影像匹配)或 'description'(與文字匹配)。"
+ },
+ "data": {
+ "label": "觸發器內容",
+ "description": "要與追蹤目標匹配的文字短語或縮圖 ID。"
+ },
+ "threshold": {
+ "label": "觸發器閾值",
+ "description": "啟用此觸發器所需的最小相似度分數(0-1)。"
+ },
+ "actions": {
+ "label": "觸發器操作",
+ "description": "觸發器匹配時要執行的操作清單(通知、sub_label、屬性)。"
+ }
+ },
+ "description": "語意搜尋設定,用於構建和查詢目標嵌入以查詢相似項目。"
+ },
+ "face_recognition": {
+ "label": "人臉辨識",
+ "enabled": {
+ "label": "開啟人臉辨識",
+ "description": "開啟或關閉人臉辨識。"
+ },
+ "min_area": {
+ "label": "最小人臉區域",
+ "description": "需要嘗試進行人臉辨識的人臉偵測框最小大小(像素)。"
+ },
+ "description": "該攝影機的人臉偵測與辨識設定。"
+ },
+ "lpr": {
+ "label": "車牌辨識",
+ "description": "車牌辨識設定,包括偵測閾值、格式化和已知車牌。",
+ "enabled": {
+ "label": "開啟車牌辨識",
+ "description": "在此攝影機上啟用或停用車牌辨識。"
+ },
+ "min_area": {
+ "label": "最小車牌區域",
+ "description": "嘗試辨識所需的最小車牌區域(像素)。"
+ },
+ "enhancement": {
+ "label": "增強級別",
+ "description": "在 OCR 之前應用於車牌裁剪的增強級別(0-10);較高的值可能不總是改善結果,5 以上的級別可能僅適用於夜間車牌,應謹慎使用。"
+ },
+ "expire_time": {
+ "label": "過期秒數",
+ "description": "未見到的車牌從追蹤器中過期的時間(秒)(僅適用於專用 LPR 攝影機)。"
+ }
+ },
+ "profiles": {
+ "label": "設定檔",
+ "description": "可在執行時切換指定命名的設定檔,支援區域性覆蓋引數。"
+ },
+ "onvif": {
+ "label": "ONVIF",
+ "description": "此攝影機的 ONVIF 連線和 PTZ 自動追蹤設定。",
+ "host": {
+ "label": "ONVIF 主機",
+ "description": "此攝影機 ONVIF 服務的主機(和可選協議)。"
+ },
+ "port": {
+ "label": "ONVIF 埠",
+ "description": "ONVIF 服務的埠號。"
+ },
+ "user": {
+ "label": "ONVIF 使用者名稱",
+ "description": "ONVIF 身份驗證的使用者名稱;某些裝置需要管理員使用者才能使用 ONVIF。"
+ },
+ "password": {
+ "label": "ONVIF 密碼",
+ "description": "ONVIF 身份驗證的密碼。"
+ },
+ "tls_insecure": {
+ "label": "停用 TLS 驗證",
+ "description": "跳過 TLS 驗證並停用 ONVIF 的摘要認證(不安全;僅用於安全網路)。"
+ },
+ "profile": {
+ "label": "ONVIF 設定檔",
+ "description": "用於 PTZ 控制的指定 ONVIF 媒體配置,將透過 Token 或名稱匹配。如果未手動指定,將自動選擇第一個包含有效 PTZ 配置的媒體配置。"
+ },
+ "autotracking": {
+ "label": "自動追蹤",
+ "description": "使用 PTZ 攝影機移動自動追蹤移動目標並使其保持在畫面中心。",
+ "enabled": {
+ "label": "開啟自動追蹤",
+ "description": "啟用或停用偵測目標的自動 PTZ 攝影機追蹤。"
+ },
+ "calibrate_on_startup": {
+ "label": "啟動時校準",
+ "description": "在啟動時測量 PTZ 電機速度以提高追蹤精度。Frigate 將在校準後用 movement_weights 更新配置。"
+ },
+ "zooming": {
+ "label": "變焦模式",
+ "description": "控制變焦行為:disabled(僅平移/傾斜)、absolute(最相容)或 relative(同時平移/傾斜/變焦)。"
+ },
+ "zoom_factor": {
+ "label": "變焦因子",
+ "description": "控制追蹤目標的變焦級別。數值越低保持更多場景可見;數值越高放大更近但可能丟失追蹤。數值範圍 0.1 到 0.75。"
+ },
+ "track": {
+ "label": "追蹤目標",
+ "description": "應觸發自動追蹤的目標型別清單。"
+ },
+ "required_zones": {
+ "label": "必需區域",
+ "description": "目標必須進入這些區域之一才能開始自動追蹤。"
+ },
+ "return_preset": {
+ "label": "返回預設",
+ "description": "追蹤結束後返回的攝影機韌體中配置的 ONVIF 預設名稱。"
+ },
+ "timeout": {
+ "label": "返回超時",
+ "description": "失去追蹤後等待多少秒後將攝影機返回到預設位置。"
+ },
+ "movement_weights": {
+ "label": "移動權重",
+ "description": "由攝影機校準自動生成的校準值。請勿手動修改。"
+ },
+ "enabled_in_config": {
+ "label": "原始自動追蹤狀態",
+ "description": "用於追蹤配置中是否啟用自動追蹤的內部欄位。"
+ }
+ },
+ "ignore_time_mismatch": {
+ "label": "忽略時間不匹配",
+ "description": "忽略 ONVIF 通訊中攝影機和 Frigate 伺服器之間的時間同步差異。"
+ }
+ },
+ "best_image_timeout": {
+ "label": "最佳影像超時",
+ "description": "等待具有最高置信度分數的影像的時間。"
+ },
+ "type": {
+ "label": "攝影機型別",
+ "description": "攝影機型別"
+ },
+ "ui": {
+ "label": "攝影機介面",
+ "description": "此攝影機在頁面中的顯示順序和可見性。顯示順序僅影響預設儀表板。如需更精細的控制,請使用“攝影機組”。",
+ "order": {
+ "label": "UI 順序",
+ "description": "用於在頁面中排序攝影機的順序(只會影響預設儀表板和清單);數值越大則在越後面。"
+ },
+ "dashboard": {
+ "label": "在 UI 中顯示",
+ "description": "切換此攝影機在 Frigate 頁面的所有位置是否可見。停用此項將需要手動編輯配置才能在頁面中再次檢視此攝影機。"
+ },
+ "review": {
+ "label": "在Review中顯示",
+ "description": "切換此攝影機在Rewiew中是否可見 (包含Review 頁面、該攝影機的過濾器、移動檢視、歷史畫面)。"
+ }
+ },
+ "webui_url": {
+ "label": "攝影機 URL",
+ "description": "從系統頁面直接存取攝影機管理後臺的 URL"
+ },
+ "zones": {
+ "label": "區域",
+ "description": "區域允許您定義幀的特定區域,以便確定目標是否在特定區域內。",
+ "friendly_name": {
+ "label": "區域名稱",
+ "description": "區域的友好名稱,顯示在 Frigate UI 中。如果未設定,將使用區域名稱的格式化版本。"
+ },
+ "enabled": {
+ "label": "開啟",
+ "description": "開啟或關閉此區域。停用的區域在執行時將被忽略。"
+ },
+ "enabled_in_config": {
+ "label": "保持區域原始狀態的跟蹤。"
+ },
+ "filters": {
+ "label": "區域過濾器",
+ "description": "應用於此區域內目標的過濾器。用於減少誤報或限制哪些目標被認為存在於區域內。",
+ "min_area": {
+ "label": "最小目標區域",
+ "description": "此目標型別所需的最小邊界框區域(像素或百分比)。可以是像素(整數)或百分比(0.000001 到 0.99 之間的浮點數)。"
+ },
+ "max_area": {
+ "label": "最大目標區域",
+ "description": "此目標型別允許的最大邊界框區域(像素或百分比)。可以是像素(整數)或百分比(0.000001 到 0.99 之間的浮點數)。"
+ },
+ "min_ratio": {
+ "label": "最小縱橫比",
+ "description": "邊界框所需的最小寬高比。"
+ },
+ "max_ratio": {
+ "label": "最大縱橫比",
+ "description": "邊界框允許的最大寬高比。"
+ },
+ "threshold": {
+ "label": "置信度閾值",
+ "description": "目標被視為真正陽性所需的平均偵測置信度閾值。"
+ },
+ "min_score": {
+ "label": "最小置信度",
+ "description": "目標被計入所需的最小單幀偵測置信度。"
+ },
+ "mask": {
+ "label": "過濾器遮罩",
+ "description": "定義此過濾器在幀內應用位置的多邊形座標。"
+ },
+ "raw_mask": {
+ "label": "原始遮罩"
+ }
+ },
+ "coordinates": {
+ "label": "座標",
+ "description": "定義區域區域的多邊形座標。可以是逗號分隔的字串或座標字串清單。座標應該是相對的(0-1)或絕對的(傳統)。"
+ },
+ "distances": {
+ "label": "真實世界距離",
+ "description": "區域四邊形每邊的可選真實世界距離,用於速度或距離計算。如果設定,必須恰好有 4 個值。"
+ },
+ "inertia": {
+ "label": "慣性幀數",
+ "description": "目標必須在區域內被連續偵測多少幀才能被認為存在。有助於過濾掉短暫偵測。"
+ },
+ "loitering_time": {
+ "label": "徘徊秒數",
+ "description": "目標必須在區域內停留多少秒才能被視為徘徊。設定為 0 可停用徘徊偵測。"
+ },
+ "speed_threshold": {
+ "label": "最小速度",
+ "description": "目標被認為存在於區域所需的最小速度(如果設定了距離,則為真實世界單位)。用於基於速度的區域觸發器。"
+ },
+ "objects": {
+ "label": "觸發目標",
+ "description": "可以觸發此區域的目標型別清單(來自標籤對映)。可以是字串或字串清單。如果為空,則考慮所有目標。"
+ }
+ },
+ "enabled_in_config": {
+ "label": "原始攝影機狀態",
+ "description": "保持攝影機的原始狀態跟蹤。"
}
}
diff --git a/web/public/locales/zh-Hant/config/global.json b/web/public/locales/zh-Hant/config/global.json
index 0f254ab830..a71d8c6210 100644
--- a/web/public/locales/zh-Hant/config/global.json
+++ b/web/public/locales/zh-Hant/config/global.json
@@ -2,7 +2,8 @@
"audio": {
"label": "音訊事件",
"enabled": {
- "label": "啟用音訊偵測"
+ "label": "啟用音訊偵測",
+ "description": "為所有攝影機啟用或停用音訊事件偵測;可按攝影機覆蓋。"
},
"max_not_heard": {
"label": "結束逾時",
@@ -15,6 +16,1624 @@
"listen": {
"label": "監聽的音訊類型",
"description": "要偵測的音訊事件類型清單(例如:狗吠、火警、尖叫、說話、大叫)。"
+ },
+ "description": "所有攝影機的基於音訊的事件偵測設定;可按攝影機覆蓋。",
+ "filters": {
+ "label": "音訊過濾器",
+ "description": "按音訊型別的過濾器設定,如用於減少誤報的置信度閾值。",
+ "threshold": {
+ "label": "最低音訊置信度",
+ "description": "音訊事件被計入的最低置信度閾值。"
+ }
+ },
+ "enabled_in_config": {
+ "label": "原始音訊狀態",
+ "description": "指示原始靜態設定檔中是否開啟了音訊偵測。"
+ },
+ "num_threads": {
+ "label": "偵測執行緒",
+ "description": "用於音訊偵測處理的執行緒數量。"
+ }
+ },
+ "version": {
+ "label": "當前配置版本",
+ "description": "用於標識當前生效配置的版本號(數字或字串均可),幫助辨識配置遷移或格式是否發生變更。"
+ },
+ "safe_mode": {
+ "label": "安全模式",
+ "description": "開啟後,Frigate 將以安全模式啟動,將會關閉部分功能,以便排查問題。"
+ },
+ "environment_vars": {
+ "label": "環境變數",
+ "description": "用於在 Home Assistant OS 中為 Frigate 程序設定的環境變數。非 HAOS 使用者不能使用該配置項,而必須使用 Docker 的環境變數配置。"
+ },
+ "logger": {
+ "label": "日誌",
+ "description": "控制預設日誌詳細程度,以及各元件的日誌級別覆蓋。",
+ "default": {
+ "label": "日誌等級",
+ "description": "預設全域性日誌詳細程度(除錯、資訊、警告、錯誤)。"
+ },
+ "logs": {
+ "label": "單程序日誌級別",
+ "description": "按元件覆蓋日誌級別配置,用於提高或降低特定模組的日誌詳細程度。"
+ }
+ },
+ "auth": {
+ "label": "身份驗證",
+ "description": "身份驗證和工作階段相關設定,包括 Cookie 和速率限制選項。",
+ "enabled": {
+ "label": "開啟身份驗證",
+ "description": "為 Frigate 頁面開啟原生身份驗證。"
+ },
+ "reset_admin_password": {
+ "label": "重設管理員密碼",
+ "description": "開啟後,啟動時將重設管理員使用者密碼,並在日誌中列印新密碼。"
+ },
+ "cookie_name": {
+ "label": "JWT Cookie 名稱",
+ "description": "用於儲存原生身份驗證 JWT 令牌的 Cookie 名稱。"
+ },
+ "cookie_secure": {
+ "label": "安全 Cookie 標誌",
+ "description": "在身份驗證 Cookie 上設定安全標誌;使用 TLS 時應啟用此選項。"
+ },
+ "session_length": {
+ "label": "工作階段時長",
+ "description": "基於 JWT 的工作階段持續時間(秒)。"
+ },
+ "refresh_time": {
+ "label": "工作階段重新整理視窗",
+ "description": "當工作階段距離過期時間在此秒數範圍內時,將工作階段重新整理回完整時長。"
+ },
+ "failed_login_rate_limit": {
+ "label": "登入失敗限制",
+ "description": "用於限制登入失敗嘗試次數的規則,以減少暴力破解攻擊。"
+ },
+ "trusted_proxies": {
+ "label": "受信任的代理",
+ "description": "用於確定客戶端 IP 以進行速率限制的受信任代理 IP 清單。"
+ },
+ "hash_iterations": {
+ "label": "雜湊迭代次數",
+ "description": "對使用者密碼進行雜湊處理時使用的 PBKDF2-SHA256 迭代次數。"
+ },
+ "roles": {
+ "label": "權限組對映",
+ "description": "將權限組對映到攝影機清單。空清單表示該權限組可以存取所有攝影機。"
+ },
+ "admin_first_time_login": {
+ "label": "管理員首次登入標誌",
+ "description": "啟用後,UI 可能會在登入頁面顯示幫助連結,告知使用者如何在管理員密碼重設後登入。 "
+ }
+ },
+ "database": {
+ "label": "資料庫",
+ "description": "Frigate 用於儲存追蹤目標和錄影元資料的 SQLite 資料庫設定。",
+ "path": {
+ "label": "資料庫路徑",
+ "description": "Frigate SQLite 資料庫檔案的儲存路徑。"
+ }
+ },
+ "go2rtc": {
+ "label": "go2rtc",
+ "description": "整合的 go2rtc 轉發服務設定,用於即時監控流轉發和轉碼。"
+ },
+ "mqtt": {
+ "label": "MQTT",
+ "description": "連線到 MQTT 代理併發布遙測資料、快照和事件詳情的設定。",
+ "enabled": {
+ "label": "開啟 MQTT",
+ "description": "啟用或停用 MQTT 整合,用於狀態、事件和快照。"
+ },
+ "host": {
+ "label": "MQTT 主機",
+ "description": "MQTT 代理的主機名或 IP 地址。"
+ },
+ "port": {
+ "label": "MQTT 埠",
+ "description": "MQTT 代理的埠(普通 MQTT 通常為 1883)。"
+ },
+ "topic_prefix": {
+ "label": "主題字首",
+ "description": "所有 Frigate 主題的 MQTT 主題字首;如果執行多個例項,必須唯一。"
+ },
+ "client_id": {
+ "label": "客戶端 ID",
+ "description": "連線到 MQTT 代理時使用的客戶端辨識符號;每個例項應該唯一。"
+ },
+ "stats_interval": {
+ "label": "統計資訊間隔",
+ "description": "向 MQTT 釋出系統和攝影機統計資訊的時間間隔(秒)。"
+ },
+ "user": {
+ "label": "MQTT 使用者名稱",
+ "description": "可選的 MQTT 使用者名稱;可以透過環境變數或金鑰提供。"
+ },
+ "password": {
+ "label": "MQTT 密碼",
+ "description": "可選的 MQTT 密碼;可以透過環境變數或金鑰提供。"
+ },
+ "tls_ca_certs": {
+ "label": "TLS CA 證書",
+ "description": "用於 TLS 連線到代理的 CA 證書路徑(用於自簽名證書)。"
+ },
+ "tls_client_cert": {
+ "label": "客戶端證書",
+ "description": "TLS 雙向認證的客戶端證書路徑;使用客戶端證書時不要設定使用者名稱/密碼。"
+ },
+ "tls_client_key": {
+ "label": "客戶端金鑰",
+ "description": "客戶端證書的私鑰路徑。"
+ },
+ "tls_insecure": {
+ "label": "TLS 不安全連線",
+ "description": "透過跳過主機名驗證允許不安全的 TLS 連線(不推薦)。"
+ },
+ "qos": {
+ "label": "MQTT QoS",
+ "description": "MQTT 釋出/訂閱的服務品質級別(0、1 或 2)。"
+ }
+ },
+ "notifications": {
+ "label": "通知",
+ "description": "為所有攝影機啟用和控制通知的設定;可按攝影機覆蓋。",
+ "enabled": {
+ "label": "開啟通知",
+ "description": "為所有攝影機啟用或停用通知;可按攝影機覆蓋。"
+ },
+ "email": {
+ "label": "通知郵箱",
+ "description": "用於推送通知或某些通知提供商要求的郵箱地址。"
+ },
+ "cooldown": {
+ "label": "冷卻時間",
+ "description": "通知之間的冷卻時間(秒),以避免向收件人傳送垃圾資訊。"
+ },
+ "enabled_in_config": {
+ "label": "原始通知狀態",
+ "description": "指示原始靜態配置中是否啟用了通知。"
+ }
+ },
+ "networking": {
+ "label": "網路",
+ "description": "網路相關設定,如 Frigate 端點的 IPv6 啟用。",
+ "ipv6": {
+ "label": "IPv6 配置",
+ "description": "Frigate 網路服務的 IPv6 特定設定。",
+ "enabled": {
+ "label": "開啟 IPv6",
+ "description": "在適用的情況下為 Frigate 服務(API 和 UI)啟用 IPv6 支援。"
+ }
+ },
+ "listen": {
+ "label": "監聽埠配置",
+ "description": "內部和外部監聽埠的配置。此選項適用於高階使用者。對於大多數用例,建議在 Docker compose 檔案的 ports 部分進行更改。",
+ "internal": {
+ "label": "內部埠",
+ "description": "Frigate 的內部監聽埠(預設 5000)。"
+ },
+ "external": {
+ "label": "外部埠",
+ "description": "Frigate 的外部監聽埠(預設 8971)。"
+ }
+ }
+ },
+ "proxy": {
+ "label": "代理",
+ "description": "用於將 Frigate 整合到傳遞已認證使用者頭的反向代理後面的設定。",
+ "header_map": {
+ "label": "請求頭對映",
+ "description": "將傳入的代理請求頭對映到 Frigate 使用者和權限組欄位,用於基於代理的身份驗證。",
+ "user": {
+ "label": "使用者請求頭",
+ "description": "包含上游代理提供的已認證使用者名稱的請求頭。"
+ },
+ "role": {
+ "label": "權限組請求頭",
+ "description": "包含來自上游代理的已認證使用者權限組或使用者組的請求頭。"
+ },
+ "role_map": {
+ "label": "權限組對映",
+ "description": "將上游組值對映到 Frigate 權限組(例如將管理員組對映到管理員權限組)。"
+ }
+ },
+ "logout_url": {
+ "label": "登出 URL",
+ "description": "透過代理登出時重定向使用者的 URL。"
+ },
+ "auth_secret": {
+ "label": "代理金鑰",
+ "description": "與 X-Proxy-Secret 請求頭進行比對的可選金鑰,用於驗證受信任的代理。"
+ },
+ "default_role": {
+ "label": "預設權限組",
+ "description": "當沒有權限組對映適用時分配給代理認證使用者的預設權限組(admin 或 viewer)。"
+ },
+ "separator": {
+ "label": "分隔符",
+ "description": "用於分割代理請求頭中多個值的字元。"
+ }
+ },
+ "telemetry": {
+ "label": "遙測",
+ "description": "系統遙測和統計選項,包括 GPU 和網路頻寬監控。",
+ "network_interfaces": {
+ "label": "網路介面",
+ "description": "要監控頻寬統計資訊的網路介面名稱字首清單。"
+ },
+ "stats": {
+ "label": "系統統計",
+ "description": "用於啟用/停用各種系統和 GPU 統計資訊收集的選項。",
+ "amd_gpu_stats": {
+ "label": "AMD GPU 統計",
+ "description": "如果存在 AMD GPU,則啟用 AMD GPU 統計資訊收集。"
+ },
+ "intel_gpu_stats": {
+ "label": "Intel GPU 統計",
+ "description": "如果存在 Intel GPU,則啟用 Intel GPU 統計資訊收集。"
+ },
+ "network_bandwidth": {
+ "label": "網路頻寬",
+ "description": "為攝影機 ffmpeg 程序和偵測器啟用按程序網路頻寬監控(需要權限)。"
+ },
+ "intel_gpu_device": {
+ "label": "Intel GPU 裝置",
+ "description": "當系統存在多個 Intel 顯示卡時,用於將顯示卡執行資料繫結到指定裝置的 PCI 匯流排地址或 DRM 裝置路徑(示例:/dev/dri/card1)。"
+ }
+ },
+ "version_check": {
+ "label": "版本檢查",
+ "description": "啟用出站檢查以偵測是否有更新版本的 Frigate 可用。"
+ }
+ },
+ "tls": {
+ "label": "TLS",
+ "description": "Frigate Web 端點(埠 8971)的 TLS 設定。",
+ "enabled": {
+ "label": "開啟 TLS",
+ "description": "為 Frigate 的網頁頁面和 API 的埠開啟 TLS 加密。"
+ }
+ },
+ "ui": {
+ "label": "使用者介面",
+ "description": "使用者介面偏好設定,如時區、時間/日期格式和單位。",
+ "timezone": {
+ "label": "時區",
+ "description": "UI 中顯示的可選時區(如果未設定,則預設為瀏覽器本地時間)。"
+ },
+ "time_format": {
+ "label": "時間格式",
+ "description": "UI 中使用的時間格式(browser、12hour 或 24hour)。"
+ },
+ "date_style": {
+ "label": "日期樣式",
+ "description": "UI 中使用的日期樣式(full、long、medium、short)。"
+ },
+ "time_style": {
+ "label": "時間樣式",
+ "description": "UI 中使用的時間樣式(full、long、medium、short)。"
+ },
+ "unit_system": {
+ "label": "單位系統",
+ "description": "UI 和 MQTT 中使用的顯示單位系統(公制或英制)。"
+ }
+ },
+ "detectors": {
+ "label": "偵測器硬體",
+ "description": "目標偵測器(CPU、GPU、ONNX 後端)的配置以及任何偵測器特定的模型設定。",
+ "type": {
+ "label": "型別"
+ },
+ "model": {
+ "label": "偵測器特定的模型配置",
+ "description": "偵測器特定的模型配置選項(路徑、輸入尺寸等)。",
+ "path": {
+ "label": "自訂目標偵測模型路徑",
+ "description": "自訂偵測模型檔案的路徑(或使用 plus:// 指定 Frigate+ 模型)。"
+ },
+ "labelmap_path": {
+ "label": "自訂目標偵測器的標籤對映(labelmap)",
+ "description": "偵測器標籤對映檔案(labelmap)路徑,用於將數字類別對映為文字標籤。"
+ },
+ "width": {
+ "label": "目標偵測模型輸入寬度",
+ "description": "模型輸入張量(input tensor)的寬度(以像素為單位)。"
+ },
+ "height": {
+ "label": "目標偵測模型輸入高度",
+ "description": "模型輸入張量(input tensor)的高度(以像素為單位)。"
+ },
+ "labelmap": {
+ "label": "標籤對映(labelmap)自訂",
+ "description": "合併到標準標籤對映表中的覆蓋 / 重對映規則。"
+ },
+ "attributes_map": {
+ "label": "目標標籤到其屬性標籤的對映",
+ "description": "用於繫結元資料的目標標籤 → 屬性標籤對映關係(例如:'car'→ ['license_plate'] 為將車牌屬性繫結到車輛上)。"
+ },
+ "input_tensor": {
+ "label": "模型輸入張量形狀",
+ "description": "模型期望的張量格式(Tensor format):'nhwc' 或 'nchw'。"
+ },
+ "input_pixel_format": {
+ "label": "模型輸入像素顏色格式",
+ "description": "模型期望的像素顏色空間:'rgb'、'bgr' 或 'yuv'。"
+ },
+ "input_dtype": {
+ "label": "模型輸入資料型別",
+ "description": "模型輸入張量的資料型別(例如 'float32')。"
+ },
+ "model_type": {
+ "label": "目標偵測模型型別",
+ "description": "某些偵測器用於最佳化的偵測器模型架構型別(ssd、yolox、yolonas)。"
+ }
+ },
+ "model_path": {
+ "label": "偵測器專用模型路徑",
+ "description": "所選偵測器需要時,需填寫其模型檔案的路徑。"
+ },
+ "axengine": {
+ "label": "愛芯元智 NPU",
+ "description": "AXERA AX650N/AX8850N NPU 偵測器,透過 AXEngine 執行庫載入並執行編譯後的 .axmodel 模型檔案。"
+ },
+ "cpu": {
+ "label": "CPU",
+ "description": "在主機 CPU 上執行 TensorFlow Lite 模型的 CPU TFLite 偵測器,無硬體加速。不推薦使用。",
+ "num_threads": {
+ "label": "偵測執行緒數",
+ "description": "用於基於 CPU 的推理的執行緒數。"
+ }
+ },
+ "deepstack": {
+ "label": "DeepStack",
+ "description": "將影像傳送到遠端 DeepStack HTTP API 進行推理的 DeepStack/CodeProject.AI 偵測器。不推薦使用。",
+ "api_url": {
+ "label": "DeepStack API 網址",
+ "description": "DeepStack API 的 URL。"
+ },
+ "api_timeout": {
+ "label": "DeepStack API 超時時間(秒)",
+ "description": "DeepStack API 請求允許的最長時間。"
+ },
+ "api_key": {
+ "label": "DeepStack API 金鑰(如需要)",
+ "description": "用於認證 DeepStack 服務的可選 API 金鑰。"
+ }
+ },
+ "degirum": {
+ "label": "DeGirum",
+ "description": "透過 DeGirum 雲或本地推理服務執行模型的 DeGirum 偵測器。",
+ "location": {
+ "label": "推理位置",
+ "description": "DeGirum 推理引擎的位置(例如 '@cloud'、'127.0.0.1')。"
+ },
+ "zoo": {
+ "label": "模型庫",
+ "description": "DeGirum 模型庫的路徑或 URL。"
+ },
+ "token": {
+ "label": "DeGirum 雲令牌",
+ "description": "用於 DeGirum 雲存取的令牌。"
+ }
+ },
+ "edgetpu": {
+ "label": "EdgeTPU",
+ "description": "使用 EdgeTPU 委託執行為 Coral EdgeTPU 編譯的 TensorFlow Lite 模型的 EdgeTPU 偵測器。",
+ "device": {
+ "label": "裝置型別",
+ "description": "用於 EdgeTPU 推理的裝置(例如 'usb'、'pci')。"
+ }
+ },
+ "hailo8l": {
+ "label": "Hailo-8/Hailo-8L",
+ "description": "使用 HEF 模型和 HailoRT SDK 在 Hailo 硬體上進行推理的 Hailo-8/Hailo-8L 偵測器。",
+ "device": {
+ "label": "裝置型別",
+ "description": "用於 Hailo 推理的裝置(例如 'PCIe'、'M.2')。"
+ }
+ },
+ "memryx": {
+ "label": "MemryX",
+ "description": "在 MemryX 加速器上執行編譯的 DFP 模型的 MemryX MX3 偵測器。",
+ "device": {
+ "label": "裝置路徑",
+ "description": "用於 MemryX 推理的裝置(例如 'PCIe')。"
+ }
+ },
+ "onnx": {
+ "label": "ONNX",
+ "description": "執行 ONNX 模型的 ONNX 偵測器;當可用時將使用可用的加速後端(CUDA/ROCm/OpenVINO)。",
+ "device": {
+ "label": "裝置型別",
+ "description": "用於 ONNX 推理的裝置(例如 'AUTO'、'CPU'、'GPU')。"
+ }
+ },
+ "openvino": {
+ "label": "OpenVINO",
+ "description": "適用於 AMD 和 Intel CPU、Intel GPU 和 Intel VPU 硬體的 OpenVINO 偵測器。",
+ "device": {
+ "label": "裝置型別",
+ "description": "用於 OpenVINO 推理的裝置(例如 'CPU'、'GPU'、'NPU')。"
+ }
+ },
+ "rknn": {
+ "label": "RKNN",
+ "description": "用於 Rockchip NPU 的 RKNN 偵測器;在 Rockchip 硬體上執行編譯的 RKNN 模型。",
+ "num_cores": {
+ "label": "使用的 NPU 核心數。",
+ "description": "要使用的 NPU 核心數(0 表示自動)。"
+ }
+ },
+ "synaptics": {
+ "label": "Synaptics",
+ "description": "使用 Synap SDK 在 Synaptics 硬體上執行 .synap 格式模型的 Synaptics NPU 偵測器。"
+ },
+ "teflon_tfl": {
+ "label": "Teflon",
+ "description": "使用 Mesa Teflon 委託庫在支援的 GPU 上加速推理的 TFLite Teflon 委託偵測器。"
+ },
+ "tensorrt": {
+ "label": "TensorRT",
+ "description": "使用序列化的 TensorRT 引擎進行加速推理的 Nvidia Jetson 裝置 TensorRT 偵測器。",
+ "device": {
+ "label": "GPU 裝置索引",
+ "description": "要使用的 GPU 裝置索引。"
+ }
+ },
+ "zmq": {
+ "label": "ZMQ IPC",
+ "description": "透過 ZeroMQ IPC 端點將推理解除安裝到外部程序的 ZMQ IPC 偵測器。",
+ "endpoint": {
+ "label": "ZMQ IPC 端點",
+ "description": "要連線的 ZMQ 端點。"
+ },
+ "request_timeout_ms": {
+ "label": "ZMQ 請求超時(毫秒)",
+ "description": "ZMQ 請求的超時時間(毫秒)。"
+ },
+ "linger_ms": {
+ "label": "ZMQ 套接字逗留時間(毫秒)",
+ "description": "套接字逗留時間(毫秒)。"
+ }
+ }
+ },
+ "model": {
+ "label": "偵測模型",
+ "description": "用於配置自訂目標偵測模型及其輸入形狀的設定。",
+ "path": {
+ "label": "自訂目標偵測模型路徑",
+ "description": "自訂偵測模型檔案的路徑(或 Frigate+ 模型的 plus://)。"
+ },
+ "labelmap_path": {
+ "label": "自訂目標偵測器的標籤對映",
+ "description": "將數字類別對映到偵測器字串標籤的標籤對映檔案路徑。"
+ },
+ "width": {
+ "label": "目標偵測模型輸入寬度",
+ "description": "模型輸入張量的寬度(像素)。"
+ },
+ "height": {
+ "label": "目標偵測模型輸入高度",
+ "description": "模型輸入張量的高度(像素)。"
+ },
+ "labelmap": {
+ "label": "標籤對映自訂",
+ "description": "要合併到標準標籤對映中的覆蓋或重對映條目。"
+ },
+ "attributes_map": {
+ "label": "目標標籤到屬性標籤的對映",
+ "description": "從目標標籤到屬性標籤的對映,用於附加元資料(例如 'car' -> ['license_plate'])。"
+ },
+ "input_tensor": {
+ "label": "模型輸入張量形狀",
+ "description": "模型期望的張量格式:'nhwc' 或 'nchw'。"
+ },
+ "input_pixel_format": {
+ "label": "模型輸入像素顏色格式",
+ "description": "模型期望的像素色彩空間:'rgb'、'bgr' 或 'yuv'。"
+ },
+ "input_dtype": {
+ "label": "模型輸入資料型別",
+ "description": "模型輸入張量的資料型別(例如 'float32')。"
+ },
+ "model_type": {
+ "label": "目標偵測模型型別",
+ "description": "某些偵測器用於最佳化的偵測器模型架構型別(ssd、yolox、yolonas)。"
+ }
+ },
+ "genai": {
+ "label": "生成式 AI 配置",
+ "description": "用於生成目標描述和審閱摘要的整合生成式 AI 提供商設定。",
+ "api_key": {
+ "label": "API 金鑰",
+ "description": "某些提供商要求的 API 金鑰(也可以透過環境變數設定)。"
+ },
+ "base_url": {
+ "label": "基礎 URL",
+ "description": "自託管或相容提供商的基礎 URL(例如 Ollama 例項)。"
+ },
+ "model": {
+ "label": "模型",
+ "description": "用於生成描述或摘要的提供商模型。"
+ },
+ "provider": {
+ "label": "提供商",
+ "description": "要使用的生成式 AI 提供商(例如:ollama、gemini、openai 等。國產大模型廠商可使用 openai 介面)。"
+ },
+ "roles": {
+ "label": "功能",
+ "description": "生成式 AI 功能(對話、描述、嵌入);每個功能單獨一個提供商。"
+ },
+ "provider_options": {
+ "label": "提供商選項",
+ "description": "要傳遞給生成式 AI 客戶端的、與服務提供商相關的額外配置項。"
+ },
+ "runtime_options": {
+ "label": "執行時選項",
+ "description": "每次推理呼叫時傳遞給提供商的執行時選項。"
+ }
+ },
+ "birdseye": {
+ "label": "鳥瞰圖",
+ "description": "將多路攝影機畫面合併為統一佈局的鳥瞰合成檢視設定。",
+ "enabled": {
+ "label": "開啟鳥瞰圖",
+ "description": "開啟或關閉鳥瞰圖功能。"
+ },
+ "mode": {
+ "label": "追蹤模式",
+ "description": "在鳥瞰檢視中包含攝影機的模式:'objects'(目標)、'motion'(動作)或 'continuous'(持續)。"
+ },
+ "restream": {
+ "label": "轉發 RTSP",
+ "description": "將鳥瞰圖輸出作為 RTSP 流重新轉發;啟用此功能將使鳥瞰圖持續執行。"
+ },
+ "width": {
+ "label": "寬度",
+ "description": "合成的鳥瞰幀的輸出寬度(像素)。"
+ },
+ "height": {
+ "label": "高度",
+ "description": "合成的鳥瞰幀的輸出高度(像素)。"
+ },
+ "quality": {
+ "label": "編碼品質",
+ "description": "鳥瞰圖 mpeg1 流的編碼品質(1 最高品質,31 最低)。"
+ },
+ "inactivity_threshold": {
+ "label": "非活動閾值",
+ "description": "攝影機停止在鳥瞰圖中顯示的非活動秒數。"
+ },
+ "layout": {
+ "label": "佈局",
+ "description": "鳥瞰圖合成的佈局選項。",
+ "scaling_factor": {
+ "label": "縮放因子",
+ "description": "佈局計算器使用的縮放因子(範圍 1.0 到 5.0)。"
+ },
+ "max_cameras": {
+ "label": "最大攝影機數",
+ "description": "鳥瞰圖中同時顯示的最大攝影機數量;顯示最近的攝影機。"
+ }
+ },
+ "idle_heartbeat_fps": {
+ "label": "空閒心跳 FPS",
+ "description": "空閒時重新發送最後一個合成鳥瞰幀的每秒幀數;設為 0 則停用。"
+ },
+ "order": {
+ "label": "排序位置",
+ "description": "用於控制攝影機在鳥瞰檢視佈局中排序位置的數值。"
+ }
+ },
+ "detect": {
+ "label": "目標偵測",
+ "description": "用於執行目標偵測、初始化追蹤器的偵測模組設定。",
+ "enabled": {
+ "label": "開啟目標偵測",
+ "description": "為所有攝影機啟用或停用目標偵測,可按攝影機覆蓋。"
+ },
+ "height": {
+ "label": "偵測畫面高度",
+ "description": "用於配置偵測流的畫面高度(像素);留空則使用原始影片流解析度。"
+ },
+ "width": {
+ "label": "偵測畫面寬度",
+ "description": "用於配置偵測流的畫面寬度(像素);留空則使用原始影片流解析度。"
+ },
+ "fps": {
+ "label": "偵測幀率",
+ "description": "偵測時希望使用的幀率;數值越低,CPU 佔用越小(推薦值為 5,僅在追蹤極高速運動的目標時才設定更高數值,最高不建議超過 10)。"
+ },
+ "min_initialized": {
+ "label": "最小初始化幀數",
+ "description": "建立追蹤目標前,需要連續偵測到目標的次數。數值越大,錯誤觸發的追蹤越少。預設值為幀率除以 2。"
+ },
+ "max_disappeared": {
+ "label": "最大消失幀數",
+ "description": "追蹤目標在連續多少幀未被偵測到時,將被判定為已消失。"
+ },
+ "stationary": {
+ "label": "靜止目標配置",
+ "description": "用於偵測和管理長時間靜止目標的相關設定。",
+ "interval": {
+ "label": "靜止間隔",
+ "description": "設定每隔多少幀執行一次偵測,用於確認目標是否處於靜止狀態。"
+ },
+ "threshold": {
+ "label": "靜止閾值",
+ "description": "目標需要連續多少幀位置不變,才會被標記為靜止狀態。"
+ },
+ "max_frames": {
+ "label": "最大幀數",
+ "description": "限制靜止目標最大追蹤時長(以幀數為單位),超過將會停止追蹤。",
+ "default": {
+ "label": "預設最大幀數",
+ "description": "停止追蹤前,用於追蹤靜止目標的預設最大幀數。"
+ },
+ "objects": {
+ "label": "目標最大幀數",
+ "description": "可對不同型別目標分別設定靜止追蹤的最大幀數(覆蓋全域性設定)。"
+ }
+ },
+ "classifier": {
+ "label": "開啟視覺分類器",
+ "description": "使用視覺分類器,即使偵測框有輕微抖動,也能準確判斷物體是否為靜止。"
+ }
+ },
+ "annotation_offset": {
+ "label": "標記偏移量",
+ "description": "偵測標記的時間偏移量(毫秒),用於讓時間軸上的偵測框與錄影畫面更精準對齊;可設定為正數或負數。"
+ }
+ },
+ "ffmpeg": {
+ "label": "FFmpeg",
+ "description": "FFmpeg 編解碼相關設定,包含可執行檔案路徑、命令列引數、硬體加速選項,以及按不同功能劃分的輸出引數。",
+ "path": {
+ "label": "FFmpeg 路徑",
+ "description": "要使用的 FFmpeg 可執行檔案路徑,或版本別名(如 \"5.0\" 或 \"7.0\")。"
+ },
+ "global_args": {
+ "label": "FFmpeg 全域性引數",
+ "description": "傳遞給 FFmpeg 程序的全域性引數。"
+ },
+ "hwaccel_args": {
+ "label": "硬體加速引數",
+ "description": "用於 FFmpeg 的硬體加速引數。建議使用對應硬體廠商的預設配置。"
+ },
+ "input_args": {
+ "label": "輸入引數",
+ "description": "應用於 FFmpeg 輸入影片流的輸入引數。"
+ },
+ "output_args": {
+ "label": "輸出引數",
+ "description": "用於不同 FFmpeg 功能(如偵測、錄製)的預設輸出引數。",
+ "detect": {
+ "label": "偵測輸出引數",
+ "description": "偵測功能影片流的預設輸出引數。"
+ },
+ "record": {
+ "label": "錄製輸出引數",
+ "description": "錄製功能影片流的預設輸出引數。"
+ }
+ },
+ "retry_interval": {
+ "label": "FFmpeg 重試時間",
+ "description": "攝影機影片流異常斷開後,重新連線前的等待時間。預設為 10 秒。"
+ },
+ "apple_compatibility": {
+ "label": "Apple 相容性",
+ "description": "錄製 H.265 影片時啟用 HEVC 標記,以提升對 Apple 裝置播放的相容性。"
+ },
+ "gpu": {
+ "label": "GPU 索引",
+ "description": "在啟用硬體加速時,預設使用的 GPU 索引。"
+ },
+ "inputs": {
+ "label": "攝影機輸入影片流",
+ "description": "該攝影機的所有輸入流配置清單(包含路徑和功能)。",
+ "path": {
+ "label": "輸入路徑",
+ "description": "攝影機輸入影片流的地址或路徑。"
+ },
+ "roles": {
+ "label": "輸入流功能",
+ "description": "定義該影片流的功能。"
+ },
+ "global_args": {
+ "label": "FFmpeg 全域性引數",
+ "description": "該輸入影片流使用的 FFmpeg 全域性通用引數。"
+ },
+ "hwaccel_args": {
+ "label": "硬體加速引數",
+ "description": "該輸入影片流的硬體加速引數。"
+ },
+ "input_args": {
+ "label": "輸入引數",
+ "description": "該影片流特定的輸入引數。"
+ }
+ }
+ },
+ "live": {
+ "label": "即時監控觀看",
+ "description": "用於控制 jsmpeg 即時串流解析度與畫質的設定。此設定不影響使用 go2rtc 進行即時預覽的重新串流攝影機。",
+ "streams": {
+ "label": "即時監控流名稱",
+ "description": "配置的流名稱到用於即時監控播放的 restream/go2rtc 名稱的對映。"
+ },
+ "height": {
+ "label": "即時監控高度",
+ "description": "在網頁頁面中渲染 jsmpeg 即時監控流的高度(像素);必須小於等於偵測流高度。"
+ },
+ "quality": {
+ "label": "即時監控品質",
+ "description": "jsmpeg 流的編碼品質(1 最高,31 最低)。"
+ }
+ },
+ "motion": {
+ "label": "畫面變動偵測",
+ "description": "應用於攝影機的預設動作偵測設定,除非按攝影機覆蓋。",
+ "enabled": {
+ "label": "開啟畫面變動偵測",
+ "description": "為所有攝影機啟用或停用動作偵測;可按攝影機覆蓋。"
+ },
+ "threshold": {
+ "label": "畫面變動閾值",
+ "description": "畫面變動偵測器使用的像素差異閾值;數值越高靈敏度越低(範圍 1-255)。"
+ },
+ "lightning_threshold": {
+ "label": "閃電閾值",
+ "description": "用於偵測和忽略短暫閃電閃爍的閾值(數值越低越敏感,範圍 0.3 到 1.0)。這不會完全阻止畫面變動偵測;只是當超過閾值時偵測器會停止分析額外的幀。在此類事件期間仍會建立基於畫面變動的錄影。"
+ },
+ "skip_motion_threshold": {
+ "label": "跳過畫面變動閾值",
+ "description": "如果單幀中畫面變化超過此比例,偵測器將判定為無畫面變動並立即重新校準。這可以節省 CPU 並減少閃電、風暴等情況下的誤報,但也可能會錯過真正的事件,如 PTZ 攝影機自動追蹤目標。你需要權衡取捨:是否犧牲少量錄製片段,換取更少無效影片與更低的誤檢。保持為空即可關閉該功能。"
+ },
+ "improve_contrast": {
+ "label": "改善對比度",
+ "description": "在畫面變動分析之前對幀應用對比度改善以幫助偵測。"
+ },
+ "contour_area": {
+ "label": "輪廓區域",
+ "description": "畫面變動輪廓被計入所需的最小輪廓區域(像素)。"
+ },
+ "delta_alpha": {
+ "label": "Delta alpha",
+ "description": "用於畫面變動計算的幀差異中使用的 alpha 混合因子。"
+ },
+ "frame_alpha": {
+ "label": "畫面 alpha 通道",
+ "description": "畫面變動預處理時混合畫面所使用的 alpha 值。"
+ },
+ "frame_height": {
+ "label": "畫面高度",
+ "description": "計算畫面變動時縮放畫面的高度(像素)。"
+ },
+ "mask": {
+ "label": "遮罩座標",
+ "description": "定義用於包含/排除區域的畫面變動遮罩多邊形的有序 x,y 座標。"
+ },
+ "mqtt_off_delay": {
+ "label": "MQTT 關閉延遲",
+ "description": "在釋出 MQTT 'off' 狀態之前,最後一次畫面變動後等待的秒數。"
+ },
+ "enabled_in_config": {
+ "label": "原始畫面變動狀態",
+ "description": "指示原始靜態配置中是否啟用了畫面變動偵測。"
+ },
+ "raw_mask": {
+ "label": "原始遮罩"
+ }
+ },
+ "objects": {
+ "label": "目標",
+ "description": "目標追蹤預設設定,包括要追蹤的標籤和按目標的過濾器。",
+ "track": {
+ "label": "要追蹤的目標",
+ "description": "所有攝影機要追蹤的目標標籤清單;可按攝影機覆蓋。"
+ },
+ "filters": {
+ "label": "目標過濾器",
+ "description": "應用於偵測到的目標以減少誤報的過濾器(區域、比例、置信度)。",
+ "min_area": {
+ "label": "最小目標區域",
+ "description": "此目標型別所需的最小邊界框區域(像素或百分比)。可以是像素(整數)或百分比(0.000001 到 0.99 之間的浮點數)。"
+ },
+ "max_area": {
+ "label": "最大目標區域",
+ "description": "此目標型別允許的最大邊界框區域(像素或百分比)。可以是像素(整數)或百分比(0.000001 到 0.99 之間的浮點數)。"
+ },
+ "min_ratio": {
+ "label": "最小縱橫比",
+ "description": "邊界框所需的最小寬高比。"
+ },
+ "max_ratio": {
+ "label": "最大縱橫比",
+ "description": "邊界框允許的最大寬高比。"
+ },
+ "threshold": {
+ "label": "置信度閾值",
+ "description": "目標被視為真正陽性所需的平均偵測置信度閾值。"
+ },
+ "min_score": {
+ "label": "最小置信度",
+ "description": "目標被計入所需的最小單幀偵測置信度。"
+ },
+ "mask": {
+ "label": "過濾器遮罩",
+ "description": "定義此過濾器在幀內應用位置的多邊形座標。"
+ },
+ "raw_mask": {
+ "label": "原始遮罩"
+ }
+ },
+ "mask": {
+ "label": "目標遮罩",
+ "description": "用於防止在指定區域進行目標偵測的遮罩多邊形。"
+ },
+ "raw_mask": {
+ "label": "原始遮罩"
+ },
+ "genai": {
+ "label": "生成式 AI 目標配置",
+ "description": "用於傳送畫面給生成式 AI 進行生成和描述追蹤目標的選項。",
+ "enabled": {
+ "label": "開啟生成式 AI",
+ "description": "預設開啟生成式 AI 生成追蹤目標的描述。"
+ },
+ "use_snapshot": {
+ "label": "使用快照",
+ "description": "使用目標快照而不是縮圖給生成式 AI 進行描述生成。"
+ },
+ "prompt": {
+ "label": "字幕提示",
+ "description": "使用生成式 AI 生成描述時使用的預設提示模板。"
+ },
+ "object_prompts": {
+ "label": "目標提示",
+ "description": "按目標設定提示詞,讓生成式 AI 對不同標籤的輸出進行定製。"
+ },
+ "objects": {
+ "label": "生成式 AI 目標",
+ "description": "預設傳送給生成式 AI 的目標標籤清單。"
+ },
+ "required_zones": {
+ "label": "必需區域",
+ "description": "目標必須進入這些區域,才會觸發生成式 AI 描述生成。"
+ },
+ "debug_save_thumbnails": {
+ "label": "儲存縮圖",
+ "description": "儲存傳送給生成式 AI 的縮圖用於除錯和審閱。"
+ },
+ "send_triggers": {
+ "label": "生成式 AI 觸發器",
+ "description": "定義畫面幀應在何時傳送給生成式 AI(如偵測結束時、更新後等)。",
+ "tracked_object_end": {
+ "label": "結束時傳送",
+ "description": "目標追蹤結束時向生成式 AI 傳送請求。"
+ },
+ "after_significant_updates": {
+ "label": "生成式 AI 提前觸發",
+ "description": "在追蹤目標發生指定次數的重要變化後,向生成式 AI 傳送請求。"
+ }
+ },
+ "enabled_in_config": {
+ "label": "原配置生成式 AI 狀態",
+ "description": "表示在原始靜態配置中是否已啟用生成式 AI。"
+ }
+ },
+ "filters_attribute": {
+ "label": "屬性篩選器",
+ "description": "篩選器用來篩選屬性,以此降低誤報(區域、比例、置信度)。",
+ "min_area": {
+ "label": "最小屬性區域",
+ "description": "此屬性允許的最小邊界框區域(像素或百分比)。可以是像素(整數)或百分比(0.000001 到 0.99 之間的浮點數)。"
+ },
+ "max_area": {
+ "label": "最大屬性區域",
+ "description": "此屬性允許的最大邊界框區域(像素或百分比)。可以是像素(整數)或百分比(0.000001 到 0.99 之間的浮點數)。"
+ },
+ "min_ratio": {
+ "label": "最小長寬比",
+ "description": "邊界框所需的最小長寬比。"
+ },
+ "max_ratio": {
+ "label": "最大長寬比",
+ "description": "邊界框允許的最大長寬比。"
+ },
+ "threshold": {
+ "label": "置信度門檻",
+ "description": "判定屬性有效所需的平均偵測置信度門檻。"
+ },
+ "min_score": {
+ "label": "最小置信度",
+ "description": "此屬性與其父物件關聯所需的最小單幀畫面的偵測置信度。"
+ },
+ "mask": {
+ "label": "篩選遮罩",
+ "description": "定義此篩選器在畫面內應用位置的多邊形座標。"
+ },
+ "raw_mask": {
+ "label": "原始遮罩"
+ }
+ }
+ },
+ "record": {
+ "label": "錄影",
+ "description": "應用於攝影機的錄影和保留設定,除非按攝影機覆蓋。",
+ "enabled": {
+ "label": "開啟錄影",
+ "description": "為所有攝影機啟用或停用錄影;可按攝影機覆蓋。"
+ },
+ "expire_interval": {
+ "label": "錄影清理間隔",
+ "description": "清理過期錄影片段的間隔分鐘數。"
+ },
+ "continuous": {
+ "label": "持續保留",
+ "description": "無論是否有追蹤目標或動作,保留錄影的天數。如果只想保留警報和偵測的錄影,請設定為 0。",
+ "days": {
+ "label": "保留天數",
+ "description": "保留錄影的天數。"
+ }
+ },
+ "motion": {
+ "label": "動作保留",
+ "description": "無論是否有追蹤目標,由動作觸發的錄影保留天數。如果只想保留警報和偵測的錄影,請設定為 0。",
+ "days": {
+ "label": "保留天數",
+ "description": "保留錄影的天數。"
+ }
+ },
+ "detections": {
+ "label": "偵測保留",
+ "description": "偵測事件的錄影保留設定,包括前後捕獲時長。",
+ "pre_capture": {
+ "label": "前捕獲秒數",
+ "description": "偵測事件之前包含在錄影中的秒數。"
+ },
+ "post_capture": {
+ "label": "後捕獲秒數",
+ "description": "偵測事件之後包含在錄影中的秒數。"
+ },
+ "retain": {
+ "label": "事件保留",
+ "description": "偵測事件錄影的保留設定。",
+ "days": {
+ "label": "保留天數",
+ "description": "保留偵測事件錄影的天數。"
+ },
+ "mode": {
+ "label": "保留模式",
+ "description": "保留模式:all(儲存所有片段)、motion(儲存有動作的片段)或 active_objects(儲存有活動目標的片段)。"
+ }
+ }
+ },
+ "alerts": {
+ "label": "警報保留",
+ "description": "警報事件的錄影保留設定,包括前後捕獲時長。",
+ "pre_capture": {
+ "label": "前捕獲秒數",
+ "description": "偵測事件之前包含在錄影中的秒數。"
+ },
+ "post_capture": {
+ "label": "後捕獲秒數",
+ "description": "偵測事件之後包含在錄影中的秒數。"
+ },
+ "retain": {
+ "label": "事件保留",
+ "description": "偵測事件錄影的保留設定。",
+ "days": {
+ "label": "保留天數",
+ "description": "保留偵測事件錄影的天數。"
+ },
+ "mode": {
+ "label": "保留模式",
+ "description": "保留模式:all(儲存所有片段)、motion(儲存有動作的片段)或 active_objects(儲存有活動目標的片段)。"
+ }
+ }
+ },
+ "export": {
+ "label": "匯出配置",
+ "description": "匯出錄影時使用的設定,如延時攝影和硬體加速。",
+ "hwaccel_args": {
+ "label": "匯出硬體加速引數",
+ "description": "用於匯出/轉碼操作的硬體加速引數。"
+ },
+ "max_concurrent": {
+ "label": "最大併發匯出數",
+ "description": "同時可處理的最大匯出任務數量。"
+ }
+ },
+ "preview": {
+ "label": "預覽配置",
+ "description": "控制介面中顯示的錄影預覽品質的設定。",
+ "quality": {
+ "label": "預覽品質",
+ "description": "預覽品質級別(very_low、low、medium、high、very_high)。"
+ }
+ },
+ "enabled_in_config": {
+ "label": "原始錄影狀態",
+ "description": "指示原始靜態配置中是否啟用了錄影。"
+ }
+ },
+ "review": {
+ "label": "審閱",
+ "description": "控制 UI 和儲存使用的警報、偵測和 GenAI 審閱摘要的設定。",
+ "alerts": {
+ "label": "警報配置",
+ "description": "哪些追蹤目標生成警報以及如何保留警報的設定。",
+ "enabled": {
+ "label": "開啟警報",
+ "description": "為所有攝影機啟用或停用警報生成;可按攝影機覆蓋。"
+ },
+ "labels": {
+ "label": "警報標籤",
+ "description": "符合警報條件的目標標籤清單(例如:car、person)。"
+ },
+ "required_zones": {
+ "label": "必需區域",
+ "description": "目標必須進入才能被視為警報的區域;留空則允許任何區域。"
+ },
+ "enabled_in_config": {
+ "label": "原始警報狀態",
+ "description": "追蹤原始靜態配置中是否啟用了警報。"
+ },
+ "cutoff_time": {
+ "label": "警報截止時間",
+ "description": "在沒有引起警報的活動後等待多少秒後截止警報。"
+ }
+ },
+ "detections": {
+ "label": "偵測配置",
+ "description": "用於設定哪些追蹤目標會生成偵測記錄(非警報類),以及偵測記錄的保留方式。",
+ "enabled": {
+ "label": "開啟偵測",
+ "description": "為所有攝影機啟用或停用偵測事件;可按攝影機覆蓋。"
+ },
+ "labels": {
+ "label": "偵測標籤",
+ "description": "符合偵測事件條件的目標標籤清單。"
+ },
+ "required_zones": {
+ "label": "必需區域",
+ "description": "目標必須進入才能被視為偵測的區域;留空則允許任何區域。"
+ },
+ "cutoff_time": {
+ "label": "偵測截止時間",
+ "description": "在沒有引起偵測的活動後等待多少秒後截止偵測。"
+ },
+ "enabled_in_config": {
+ "label": "原始偵測狀態",
+ "description": "追蹤原始靜態配置中是否啟用了偵測。"
+ }
+ },
+ "genai": {
+ "label": "生成式 AI 配置",
+ "description": "控制使用生成式 AI 為審閱項生成描述和摘要。",
+ "enabled": {
+ "label": "開啟生成式 AI 描述",
+ "description": "為審閱項開啟或關閉使用生成式 AI 生成描述和摘要。"
+ },
+ "alerts": {
+ "label": "為警報開啟生成式 AI",
+ "description": "使用生成式 AI 為警報項生成描述。"
+ },
+ "detections": {
+ "label": "為偵測開啟生成式 AI",
+ "description": "使用生成式 AI 為偵測項生成描述。"
+ },
+ "image_source": {
+ "label": "審閱影像來源",
+ "description": "傳送給生成式 AI 的畫面來源('preview' 或 'recordings');'recordings' 使用更高品質的畫面幀,但會消耗更多的 token。"
+ },
+ "additional_concerns": {
+ "label": "額外關注事項",
+ "description": "生成式 AI 在分析此攝影機的監控行為時,需要額外注意的事項或說明清單。"
+ },
+ "debug_save_thumbnails": {
+ "label": "儲存縮圖",
+ "description": "儲存傳送給生成式 AI 提供商的縮圖用於除錯和審閱。"
+ },
+ "enabled_in_config": {
+ "label": "原配置生成式 AI 狀態",
+ "description": "記錄在靜態配置中最初是否已啟用生成式 AI 審閱功能。"
+ },
+ "preferred_language": {
+ "label": "首選語言",
+ "description": "向生成式 AI 提供商請求生成回應的首選語言。"
+ },
+ "activity_context_prompt": {
+ "label": "活動上下文提示",
+ "description": "自訂提示詞,用於說明可疑行為與非可疑行為的界定,為生成式 AI 生成摘要提供上下文依據。"
+ }
+ }
+ },
+ "snapshots": {
+ "label": "快照",
+ "description": "所有攝影機的追蹤目標 API 快照設定;可攝影機單獨配置覆蓋全域性配置。",
+ "enabled": {
+ "label": "開啟快照",
+ "description": "為所有攝影機啟用或停用儲存快照;可按攝影機覆蓋。"
+ },
+ "timestamp": {
+ "label": "時間戳疊加",
+ "description": "在 API 生成的快照上疊加時間戳。"
+ },
+ "bounding_box": {
+ "label": "邊界框疊加",
+ "description": "在 API 生成的快照上繪製追蹤目標的邊界框。"
+ },
+ "crop": {
+ "label": "裁剪快照",
+ "description": "在 API 生成的快照裁剪到偵測到的目標邊界框。"
+ },
+ "required_zones": {
+ "label": "必需區域",
+ "description": "目標必須進入才能儲存快照的區域。"
+ },
+ "height": {
+ "label": "快照高度",
+ "description": "將 API 生成的快照調整到的目標高度(像素);留空則保持原始大小。"
+ },
+ "retain": {
+ "label": "快照保留",
+ "description": "快照的保留設定,包括預設天數和按目標覆蓋。",
+ "default": {
+ "label": "預設保留",
+ "description": "保留快照的預設天數。"
+ },
+ "mode": {
+ "label": "保留模式",
+ "description": "保留模式:all(儲存所有片段)、motion(儲存有動作的片段)或 active_objects(儲存有活動目標的片段)。"
+ },
+ "objects": {
+ "label": "目標保留",
+ "description": "按目標覆蓋的快照保留天數。"
+ }
+ },
+ "quality": {
+ "label": "快照品質",
+ "description": "儲存快照的編碼品質(0-100)。"
+ }
+ },
+ "timestamp_style": {
+ "label": "時間戳樣式",
+ "description": "應用於除錯檢視和快照的幀內時間戳樣式選項。",
+ "position": {
+ "label": "時間戳位置",
+ "description": "時間戳在影像上的位置(tl/tr/bl/br)。"
+ },
+ "format": {
+ "label": "時間戳格式",
+ "description": "用於時間戳的日期時間格式字串(Python 日期時間格式程式碼)。"
+ },
+ "color": {
+ "label": "時間戳顏色",
+ "description": "時間戳文字的 RGB 顏色值(所有值 0-255)。",
+ "red": {
+ "label": "紅色",
+ "description": "時間戳顏色的紅色分量(0-255)。"
+ },
+ "green": {
+ "label": "綠色",
+ "description": "時間戳顏色的綠色分量(0-255)。"
+ },
+ "blue": {
+ "label": "藍色",
+ "description": "時間戳顏色的藍色分量(0-255)。"
+ }
+ },
+ "thickness": {
+ "label": "時間戳粗細",
+ "description": "時間戳文字的線條粗細。"
+ },
+ "effect": {
+ "label": "時間戳效果",
+ "description": "時間戳文字的視覺效果(none、solid、shadow)。"
+ }
+ },
+ "audio_transcription": {
+ "label": "音訊轉錄",
+ "description": "用於事件和即時字幕的即時和語音音訊轉錄設定。",
+ "enabled": {
+ "label": "啟用語音轉文字",
+ "description": "為所有攝影機啟用或停用自動音訊轉錄;可按攝影機覆蓋。"
+ },
+ "language": {
+ "label": "轉錄語言",
+ "description": "用於轉錄/翻譯的語言程式碼(例如 'en' 表示英語)。請參閱 https://whisper-api.com/docs/languages/ 瞭解支援的語言程式碼。"
+ },
+ "device": {
+ "label": "轉錄裝置",
+ "description": "執行轉錄模型的裝置金鑰(CPU/GPU)。目前僅支援 NVIDIA CUDA GPU 進行轉錄。"
+ },
+ "model_size": {
+ "label": "模型大小",
+ "description": "用於離線音訊事件轉錄的模型大小。"
+ },
+ "live_enabled": {
+ "label": "即時監控轉寫",
+ "description": "在接收到音訊時開啟即時監控持續轉寫。"
+ }
+ },
+ "classification": {
+ "label": "目標分類",
+ "description": "用於最佳化目標標籤或狀態分類的分類模型設定。",
+ "bird": {
+ "label": "鳥類分類配置",
+ "description": "鳥類分類模型特定的設定。",
+ "enabled": {
+ "label": "鳥類分類",
+ "description": "啟用或停用鳥類分類。"
+ },
+ "threshold": {
+ "label": "最小分數",
+ "description": "接受鳥類分類所需的最小分類分數。"
+ }
+ },
+ "custom": {
+ "label": "自訂分類模型",
+ "description": "用於目標或狀態偵測的自訂分類模型配置。",
+ "enabled": {
+ "label": "開啟模型",
+ "description": "啟用或停用自訂分類模型。"
+ },
+ "name": {
+ "label": "模型名稱",
+ "description": "要使用的自訂分類模型的辨識符號。"
+ },
+ "threshold": {
+ "label": "分數閾值",
+ "description": "用於更改分類狀態的分數閾值。"
+ },
+ "save_attempts": {
+ "label": "儲存嘗試",
+ "description": "為最近分類 UI 儲存多少次分類嘗試。"
+ },
+ "object_config": {
+ "objects": {
+ "label": "分類目標",
+ "description": "要執行目標分類的目標型別清單。"
+ },
+ "classification_type": {
+ "label": "分類型別",
+ "description": "應用的分類型別:'sub_label'(新增 sub_label)或其他支援的型別。"
+ }
+ },
+ "state_config": {
+ "cameras": {
+ "label": "分類攝影機",
+ "description": "用於執行狀態分類的按攝影機裁剪和設定。",
+ "crop": {
+ "label": "分類裁剪",
+ "description": "用於在此攝影機上執行分類的裁剪座標。"
+ }
+ },
+ "motion": {
+ "label": "動作時執行",
+ "description": "啟用後,當在指定裁剪區域內偵測到動作時執行分類。"
+ },
+ "interval": {
+ "label": "分類間隔",
+ "description": "狀態分類的定期分類執行間隔(秒)。"
+ }
+ }
+ }
+ },
+ "semantic_search": {
+ "label": "語意搜尋",
+ "description": "用於構建和查詢目標嵌入以查詢相似項的語意搜尋設定。",
+ "enabled": {
+ "label": "開啟語意搜尋",
+ "description": "啟用或停用語意搜尋功能。"
+ },
+ "reindex": {
+ "label": "啟動時重建索引",
+ "description": "觸發將歷史追蹤目標完全重新索引到嵌入資料庫。"
+ },
+ "model": {
+ "label": "語意搜尋模型或生成式 AI 服務名稱",
+ "description": "用於語意搜尋的嵌入模型(例如 'jinav1'),或具有嵌入功能(embeddings)的生成式 AI 服務名稱。"
+ },
+ "model_size": {
+ "label": "模型大小",
+ "description": "選擇模型大小;'small' 在 CPU 上執行,'large' 通常需要 GPU。"
+ },
+ "device": {
+ "label": "裝置",
+ "description": "這是一個覆蓋選項,用於指定特定裝置。請參閱 https://onnxruntime.ai/docs/execution-providers/ 瞭解更多資訊"
+ },
+ "triggers": {
+ "label": "觸發器",
+ "description": "攝影機特定語意搜尋觸發器的操作和匹配條件。",
+ "friendly_name": {
+ "label": "友好名稱",
+ "description": "在 UI 中為此觸發器顯示的可選友好名稱。"
+ },
+ "enabled": {
+ "label": "開啟此觸發器",
+ "description": "啟用或停用此語意搜尋觸發器。"
+ },
+ "type": {
+ "label": "觸發器型別",
+ "description": "觸發器型別:'thumbnail'(與影像匹配)或 'description'(與文字匹配)。"
+ },
+ "data": {
+ "label": "觸發器內容",
+ "description": "要與追蹤目標匹配的文字短語或縮圖 ID。"
+ },
+ "threshold": {
+ "label": "觸發器閾值",
+ "description": "啟用此觸發器所需的最小相似度分數(0-1)。"
+ },
+ "actions": {
+ "label": "觸發器操作",
+ "description": "觸發器匹配時要執行的操作清單(通知、sub_label、屬性)。"
+ }
+ }
+ },
+ "face_recognition": {
+ "label": "人臉辨識",
+ "description": "所有攝影機的人臉偵測和辨識設定;可按攝影機覆蓋。",
+ "enabled": {
+ "label": "開啟人臉辨識",
+ "description": "為所有攝影機啟用或停用人臉辨識;可按攝影機覆蓋。"
+ },
+ "model_size": {
+ "label": "模型大小",
+ "description": "用於人臉嵌入的模型大小(small/large);較大的可能需要 GPU。"
+ },
+ "unknown_score": {
+ "label": "未知分數閾值",
+ "description": "低於此距離閾值的人臉被視為潛在匹配(數值越高越嚴格)。"
+ },
+ "detection_threshold": {
+ "label": "偵測閾值",
+ "description": "將人臉偵測視為有效所需的最小偵測置信度。"
+ },
+ "recognition_threshold": {
+ "label": "辨識閾值",
+ "description": "將兩張人臉視為匹配的人臉嵌入距離閾值。"
+ },
+ "min_area": {
+ "label": "最小人臉區域",
+ "description": "需要嘗試進行人臉辨識的人臉偵測框最小大小(像素)。"
+ },
+ "min_faces": {
+ "label": "最小人臉數",
+ "description": "在將辨識的子標籤應用於人員之前所需的最小人臉辨識次數。"
+ },
+ "save_attempts": {
+ "label": "儲存嘗試",
+ "description": "為最近辨識 UI 保留的人臉辨識嘗試次數。"
+ },
+ "blur_confidence_filter": {
+ "label": "模糊置信度過濾器",
+ "description": "根據影像模糊程度調整置信度分數,以減少低品質人臉的誤報。"
+ },
+ "device": {
+ "label": "裝置",
+ "description": "這是一個覆蓋選項,用於指定特定裝置。請參閱 https://onnxruntime.ai/docs/execution-providers/ 瞭解更多資訊"
+ }
+ },
+ "lpr": {
+ "label": "車牌辨識",
+ "description": "車牌辨識設定,包括偵測閾值、格式化和已知車牌。",
+ "enabled": {
+ "label": "開啟車牌辨識",
+ "description": "為所有攝影機啟用或停用車牌辨識;可按攝影機覆蓋。"
+ },
+ "model_size": {
+ "label": "模型大小",
+ "description": "用於文字偵測/辨識的模型大小,大多數使用者應使用 'small',只有'small'模型支援中文。"
+ },
+ "detection_threshold": {
+ "label": "偵測閾值",
+ "description": "開始對疑似車牌執行 OCR 的偵測置信度閾值。"
+ },
+ "min_area": {
+ "label": "最小車牌區域",
+ "description": "嘗試辨識所需的最小車牌區域(像素)。"
+ },
+ "recognition_threshold": {
+ "label": "辨識閾值",
+ "description": "辨識的車牌文字作為子標籤附加所需的置信度閾值。"
+ },
+ "min_plate_length": {
+ "label": "最小車牌長度",
+ "description": "辨識的車牌被視為有效所需的最小字元數。"
+ },
+ "format": {
+ "label": "車牌格式正則",
+ "description": "用於驗證辨識的車牌字串是否符合預期格式的可選正則表示式。"
+ },
+ "match_distance": {
+ "label": "匹配距離",
+ "description": "將偵測到的車牌與已知車牌比較時允許的字元不匹配數。"
+ },
+ "known_plates": {
+ "label": "已知車牌",
+ "description": "要特別追蹤或報警的車牌或正則表示式清單。"
+ },
+ "enhancement": {
+ "label": "增強級別",
+ "description": "在 OCR 之前應用於車牌裁剪的增強級別(0-10);較高的值可能不總是改善結果,5 以上的級別可能僅適用於夜間車牌,應謹慎使用。"
+ },
+ "debug_save_plates": {
+ "label": "儲存除錯車牌",
+ "description": "儲存車牌裁剪影像用於除錯 LPR 效能。"
+ },
+ "device": {
+ "label": "裝置",
+ "description": "這是一個覆蓋選項,用於指定特定裝置。請參閱 https://onnxruntime.ai/docs/execution-providers/ 瞭解更多資訊"
+ },
+ "replace_rules": {
+ "label": "替換規則",
+ "description": "用於在匹配之前規範化偵測到的車牌字串的正則替換規則。",
+ "pattern": {
+ "label": "正則模式"
+ },
+ "replacement": {
+ "label": "替換字串"
+ }
+ },
+ "expire_time": {
+ "label": "過期秒數",
+ "description": "未見到的車牌從追蹤器中過期的時間(秒)(僅適用於專用 LPR 攝影機)。"
+ }
+ },
+ "camera_groups": {
+ "label": "攝影機分組",
+ "description": "用於在頁面中組織攝影機的命名攝影機分組配置。",
+ "cameras": {
+ "label": "攝影機清單",
+ "description": "此分組中包含的攝影機名稱陣列。"
+ },
+ "icon": {
+ "label": "分組圖示",
+ "description": "在頁面中代表攝影機分組的圖示。"
+ },
+ "order": {
+ "label": "排序順序",
+ "description": "用於在頁面中對攝影機分組進行排序的數字順序;數值越大越靠後。"
+ }
+ },
+ "profiles": {
+ "label": "設定檔",
+ "description": "帶有別名的命名設定檔定義。攝影機設定檔必須引用此處定義的名稱。",
+ "friendly_name": {
+ "label": "別名",
+ "description": "在介面中顯示的此設定檔名稱,可以使用中文。"
+ }
+ },
+ "active_profile": {
+ "label": "啟用設定檔",
+ "description": "當前啟用的設定檔名稱。僅在執行時使用,不會寫入 YAML 設定檔中。"
+ },
+ "camera_mqtt": {
+ "label": "MQTT",
+ "description": "MQTT 影像釋出設定。",
+ "enabled": {
+ "label": "傳送影像",
+ "description": "為此攝影機啟用將目標快照影像釋出到 MQTT 主題。"
+ },
+ "timestamp": {
+ "label": "新增時間戳",
+ "description": "在釋出到 MQTT 的影像上疊加時間戳。"
+ },
+ "bounding_box": {
+ "label": "新增邊界框",
+ "description": "在透過 MQTT 釋出的影像上繪製邊界框。"
+ },
+ "crop": {
+ "label": "裁剪影像",
+ "description": "將釋出到 MQTT 的影像裁剪到偵測到的目標邊界框。"
+ },
+ "height": {
+ "label": "影像高度",
+ "description": "透過 MQTT 釋出的影像調整到的目標高度(像素)。"
+ },
+ "required_zones": {
+ "label": "必需區域",
+ "description": "目標必須進入才能釋出 MQTT 影像的區域。"
+ },
+ "quality": {
+ "label": "JPEG 品質",
+ "description": "釋出到 MQTT 的影像的 JPEG 品質(0-100)。"
+ }
+ },
+ "camera_ui": {
+ "label": "攝影機頁面",
+ "description": "此攝影機在頁面中的顯示順序和可見性。顯示順序僅影響預設儀表板。如需更精細的控制,請使用“攝影機組”。",
+ "order": {
+ "label": "UI 順序",
+ "description": "用於在頁面中排序攝影機的順序(只會影響預設儀表板和清單);數值越大則在越後面。"
+ },
+ "dashboard": {
+ "label": "在 UI 中顯示",
+ "description": "切換此攝影機在 Frigate 頁面中是否可見。停用後需要手動編輯配置才能再次在頁面中檢視此攝影機。"
+ },
+ "review": {
+ "label": "在審查中顯示",
+ "description": "切換此攝影機在審查中是否顯示(包含審查頁面及其攝影機篩選器、動態審查和歷史檢視)。"
+ }
+ },
+ "onvif": {
+ "label": "ONVIF",
+ "description": "此攝影機的 ONVIF 連線和 PTZ 自動追蹤設定。",
+ "host": {
+ "label": "ONVIF 主機",
+ "description": "此攝影機 ONVIF 服務的主機(和可選協議)。"
+ },
+ "port": {
+ "label": "ONVIF 埠",
+ "description": "ONVIF 服務的埠號。"
+ },
+ "user": {
+ "label": "ONVIF 使用者名稱",
+ "description": "ONVIF 身份驗證的使用者名稱;某些裝置需要管理員使用者才能使用 ONVIF。"
+ },
+ "password": {
+ "label": "ONVIF 密碼",
+ "description": "ONVIF 身份驗證的密碼。"
+ },
+ "tls_insecure": {
+ "label": "停用 TLS 驗證",
+ "description": "跳過 TLS 驗證並停用 ONVIF 的摘要認證(不安全;僅用於安全網路)。"
+ },
+ "profile": {
+ "label": "ONVIF 設定檔",
+ "description": "用於 PTZ 控制的指定 ONVIF 媒體配置,將透過 Token 或名稱匹配。如果未手動指定,將自動選擇第一個包含有效 PTZ 配置的媒體配置。"
+ },
+ "autotracking": {
+ "label": "自動追蹤",
+ "description": "使用 PTZ 攝影機移動自動追蹤移動目標並使其保持在畫面中心。",
+ "enabled": {
+ "label": "開啟自動追蹤",
+ "description": "啟用或停用偵測目標的自動 PTZ 攝影機追蹤。"
+ },
+ "calibrate_on_startup": {
+ "label": "啟動時校準",
+ "description": "在啟動時測量 PTZ 電機速度以提高追蹤精度。Frigate 將在校準後用 movement_weights 更新配置。"
+ },
+ "zooming": {
+ "label": "變焦模式",
+ "description": "控制變焦行為:disabled(僅平移/傾斜)、absolute(最相容)或 relative(同時平移/傾斜/變焦)。"
+ },
+ "zoom_factor": {
+ "label": "變焦因子",
+ "description": "控制追蹤目標的變焦級別。數值越低保持更多場景可見;數值越高放大更近但可能丟失追蹤。數值範圍 0.1 到 0.75。"
+ },
+ "track": {
+ "label": "追蹤目標",
+ "description": "應觸發自動追蹤的目標型別清單。"
+ },
+ "required_zones": {
+ "label": "必需區域",
+ "description": "目標必須進入這些區域之一才能開始自動追蹤。"
+ },
+ "return_preset": {
+ "label": "返回預設",
+ "description": "追蹤結束後返回的攝影機韌體中配置的 ONVIF 預設名稱。"
+ },
+ "timeout": {
+ "label": "返回超時",
+ "description": "失去追蹤後等待多少秒後將攝影機返回到預設位置。"
+ },
+ "movement_weights": {
+ "label": "移動權重",
+ "description": "由攝影機校準自動生成的校準值。請勿手動修改。"
+ },
+ "enabled_in_config": {
+ "label": "原始自動追蹤狀態",
+ "description": "用於追蹤配置中是否啟用自動追蹤的內部欄位。"
+ }
+ },
+ "ignore_time_mismatch": {
+ "label": "忽略時間不匹配",
+ "description": "忽略 ONVIF 通訊中攝影機和 Frigate 伺服器之間的時間同步差異。"
}
}
}
diff --git a/web/public/locales/zh-Hant/config/groups.json b/web/public/locales/zh-Hant/config/groups.json
index 0967ef424b..5180463b90 100644
--- a/web/public/locales/zh-Hant/config/groups.json
+++ b/web/public/locales/zh-Hant/config/groups.json
@@ -1 +1,73 @@
-{}
+{
+ "audio": {
+ "global": {
+ "detection": "全域性偵測",
+ "sensitivity": "全域性靈敏度"
+ },
+ "cameras": {
+ "detection": "偵測",
+ "sensitivity": "靈敏度"
+ }
+ },
+ "timestamp_style": {
+ "global": {
+ "appearance": "全域性外觀"
+ },
+ "cameras": {
+ "appearance": "外觀"
+ }
+ },
+ "motion": {
+ "global": {
+ "sensitivity": "全域性靈敏度",
+ "algorithm": "全域性演算法"
+ },
+ "cameras": {
+ "sensitivity": "靈敏度",
+ "algorithm": "演算法"
+ }
+ },
+ "snapshots": {
+ "global": {
+ "display": "全域性顯示"
+ },
+ "cameras": {
+ "display": "顯示"
+ }
+ },
+ "detect": {
+ "global": {
+ "resolution": "全域性解析度",
+ "tracking": "全域性追蹤"
+ },
+ "cameras": {
+ "resolution": "解析度",
+ "tracking": "追蹤"
+ }
+ },
+ "objects": {
+ "global": {
+ "tracking": "全域性追蹤",
+ "filtering": "全域性篩選"
+ },
+ "cameras": {
+ "tracking": "追蹤",
+ "filtering": "篩選"
+ }
+ },
+ "record": {
+ "global": {
+ "retention": "全域性保留",
+ "events": "全域性事件"
+ },
+ "cameras": {
+ "retention": "保留",
+ "events": "事件"
+ }
+ },
+ "ffmpeg": {
+ "cameras": {
+ "cameraFfmpeg": "攝影機特定的 FFmpeg 引數"
+ }
+ }
+}
diff --git a/web/public/locales/zh-Hant/config/validation.json b/web/public/locales/zh-Hant/config/validation.json
index 0967ef424b..c75f681df8 100644
--- a/web/public/locales/zh-Hant/config/validation.json
+++ b/web/public/locales/zh-Hant/config/validation.json
@@ -1 +1,35 @@
-{}
+{
+ "minimum": "必須至少為 {{limit}}",
+ "maximum": "最大值不能超過 {{limit}}",
+ "exclusiveMinimum": "必須大於 {{limit}}",
+ "exclusiveMaximum": "必須小於 {{limit}}",
+ "minLength": "長度至少為 {{limit}} 個字元",
+ "maxLength": "長度最多為 {{limit}} 個字元",
+ "minItems": "至少包含 {{limit}} 項",
+ "maxItems": "最多包含 {{limit}} 項",
+ "pattern": "格式無效",
+ "required": "此欄位為必填項",
+ "type": "值型別無效",
+ "enum": "必須是允許的值之一",
+ "const": "值與預期的常量不匹配",
+ "uniqueItems": "所有項必須唯一",
+ "format": "格式無效",
+ "additionalProperties": "不允許未知屬性",
+ "oneOf": "必須完全匹配一個允許的模式",
+ "anyOf": "必須至少匹配一個允許的模式",
+ "proxy": {
+ "header_map": {
+ "roleHeaderRequired": "設定角色對應時必須要有 role 標頭。"
+ }
+ },
+ "ffmpeg": {
+ "inputs": {
+ "rolesUnique": "每個角色只能分配給一個輸入串流。",
+ "detectRequired": "必須至少有一個輸入串流分配為 'detect' 角色。",
+ "hwaccelDetectOnly": "只有分配了 detect 角色的輸入串流才能定義硬體加速引數。"
+ }
+ },
+ "detect": {
+ "dimensionMustBeEven": "必須是偶數。"
+ }
+}
diff --git a/web/public/locales/zh-Hant/objects.json b/web/public/locales/zh-Hant/objects.json
index 092506cdd4..2dc4ad5e8f 100644
--- a/web/public/locales/zh-Hant/objects.json
+++ b/web/public/locales/zh-Hant/objects.json
@@ -116,5 +116,14 @@
"nzpost": "紐西蘭郵政(NZ Post)",
"postnord": "北歐郵政(PostNord)",
"gls": "GLS 快遞",
- "dpd": "DPD 快遞"
+ "dpd": "DPD 快遞",
+ "canada_post": "加拿大郵政",
+ "royal_mail": "英國皇家郵政",
+ "school_bus": "校車",
+ "skunk": "臭鼬",
+ "kangaroo": "袋鼠",
+ "baby": "嬰兒",
+ "baby_stroller": "嬰兒推車",
+ "rickshaw": "人力車",
+ "rodent": "齧齒動物"
}
diff --git a/web/public/locales/zh-Hant/views/chat.json b/web/public/locales/zh-Hant/views/chat.json
new file mode 100644
index 0000000000..af68b79f56
--- /dev/null
+++ b/web/public/locales/zh-Hant/views/chat.json
@@ -0,0 +1,72 @@
+{
+ "documentTitle": "聊天 - Frigate",
+ "title": "Frigate 聊天",
+ "subtitle": "你的攝影機管理與智慧分析 AI 助手",
+ "placeholder": "嘗試問我任何事…",
+ "error": "出現錯誤,請稍後重試。",
+ "processing": "進行中…",
+ "toolsUsed": "使用:{{tools}}",
+ "showTools": "顯示工具({{count}})",
+ "hideTools": "隱藏工具",
+ "call": "呼叫",
+ "result": "結果",
+ "arguments": "引數:",
+ "response": "回應:",
+ "attachment_chip_label": "在 {{camera}} 的 {{label}}",
+ "attachment_chip_remove": "移除附件",
+ "open_in_explore": "從瀏覽中開啟",
+ "attach_event_aria": "關聯事件 {{eventId}}",
+ "attachment_picker_paste_label": "或貼上事件 ID",
+ "attachment_picker_attach": "關聯",
+ "attachment_picker_placeholder": "關聯一個事件",
+ "quick_reply_find_similar": "查詢相似抓拍事件",
+ "quick_reply_tell_me_more": "瞭解更多詳情",
+ "quick_reply_when_else": "還在哪些時段出現過?",
+ "quick_reply_find_similar_text": "查詢與此相似的抓拍記錄。",
+ "quick_reply_tell_me_more_text": "瞭解此條更多詳情。",
+ "quick_reply_when_else_text": "還在哪些時間出現過?",
+ "anchor": "來源",
+ "similarity_score": "相似度",
+ "no_similar_objects_found": "未找到相似目標。",
+ "semantic_search_required": "必須啟用語意搜尋才能查詢相似目標。",
+ "send": "傳送",
+ "suggested_requests": "嘗試問問:",
+ "starting_requests": {
+ "show_recent_events": "檢視近期事件",
+ "show_camera_status": "顯示攝影機狀態",
+ "recap": "我不在的時候發生了什麼?",
+ "watch_camera": "監控攝影機活動"
+ },
+ "starting_requests_prompts": {
+ "show_recent_events": "顯示最近一小時的事件",
+ "show_camera_status": "我的攝影機當前狀態如何?",
+ "recap": "我不在的時候發生了什麼事?",
+ "watch_camera": "監控前門,有人出現就通知我"
+ },
+ "new_chat": "新對話",
+ "settings": {
+ "title": "對話設定",
+ "show_stats": {
+ "title": "顯示統計",
+ "desc": "顯示對話回應的產生速度與上下文大小。",
+ "while_generating": "產生時",
+ "always": "一律顯示"
+ },
+ "auto_scroll": {
+ "title": "自動捲動",
+ "desc": "隨新訊息到來自動跟進。"
+ }
+ },
+ "stats": {
+ "context": "{{tokens}} 個 token",
+ "tokens_per_second": "{{rate}} tokens/秒"
+ },
+ "reasoning": {
+ "active": "推論中…",
+ "show": "顯示推論過程",
+ "hide": "隱藏推論過程"
+ },
+ "thinking": {
+ "toggle": "切換顯示推論"
+ }
+}
diff --git a/web/public/locales/zh-Hant/views/classificationModel.json b/web/public/locales/zh-Hant/views/classificationModel.json
index 796495f691..c716e89444 100644
--- a/web/public/locales/zh-Hant/views/classificationModel.json
+++ b/web/public/locales/zh-Hant/views/classificationModel.json
@@ -1,14 +1,15 @@
{
"toast": {
"success": {
- "deletedImage_other": "已刪除的圖片",
+ "deletedImage_other": "已刪除 {{count}} 張圖片",
"deletedModel_other": "已成功刪除 {{count}} 個模型",
- "deletedCategory_other": "已刪除分類",
+ "deletedCategory_other": "已刪除 {{count}} 個類別",
"categorizedImage": "成功分類圖片",
"trainedModel": "訓練模型成功。",
"trainingModel": "已開始模型訓練。",
"updatedModel": "已更新模型配置",
- "renamedCategory": "成功修改分類名稱為{{name}}"
+ "renamedCategory": "成功修改分類名稱為{{name}}",
+ "reclassifiedImage": "成功重新分類圖片"
},
"error": {
"deleteImageFailed": "刪除失敗:{{errorMessage}}",
@@ -18,7 +19,8 @@
"trainingFailed": "模型訓練失敗。請至Frigate 日誌查看詳情。",
"trainingFailedToStart": "模型訓練啟動失敗: {{errorMessage}}",
"updateModelFailed": "模型更新失敗: {{errorMessage}}",
- "renameCategoryFailed": "類別重新命名失敗: {{errorMessage}}"
+ "renameCategoryFailed": "類別重新命名失敗: {{errorMessage}}",
+ "reclassifyFailed": "重新分類圖片失敗:{{errorMessage}}"
}
},
"documentTitle": "分類模型",
@@ -95,14 +97,80 @@
"namePlaceholder": "請輸入模型名稱...",
"type": "類別",
"typeState": "狀態",
- "typeObject": "物件"
+ "typeObject": "物件",
+ "classificationTypeDesc": "子標籤會為目標標籤新增附加文字(例如:“人員:美團”)。屬性是可搜尋的元資料,獨立儲存在目標的元資訊中。",
+ "classificationSubLabel": "子標籤",
+ "classificationAttribute": "屬性",
+ "classes": "類別",
+ "states": "狀態",
+ "classesTip": "瞭解類別",
+ "classesStateDesc": "定義攝影機區域內可能出現的不同狀態。例如:車庫門的“開啟”和“關閉”。",
+ "classesObjectDesc": "定義用於分類偵測目標的不同類別。例如:人員分類中的“快遞員”、“居民”、“陌生人”。",
+ "classPlaceholder": "請輸入分類名稱……",
+ "errors": {
+ "nameRequired": "模型名稱為必填項",
+ "nameLength": "模型名稱長度不能超過 64 個字元",
+ "nameOnlyNumbers": "模型名稱不能僅包含數字",
+ "classRequired": "至少需要一個類別",
+ "classesUnique": "類別名稱必須唯一",
+ "noneNotAllowed": "不能建立“none”(無標籤)類別",
+ "stateRequiresTwoClasses": "狀態模型至少需要兩個類別",
+ "objectLabelRequired": "請選擇一個目標標籤",
+ "objectTypeRequired": "請選擇一個目標標籤"
+ }
},
"steps": {
- "chooseExamples": "選擇範本"
+ "chooseExamples": "選擇範本",
+ "nameAndDefine": "名稱與定義",
+ "stateArea": "狀態區域"
+ },
+ "title": "建立新分類",
+ "step2": {
+ "description": "選擇攝影機,併為攝影機定義要監控的區域。模型將對這些區域的狀態進行分類。",
+ "cameras": "攝影機",
+ "selectCamera": "選擇攝影機",
+ "noCameras": "點選 + 符號新增攝影機",
+ "selectCameraPrompt": "從清單中選擇一個攝影機以定義其偵測區域"
+ },
+ "step3": {
+ "selectImagesPrompt": "選擇所有屬於 {{className}} 的圖片",
+ "selectImagesDescription": "點選影像進行選擇,完成該類別後點選“繼續”。",
+ "allImagesRequired_other": "請對所有圖片進行分類。還有 {{count}} 張圖片需要分類。",
+ "generating": {
+ "title": "正在生成樣本圖片",
+ "description": "Frigate 正在從錄影中提取代表性圖片。這可能需要一些時間……"
+ },
+ "training": {
+ "title": "正在訓練模型",
+ "description": "系統正在後臺訓練模型。你可以關閉此對話方塊,訓練完成後模型將自動開始執行。"
+ },
+ "retryGenerate": "重新生成",
+ "noImages": "未生成樣本影像",
+ "classifying": "正在分類與訓練……",
+ "trainingStarted": "已開始模型訓練",
+ "modelCreated": "模型建立成功。請在“最近分類”頁面為缺失的狀態新增圖片,然後訓練模型。",
+ "errors": {
+ "noCameras": "未配置攝影機",
+ "noObjectLabel": "未選擇目標標籤",
+ "generateFailed": "示例生成失敗:{{error}}",
+ "generationFailed": "生成失敗,請重試。",
+ "classifyFailed": "圖片分類失敗:{{error}}"
+ },
+ "generateSuccess": "樣本圖片生成成功",
+ "refreshExamples": "生成新示例",
+ "refreshConfirm": {
+ "title": "需要生成新示例?",
+ "description": "此操作將生成一組新的圖片,並清除所有選擇內容(包括之前的所有類別)。你需要為所有類別重新選擇示例。"
+ },
+ "missingStatesWarning": {
+ "title": "缺失分類示例",
+ "description": "並非所有類別都有示例。可嘗試生成新示例以查詢缺失的類別,或繼續該步驟,之後透過 “最近分類” 頁面新增圖片。"
+ }
}
},
"menu": {
- "states": "狀態"
+ "states": "狀態",
+ "objects": "目標"
},
"noModels": {
"object": {
@@ -111,7 +179,13 @@
"buttonText": "建立物件模型"
},
"state": {
- "description": "建立自訂模型,用於監控和分類特定攝影機區域的狀態變化。"
+ "description": "建立自訂模型,用於監控和分類特定攝影機區域的狀態變化。",
+ "title": "尚未建立狀態分類模型",
+ "buttonText": "建立狀態模型"
}
- }
+ },
+ "categorizeImageAs": "圖片分類為:",
+ "categorizeImage": "圖片分類",
+ "reclassifyImageAs": "重新分類圖片為:",
+ "reclassifyImage": "重新分類圖片"
}
diff --git a/web/public/locales/zh-Hant/views/events.json b/web/public/locales/zh-Hant/views/events.json
index 7d5b4d28c8..bbe3f4bbae 100644
--- a/web/public/locales/zh-Hant/views/events.json
+++ b/web/public/locales/zh-Hant/views/events.json
@@ -14,7 +14,9 @@
"description": "僅當該攝影機啟用錄製功能時,才能為該攝影機建立審查項目。"
}
},
- "timeline": "時間線",
+ "timeline": {
+ "label": "時間線"
+ },
"timeline.aria": "選擇時間線",
"events": {
"label": "事件",
@@ -24,7 +26,9 @@
"documentTitle": "審核 - Frigate",
"allCameras": "所有鏡頭",
"recordings": {
- "documentTitle": "錄影 - Frigate"
+ "documentTitle": "錄影 - Frigate",
+ "invalidSharedLink": "由於解析錯誤,無法開啟帶時間戳的錄製連結。",
+ "invalidSharedCamera": "由於攝影機未知或未獲授權,無法開啟帶時間戳的錄製連結。"
},
"calendarFilter": {
"last24Hours": "過去 24 小時"
@@ -63,5 +67,28 @@
"normalActivity": "正常",
"needsReview": "待審核",
"securityConcern": "安全隱憂",
- "select_all": "全選"
+ "select_all": "全選",
+ "motionSearch": {
+ "menuItem": "畫面變動搜尋",
+ "openMenu": "攝影機選項"
+ },
+ "motionPreviews": {
+ "menuItem": "檢視畫面變動預覽",
+ "title": "畫面變動預覽:{{camera}}",
+ "mobileSettingsTitle": "畫面變動預覽設定",
+ "mobileSettingsDesc": "調整播放速度和變暗程度,並選擇日期以僅檢視畫面變動的片段。",
+ "dim": "變暗",
+ "dimAria": "調整變暗強度",
+ "dimDesc": "增加變暗程度可以提高畫面變動區域的可見性。",
+ "speed": "速度",
+ "speedAria": "選擇預覽播放速度",
+ "speedDesc": "選擇預覽片段的播放速度。",
+ "back": "返回",
+ "empty": "沒有可用的預覽",
+ "noPreview": "預覽不可用",
+ "seekAria": "將 {{camera}} 播放器定位到 {{time}}",
+ "filter": "篩選",
+ "filterDesc": "選擇區域以僅顯示在這些區域中有畫面變動的片段。",
+ "filterClear": "清除"
+ }
}
diff --git a/web/public/locales/zh-Hant/views/explore.json b/web/public/locales/zh-Hant/views/explore.json
index 5987009635..6fa668056d 100644
--- a/web/public/locales/zh-Hant/views/explore.json
+++ b/web/public/locales/zh-Hant/views/explore.json
@@ -112,7 +112,8 @@
"attributes": "分類屬性",
"title": {
"label": "標題"
- }
+ },
+ "scoreInfo": "分數資訊"
},
"trackedObjectDetails": "追蹤物件詳情",
"type": {
@@ -221,12 +222,22 @@
"viewTrackingDetails": {
"label": "檢視追蹤詳細資訊",
"aria": "顯示追蹤詳細資訊"
+ },
+ "debugReplay": {
+ "label": "除錯回放",
+ "aria": "在除錯回放檢視中檢視此被追蹤物件"
+ },
+ "more": {
+ "aria": "更多"
}
},
"dialog": {
"confirmDelete": {
"title": "確認刪除",
- "desc": "刪除此追蹤物件將移除截圖、所有已保存的嵌入,以及所有相關的追蹤詳情。歷史記錄中的錄影不會 被刪除。 你確定要刪除嗎?"
+ "desc": "刪除此追蹤物件將會移除快照、已儲存的嵌入向量資料(embeddings),以及相關的追蹤詳情紀錄。歷史檢視中的錄影檔案 不會 被刪除。 確定要刪除嗎?"
+ },
+ "toast": {
+ "error": "刪除該追蹤目標時出錯:{{errorMessage}}"
}
},
"noTrackedObjects": "找不到追蹤物件",
@@ -268,7 +279,10 @@
"zones": "區域",
"ratio": "比例",
"score": "分數",
- "area": "面積"
+ "area": "面積",
+ "computedScore": "計算得分",
+ "topScore": "最高得分",
+ "toggleAdvancedScores": "切換高階分數"
}
},
"annotationSettings": {
@@ -294,5 +308,8 @@
},
"aiAnalysis": {
"title": "AI 分析"
+ },
+ "concerns": {
+ "label": "風險等級"
}
}
diff --git a/web/public/locales/zh-Hant/views/exports.json b/web/public/locales/zh-Hant/views/exports.json
index 3d3f9e87c6..0b376bcfd2 100644
--- a/web/public/locales/zh-Hant/views/exports.json
+++ b/web/public/locales/zh-Hant/views/exports.json
@@ -2,7 +2,9 @@
"search": "搜尋",
"documentTitle": "匯出 - Frigate",
"noExports": "找不到匯出內容",
- "deleteExport": "刪除匯出內容",
+ "deleteExport": {
+ "label": "刪除匯出"
+ },
"editExport": {
"saveExport": "儲存匯出內容",
"title": "重新命名匯出內容",
@@ -10,7 +12,10 @@
},
"toast": {
"error": {
- "renameExportFailed": "重新命名匯出內容失敗:{{errorMessage}}"
+ "renameExportFailed": "重新命名匯出內容失敗:{{errorMessage}}",
+ "assignCaseFailed": "更新案件分配失敗:{{errorMessage}}",
+ "caseSaveFailed": "儲存案件失敗:{{errorMessage}}",
+ "caseDeleteFailed": "刪除案件失敗:{{errorMessage}}"
}
},
"deleteExport.desc": "你確定要刪除 {{exportName}} 嗎?",
@@ -18,6 +23,106 @@
"shareExport": "分享匯出",
"downloadVideo": "下載影片",
"editName": "編輯名稱",
- "deleteExport": "刪除匯出"
+ "deleteExport": "刪除匯出",
+ "assignToCase": "加入案件",
+ "removeFromCase": "從案件中移除"
+ },
+ "headings": {
+ "cases": "案件",
+ "uncategorizedExports": "未分類匯出項"
+ },
+ "toolbar": {
+ "newCase": "新案件",
+ "addExport": "新匯出",
+ "editCase": "編輯案件",
+ "deleteCase": "刪除案件"
+ },
+ "deleteCase": {
+ "label": "刪除案件",
+ "desc": "你確定要刪除 {{caseName}} 嗎?",
+ "descKeepExports": "匯出檔案將繼續保留為未分類匯出。",
+ "descDeleteExports": "此案件中的所有匯出項都將被永久刪除。",
+ "deleteExports": "同時刪除匯出檔案"
+ },
+ "caseDialog": {
+ "title": "加入案件",
+ "description": "選擇現有案件或建立新案件。",
+ "selectLabel": "案件",
+ "newCaseOption": "建立新案件",
+ "nameLabel": "案件名稱",
+ "descriptionLabel": "描述"
+ },
+ "caseCard": {
+ "emptyCase": "暫無匯出檔案"
+ },
+ "jobCard": {
+ "defaultName": "{{camera}} 匯出",
+ "queued": "佇列中",
+ "running": "執行中",
+ "preparing": "準備中",
+ "copying": "複製中",
+ "encoding": "編碼中",
+ "encodingRetry": "重試編碼中",
+ "finalizing": "正在完成"
+ },
+ "caseView": {
+ "noDescription": "沒有描述",
+ "createdAt": "已建立 {{value}}",
+ "exportCount_one": "1 個匯出",
+ "exportCount_other": "{{count}} 個匯出",
+ "cameraCount_one": "1 個攝影機",
+ "cameraCount_other": "{{count}} 個攝影機",
+ "showMore": "顯示更多",
+ "showLess": "顯示更少",
+ "emptyTitle": "該案件為空",
+ "emptyDescription": "將現有未分類的匯出新增進來,以便整理該條目。",
+ "emptyDescriptionNoExports": "目前沒有可新增的未分類匯出項。"
+ },
+ "caseEditor": {
+ "createTitle": "建立案件",
+ "editTitle": "編輯案件",
+ "namePlaceholder": "案件名稱",
+ "descriptionPlaceholder": "為該案件新增備註或相關說明"
+ },
+ "addExportDialog": {
+ "title": "將匯出新增到 {{caseName}}",
+ "searchPlaceholder": "搜尋未分類的匯出項",
+ "empty": "未找到匹配的未分類匯出。",
+ "addButton_one": "新增 1 個匯出",
+ "addButton_other": "新增 {{count}} 個匯出",
+ "adding": "新增中…"
+ },
+ "selected_one": "已選擇 {{count}} 個",
+ "selected_other": "已選擇 {{count}} 個",
+ "bulkActions": {
+ "addToCase": "新增至案件",
+ "moveToCase": "移動至案件",
+ "removeFromCase": "從案件中移除",
+ "delete": "刪除",
+ "deleteNow": "立即刪除"
+ },
+ "bulkDelete": {
+ "title": "刪除匯出",
+ "desc_one": "你確定要刪除 {{count}} 個匯出嗎?",
+ "desc_other": "確定要刪除 {{count}} 個匯出嗎?"
+ },
+ "bulkRemoveFromCase": {
+ "title": "從案件中移除",
+ "desc_one": "你確定要從該案件中移除這 {{count}} 個匯出嗎?",
+ "desc_other": "你確定要從該案件中移除這 {{count}} 個匯出嗎?",
+ "descKeepExports": "匯出將被移至未分類。",
+ "descDeleteExports": "匯出將被永久刪除。",
+ "deleteExports": "選擇刪除匯出"
+ },
+ "bulkToast": {
+ "success": {
+ "delete": "已刪除匯出",
+ "reassign": "已更新案件分配",
+ "remove": "已從案件中移除匯出"
+ },
+ "error": {
+ "deleteFailed": "刪除匯出失敗:{{errorMessage}}",
+ "reassignFailed": "更新案件分配失敗:{{errorMessage}}"
+ }
}
}
diff --git a/web/public/locales/zh-Hant/views/faceLibrary.json b/web/public/locales/zh-Hant/views/faceLibrary.json
index 938bf15818..496e1631db 100644
--- a/web/public/locales/zh-Hant/views/faceLibrary.json
+++ b/web/public/locales/zh-Hant/views/faceLibrary.json
@@ -2,7 +2,8 @@
"description": {
"addFace": "上傳您的第一張照片至臉部資料庫以新增一個新的集合。",
"placeholder": "輸入此集合的名稱",
- "invalidName": "無效的名稱。名稱只能包涵英數字、空格、撇(')、底線(_)及連字號(-)。"
+ "invalidName": "無效的名稱。名稱只能包涵英數字、空格、撇(')、底線(_)及連字號(-)。",
+ "nameCannotContainHash": "名稱中不允許包含“#”符號。"
},
"details": {
"person": "人",
@@ -38,7 +39,11 @@
"title": "最近的識別紀錄",
"aria": "選擇最近的識別紀錄",
"empty": "最近沒有辨識人臉的操作",
- "titleShort": "最近"
+ "titleShort": "最近",
+ "emptyNoLibrary": {
+ "title": "上傳一張人臉",
+ "description": "您必須先在資料庫中加入至少一張人臉,才能使用人臉辨識功能。"
+ }
},
"selectFace": "選擇人臉",
"deleteFaceLibrary": {
@@ -82,7 +87,8 @@
"deletedName_other": "{{count}} 個人臉已成功刪除。",
"renamedFace": "成功將人臉重新命名為 {{name}}",
"trainedFace": "成功訓練人臉。",
- "updatedFaceScore": "成功更新人臉分數{{name}}({{score}})。"
+ "updatedFaceScore": "成功更新人臉分數{{name}}({{score}})。",
+ "reclassifiedFace": "重新分類人臉成功。"
},
"error": {
"uploadingImageFailed": "上傳圖片失敗:{{errorMessage}}",
@@ -91,7 +97,10 @@
"deleteNameFailed": "刪除名稱失敗:{{errorMessage}}",
"renameFaceFailed": "重新命名人臉失敗:{{errorMessage}}",
"trainFailed": "訓練失敗:{{errorMessage}}",
- "updateFaceScoreFailed": "更新人臉分數失敗:{{errorMessage}}"
+ "updateFaceScoreFailed": "更新人臉分數失敗:{{errorMessage}}",
+ "reclassifyFailed": "重新分類人臉失敗:{{errorMessage}}"
}
- }
+ },
+ "reclassifyFaceAs": "將人臉重新分類為:",
+ "reclassifyFace": "重新分類人臉"
}
diff --git a/web/public/locales/zh-Hant/views/live.json b/web/public/locales/zh-Hant/views/live.json
index a839b4b881..cb4ed938ee 100644
--- a/web/public/locales/zh-Hant/views/live.json
+++ b/web/public/locales/zh-Hant/views/live.json
@@ -1,5 +1,7 @@
{
- "documentTitle": "即時畫面 - Frigate",
+ "documentTitle": {
+ "default": "即時監控 - Frigate"
+ },
"documentTitle.withCamera": "{{camera}} - 即時畫面 - Frigate",
"lowBandwidthMode": "低流量模式",
"twoWayTalk": {
@@ -11,7 +13,8 @@
"clickMove": {
"label": "點擊畫面以置中鏡頭",
"enable": "啟用點擊移動",
- "disable": "停用點擊移動"
+ "disable": "停用點擊移動",
+ "enableWithZoom": "開啟點選移動 / 拖動縮放功能"
},
"left": {
"label": "向左移動 PTZ 鏡頭"
@@ -55,7 +58,9 @@
},
"camera": {
"enable": "啟用鏡頭",
- "disable": "停用鏡頭"
+ "disable": "停用鏡頭",
+ "turnOn": "開啟攝影機",
+ "turnOff": "關閉攝影機"
},
"muteCameras": {
"enable": "所有鏡頭靜音",
@@ -67,7 +72,8 @@
},
"recording": {
"enable": "啟用錄影",
- "disable": "停用錄影"
+ "disable": "停用錄影",
+ "disabledInConfig": "必須先在該攝影機的設定中開啟錄製功能。"
},
"snapshots": {
"enable": "啟用截圖",
@@ -134,6 +140,9 @@
"playInBackground": {
"label": "背景播放",
"tips": "啟用此選項以在播放器被隱藏時繼續播放串流。"
+ },
+ "debug": {
+ "picker": "除錯模式下無法切換影片流。除錯將始終使用偵測(detect)功能的影片流。"
}
},
"cameraSettings": {
@@ -143,7 +152,9 @@
"recording": "錄影",
"snapshots": "截圖",
"audioDetection": "音訊偵測",
- "autotracking": "自動追蹤"
+ "autotracking": "自動追蹤",
+ "transcription": "音訊轉錄",
+ "camera": "攝影機"
},
"history": {
"label": "顯示歷史影像"
@@ -172,5 +183,24 @@
"noVideoSource": "沒有可用的影片資源以擷取快照。",
"captureFailed": "快照擷取失敗。",
"downloadStarted": "已開始下載快照。"
+ },
+ "noCameras": {
+ "title": "未設定攝影機",
+ "description": "準備開始連線攝影機至 Frigate 。",
+ "buttonText": "新增攝影機",
+ "restricted": {
+ "title": "無可用攝影機",
+ "description": "你沒有權限檢視此分組中的任何攝影機。"
+ },
+ "default": {
+ "title": "沒有配置攝影機",
+ "description": "現在就將攝影機接入到 Frigate 吧。",
+ "buttonText": "新增攝影機"
+ },
+ "group": {
+ "title": "攝影機組目前為空",
+ "description": "該攝影機組未分配或啟動了攝影機。",
+ "buttonText": "管理攝影機組"
+ }
}
}
diff --git a/web/public/locales/zh-Hant/views/motionSearch.json b/web/public/locales/zh-Hant/views/motionSearch.json
new file mode 100644
index 0000000000..7ea301ccac
--- /dev/null
+++ b/web/public/locales/zh-Hant/views/motionSearch.json
@@ -0,0 +1,78 @@
+{
+ "documentTitle": "變動搜尋 - Frigate",
+ "title": "畫面變動搜尋",
+ "description": "繪製一個多邊形以劃定感興趣區域,並指定時間範圍,檢索該區域內的動態變化。",
+ "selectCamera": "畫面變動搜尋正在載入中",
+ "startSearch": "開始搜尋",
+ "searchStarted": "搜尋已開始",
+ "searchCancelled": "搜尋已取消",
+ "cancelSearch": "取消",
+ "searching": "搜尋進行中。",
+ "searchComplete": "搜尋完成",
+ "noResultsYet": "在所選區域內執行搜尋,查詢異常變化",
+ "noChangesFound": "所選區域未偵測到像素變化",
+ "changesFound_other": "偵測到 {{count}} 處畫面變化",
+ "framesProcessed": "已處理 {{count}} 幀畫面",
+ "jumpToTime": "跳轉到該時間",
+ "results": "結果",
+ "showSegmentHeatmap": "熱力圖",
+ "newSearch": "新的搜尋",
+ "clearResults": "清除結果",
+ "clearROI": "清除多邊形選區",
+ "polygonControls": {
+ "points_other": "{{count}} 個點位",
+ "undo": "撤銷上一個點位",
+ "reset": "重設多邊形",
+ "drawMode": "繪製",
+ "moveMode": "移動"
+ },
+ "motionHeatmapLabel": "畫面變動熱力圖",
+ "dialog": {
+ "title": "畫面變動搜尋",
+ "cameraLabel": "攝影機",
+ "previewAlt": "{{camera}} 攝影機即時預覽"
+ },
+ "timeRange": {
+ "title": "搜尋範圍",
+ "start": "開始時間",
+ "end": "結束時間"
+ },
+ "settings": {
+ "title": "搜尋設定",
+ "parallelMode": "並行模式",
+ "parallelModeDesc": "同時掃描多個錄影片段(速度更快,但會消耗更多解碼資源)",
+ "threshold": "靈敏度閾值",
+ "thresholdDesc": "數值越低,可偵測到越小的變化(取值範圍 1-255)",
+ "minArea": "最小變化區域",
+ "minAreaDesc": "最小感興趣區域變化佔比,達到該比例才會判定為有效變動",
+ "frameSkip": "幀跳過",
+ "frameSkipDesc": "每隔 N 幀進行一次處理。將該值設定為攝影機的幀率,即可實現每秒處理一幀畫面(例如:5 幀 / 秒的攝影機設為 5,30 幀 / 秒的攝影機設為 30)。數值越高處理速度越快,但有可能遺漏短時移動偵測事件。",
+ "maxResults": "最大結果數",
+ "maxResultsDesc": "匹配到設定條數的錄影事件後,就自動停止檢索"
+ },
+ "errors": {
+ "noCamera": "請選擇攝影機",
+ "noROI": "請繪製感興趣的區域",
+ "noTimeRange": "請選擇時間範圍",
+ "invalidTimeRange": "結束時間必須在開始時間之後",
+ "searchFailed": "搜尋失敗:{{message}}",
+ "polygonTooSmall": "多邊形至少需要 3 個頂點",
+ "unknown": "未知錯誤"
+ },
+ "changePercentage": "{{percentage}}% 已變化",
+ "metrics": {
+ "title": "搜尋指標",
+ "segmentsScanned": "已掃描片段數",
+ "segmentsProcessed": "已處理",
+ "segmentsSkippedInactive": "已跳過(無活動)",
+ "segmentsSkippedHeatmap": "已跳過(不在感興趣區域)",
+ "fallbackFullRange": "備用全範圍掃描",
+ "framesDecoded": "畫面已解碼",
+ "wallTime": "搜尋時間",
+ "segmentErrors": "片段異常",
+ "seconds": "{{seconds}} 秒",
+ "minutesSeconds": "{{minutes}}分 {{seconds}}秒",
+ "scanSummary": "{{segments}} 個片段 · {{time}}"
+ },
+ "scanning": "掃描中 {{time}}"
+}
diff --git a/web/public/locales/zh-Hant/views/replay.json b/web/public/locales/zh-Hant/views/replay.json
new file mode 100644
index 0000000000..afe2cee4c6
--- /dev/null
+++ b/web/public/locales/zh-Hant/views/replay.json
@@ -0,0 +1,59 @@
+{
+ "title": "除錯回放",
+ "description": "回放攝影機錄影以供除錯。目標清單會延時展示已偵測目標的彙總資訊,訊息分頁則即時展示回放錄影對應的 Frigate 內部日誌資訊流。",
+ "websocket_messages": "訊息",
+ "dialog": {
+ "title": "開始除錯回放",
+ "description": "建立臨時回放攝影機,迴圈播放歷史錄製影片,用於除錯目標偵測與追蹤相關問題。臨時回放的攝影機將沿用原攝影機的偵測配置。請選擇一個時間範圍開始。",
+ "camera": "原攝影機",
+ "timeRange": "時間範圍",
+ "preset": {
+ "1m": "最後 1 分鐘",
+ "5m": "最後 5 分鐘",
+ "timeline": "從時間線",
+ "custom": "自訂"
+ },
+ "startButton": "開始回放",
+ "selectFromTimeline": "選擇",
+ "starting": "開始回放…",
+ "startLabel": "開始",
+ "endLabel": "結束",
+ "toast": {
+ "error": "除錯回放啟動失敗:{{error}}",
+ "alreadyActive": "已有回放工作階段正在執行",
+ "stopError": "除錯回放停止失敗:{{error}}",
+ "goToReplay": "進入回放"
+ }
+ },
+ "page": {
+ "noSession": "沒有正在進行的除錯回放工作階段",
+ "noSessionDesc": "從歷史回放頁面啟動除錯回放:點選工具列中的操作按鈕,選擇除錯回放即可。",
+ "goToRecordings": "檢視歷史記錄",
+ "preparingClip": "正在準備片段…",
+ "preparingClipDesc": "Frigate 正在拼接所選時間範圍的錄影片段。時間跨度較大時,該過程可能需要一分鐘左右。",
+ "startingCamera": "開始除錯回放中…",
+ "startError": {
+ "title": "除錯回放啟動失敗",
+ "back": "返回歷史記錄"
+ },
+ "sourceCamera": "源攝影機",
+ "replayCamera": "回放攝影機",
+ "initializingReplay": "初始化除錯回放中…",
+ "stoppingReplay": "正在停止除錯回放…",
+ "stopReplay": "停止回放",
+ "confirmStop": {
+ "title": "要停止除錯回放嗎?",
+ "description": "這將終止工作階段並清除所有臨時資料。是否確定?",
+ "confirm": "停止回放",
+ "cancel": "取消"
+ },
+ "activity": "活動",
+ "objects": "目標清單",
+ "audioDetections": "音訊偵測",
+ "noActivity": "未偵測到活動",
+ "activeTracking": "活動追蹤中",
+ "noActiveTracking": "沒有活動追蹤",
+ "configuration": "配置",
+ "configurationDesc": "微調除錯回放攝影機的移動偵測與目標追蹤引數。本次調整不會儲存到你的 Frigate 設定檔中。"
+ }
+}
diff --git a/web/public/locales/zh-Hant/views/settings.json b/web/public/locales/zh-Hant/views/settings.json
index 97829f5360..2d25590493 100644
--- a/web/public/locales/zh-Hant/views/settings.json
+++ b/web/public/locales/zh-Hant/views/settings.json
@@ -11,7 +11,12 @@
"motionTuner": "移動偵測調教器 - Frigate",
"object": "除錯 - Frigate",
"cameraManagement": "管理鏡頭 - Frigate",
- "cameraReview": "相機預覽設置 - Frigate"
+ "cameraReview": "相機預覽設置 - Frigate",
+ "globalConfig": "全域性配置 - Frigate",
+ "cameraConfig": "攝影機配置 - Frigate",
+ "detectorsAndModel": "偵測器與模型 - Frigate",
+ "maintenance": "維護 - Frigate",
+ "profiles": "設定檔 - Frigate"
},
"menu": {
"ui": "使用者介面",
@@ -26,7 +31,65 @@
"triggers": "觸發",
"cameraManagement": "管理",
"cameraReview": "預覽",
- "roles": "角色"
+ "roles": "角色",
+ "general": "常規",
+ "globalConfig": "全域性配置",
+ "system": "系統",
+ "integrations": "整合",
+ "uiSettings": "介面設定",
+ "profiles": "設定檔",
+ "globalDetect": "目標偵測",
+ "globalRecording": "錄製",
+ "globalSnapshots": "快照",
+ "globalFfmpeg": "FFmpeg",
+ "globalMotion": "畫面變動偵測",
+ "globalObjects": "目標",
+ "globalReview": "審閱",
+ "globalAudioEvents": "音訊偵測",
+ "globalLivePlayback": "即時監控觀看",
+ "globalTimestampStyle": "時間戳樣式",
+ "systemDatabase": "資料庫",
+ "systemTls": "TLS加密連結",
+ "systemAuthentication": "驗證",
+ "systemNetworking": "網路",
+ "systemProxy": "代理",
+ "systemUi": "使用者介面",
+ "systemLogging": "日誌",
+ "systemEnvironmentVariables": "環境變數",
+ "systemTelemetry": "遙測",
+ "systemBirdseye": "鳥瞰圖",
+ "systemFfmpeg": "FFmpeg",
+ "systemDetectorsAndModel": "偵測器與模型",
+ "systemMqtt": "MQTT",
+ "systemGo2rtcStreams": "go2rtc 影片流",
+ "integrationSemanticSearch": "語意搜尋",
+ "integrationGenerativeAi": "生成式 AI",
+ "integrationFaceRecognition": "人臉辨識",
+ "integrationLpr": "車牌辨識",
+ "integrationObjectClassification": "目標分類",
+ "integrationAudioTranscription": "音訊轉錄",
+ "cameraDetect": "目標偵測",
+ "cameraFfmpeg": "FFmpeg",
+ "cameraRecording": "錄製",
+ "cameraSnapshots": "快照",
+ "cameraMotion": "畫面變動偵測",
+ "cameraObjects": "目標",
+ "cameraConfigReview": "審閱",
+ "cameraAudioEvents": "音訊偵測",
+ "cameraAudioTranscription": "音訊轉錄",
+ "cameraNotifications": "通知",
+ "cameraLivePlayback": "即時監控觀看",
+ "cameraBirdseye": "鳥瞰圖",
+ "cameraFaceRecognition": "人臉辨識",
+ "cameraLpr": "車牌辨識",
+ "cameraMqttConfig": "MQTT",
+ "cameraOnvif": "ONVIF",
+ "cameraUi": "攝影機頁面",
+ "cameraTimestampStyle": "時間戳樣式",
+ "cameraMqtt": "攝影機 MQTT",
+ "maintenance": "維護",
+ "mediaSync": "媒體同步",
+ "regionGrid": "區域網格"
},
"dialog": {
"unsavedChanges": {
@@ -103,22 +166,56 @@
"modelSize": {
"label": "模型大小",
"small": {
- "title": "小"
+ "title": "小",
+ "desc": "使用 small 將採用量化版本的模型。該模型佔用的記憶體(RAM)較少,在 CPU 上的執行速度也較快,且在嵌入向量品質上的差異微乎其微。"
+ },
+ "desc": "用於語意搜尋的語言模型大小。",
+ "large": {
+ "title": "大",
+ "desc": "使用 large 將採用完整的 Jina 模型,並在支援的情況下自動於 GPU 上執行。"
}
},
"title": "語意搜尋",
"desc": "Frigate 中的語意搜尋功能可讓您使用圖像本身、使用者定義的文字描述或自動產生的描述,在審核專案中尋找追蹤物件。",
"reindexNow": {
"label": "立即重新索引",
- "desc": "重新索引會為所有追蹤物件重新產生嵌入向量。此過程在背景運行,可能會佔用大量 CPU 資源,並且耗時較長,具體取決於追蹤物件的數量。"
+ "desc": "重新索引會為所有追蹤物件重新產生嵌入向量。此過程在背景運行,可能會佔用大量 CPU 資源,並且耗時較長,具體取決於追蹤物件的數量。",
+ "confirmTitle": "確認重建索引",
+ "confirmDesc": "確定要為所有追蹤目標重建特徵向量索引資訊嗎?此過程將在後臺進行,但可能會導致CPU滿載並耗費較長時間。您可以在 瀏覽 頁面檢視進度。",
+ "confirmButton": "重建索引",
+ "success": "重建索引已成功啟動。",
+ "alreadyInProgress": "重建索引已在執行中。",
+ "error": "啟動重建索引失敗:{{errorMessage}}"
}
},
"faceRecognition": {
- "title": "人臉識別"
+ "title": "人臉識別",
+ "desc": "人臉辨識功能允許為人物分配名稱,當辨識到他們的面孔時,Frigate 會將人物的名字作為子標籤進行分配。這些資訊會顯示在介面、過濾器以及通知中。",
+ "modelSize": {
+ "label": "模型大小",
+ "desc": "用於人臉辨識的模型大小。",
+ "small": {
+ "title": "小",
+ "desc": "使用 小 模型將採用 FaceNet 臉部嵌入模型,可在大多數 CPU 上高效執行。"
+ },
+ "large": {
+ "title": "大",
+ "desc": "使用 大 模型將採用 ArcFace 臉部嵌入模型,並在適用情況下自動於 GPU 上執行。"
+ }
+ }
},
"birdClassification": {
"title": "鳥類分類",
"desc": "鳥類分類功能使用量化的 TensorFlow 模型識別已知鳥類。識別出已知鳥類後,其通用名稱將作為子標籤添加。此資訊會顯示在使用者介面、篩選器以及通知中。"
+ },
+ "licensePlateRecognition": {
+ "title": "車牌辨識",
+ "desc": "Frigate 可以辨識車輛的車牌,並自動將偵測到的字元新增到 辨識的車牌(recognized_license_plate)欄位中,或將已知車牌對應的名稱作為子標籤新增到該車輛目標中。該功能常用於辨識駛入車道的車輛車牌或經過街道的車輛車牌。"
+ },
+ "restart_required": "需要重啟(增強功能設定已儲存)",
+ "toast": {
+ "success": "增強功能設定已儲存。請重啟 Frigate 以應用更改。",
+ "error": "配置更改儲存失敗:{{errorMessage}}"
}
},
"cameraWizard": {
@@ -126,10 +223,12 @@
"testResultLabels": {
"resolution": "解析度",
"video": "影像",
- "audio": "語音"
+ "audio": "語音",
+ "fps": "幀率"
},
"commonErrors": {
- "testFailed": "串流測試失敗: {{error}}"
+ "testFailed": "串流測試失敗: {{error}}",
+ "noUrl": "請提供正確的影片流地址"
},
"step1": {
"description": "輸入相機詳細資訊並選擇自動偵測或手動選擇相機品牌。",
@@ -142,15 +241,1728 @@
"password": "密碼",
"passwordPlaceholder": "選填",
"selectTransport": "選擇協議",
- "cameraBrand": "相機品牌"
+ "cameraBrand": "相機品牌",
+ "selectBrand": "選擇攝影機品牌用於生成URL地址模板",
+ "customUrl": "自訂影片流地址",
+ "brandInformation": "品牌資訊",
+ "brandUrlFormat": "對於採用RTSP URL格式的攝影機,其格式為:{{exampleUrl}}",
+ "customUrlPlaceholder": "rtsp://使用者名稱:密碼@主機或ip位址:通訊埠/路徑",
+ "connectionSettings": "連線設定",
+ "detectionMethod": "影片流偵測方法",
+ "onvifPort": "ONVIF 埠",
+ "probeMode": "探測攝影機",
+ "manualMode": "手動選擇",
+ "detectionMethodDescription": "如果攝影機支援 ONVIF 協議,將使用該協議探測攝影機,以自動獲取攝影機影片流地址;若不支援,也可手動選擇攝影機品牌來使用預設地址。如需輸入自訂RTSP地址,請選擇“手動選擇”並選擇“其他”選項。",
+ "onvifPortDescription": "對於支援ONVIF協議的攝影機,該埠通常為80或8080。",
+ "useDigestAuth": "使用摘要認證",
+ "useDigestAuthDescription": "為 ONVIF 協議啟用 HTTP 摘要認證。部分攝影機可能需要專用的 ONVIF 使用者名稱/密碼,而非預設的 admin 帳戶。",
+ "errors": {
+ "brandOrCustomUrlRequired": "請選擇攝影機品牌並配置主機/ IP 地址,或選擇“其他”後手動配置影片流地址",
+ "nameRequired": "攝影機名稱為必填項",
+ "nameLength": "攝影機名稱要少於64個字元",
+ "invalidCharacters": "攝影機名稱內有不允許使用的字元",
+ "nameExists": "該攝影機名稱已存在",
+ "customUrlRtspRequired": "自訂 URL 必須以“rtsp://”開頭;對於非 RTSP 協議的攝影機流,需手動新增至設定檔。"
+ }
+ },
+ "description": "請按照以下步驟新增攝影機至 Frigate 中。",
+ "steps": {
+ "nameAndConnection": "名稱與連線",
+ "probeOrSnapshot": "探測或快照",
+ "streamConfiguration": "影片流配置",
+ "validationAndTesting": "驗證與測試"
+ },
+ "save": {
+ "success": "已儲存新攝影機 {{cameraName}}。",
+ "failure": "儲存攝影機 {{cameraName}} 遇到了錯誤。"
+ },
+ "step2": {
+ "description": "將根據你選擇的偵測方式,將會自動查詢攝影機可用流配置,或進行手動配置。",
+ "testSuccess": "影片流測試成功!",
+ "testFailed": "連線測試失敗,請檢查您的輸入後重試。",
+ "testFailedTitle": "測試失敗",
+ "streamDetails": "影片流詳情",
+ "probing": "正在偵測攝影機中……",
+ "retry": "重試",
+ "testing": {
+ "probingMetadata": "正在查詢攝影機引數……",
+ "fetchingSnapshot": "正在獲取攝影機快照……"
+ },
+ "probeFailed": "偵測攝影機失敗:{{error}}",
+ "probingDevice": "尋找裝置中……",
+ "probeSuccessful": "偵測成功",
+ "probeError": "偵測遇到錯誤",
+ "probeNoSuccess": "偵測未成功",
+ "deviceInfo": "裝置資訊",
+ "manufacturer": "製造商",
+ "model": "型號",
+ "firmware": "韌體",
+ "profiles": "設定檔",
+ "ptzSupport": "支援 PTZ",
+ "autotrackingSupport": "支援自動追蹤",
+ "presets": "預設配置",
+ "rtspCandidates": "RTSP候選地址",
+ "rtspCandidatesDescription": "透過攝影機自動偵測發現了以下RTSP地址。測試連線以檢視影片流引數。",
+ "noRtspCandidates": "未從攝影機偵測到任何 RTSP 地址。可能是你的帳號密碼錯誤,或者攝影機不支援 ONVIF 協議,亦或是當前採用的 RTSP 地址獲取方式無效。請返回上一步,嘗試手動輸入RTSP地址。",
+ "candidateStreamTitle": "候選{{number}}",
+ "useCandidate": "使用",
+ "uriCopy": "複製",
+ "uriCopied": "地址已複製到剪貼簿",
+ "testConnection": "測試連線",
+ "toggleUriView": "點選切換完整 URI 顯示",
+ "connected": "已連線",
+ "notConnected": "未連線",
+ "errors": {
+ "hostRequired": "主機/IP地址為必填"
+ }
+ },
+ "step3": {
+ "description": "為你的攝影機配置影片流功能並新增額外影片流。",
+ "streamsTitle": "攝影機影片流",
+ "addStream": "新增影片流",
+ "addAnotherStream": "新增其他影片流",
+ "streamTitle": "{{number}} 號影片流",
+ "streamUrl": "影片流地址",
+ "streamUrlPlaceholder": "rtsp://使用者名稱:密碼@主機:埠/路徑",
+ "selectStream": "選擇一個影片流",
+ "searchCandidates": "搜尋候選項……",
+ "noStreamFound": "沒有找到影片流",
+ "url": "URL地址",
+ "resolution": "解析度",
+ "selectResolution": "選擇解析度",
+ "quality": "品質",
+ "selectQuality": "選擇品質",
+ "roles": "功能",
+ "roleLabels": {
+ "detect": "目標偵測",
+ "record": "錄製",
+ "audio": "音訊偵測"
+ },
+ "testStream": "測試連線",
+ "testSuccess": "影片流測試成功!",
+ "testFailed": "影片流測試失敗",
+ "testFailedTitle": "測試失敗",
+ "connected": "已連線",
+ "notConnected": "未連線",
+ "featuresTitle": "功能特性",
+ "go2rtc": "減少與攝影機的連線數",
+ "detectRoleWarning": "必須得有一個影片流設定了“偵測”功能才能繼續操作。",
+ "rolesPopover": {
+ "title": "影片流功能",
+ "detect": "用於目標偵測的主碼流。",
+ "record": "根據配置設定儲存影片流片段。",
+ "audio": "用於音訊偵測的音影片流。"
+ },
+ "featuresPopover": {
+ "title": "影片流功能特性",
+ "description": "使用 go2rtc 中繼轉流功能,減少與攝影機的網路連線數,提升效率。"
+ }
+ },
+ "step4": {
+ "description": "將進行儲存新攝影機配置前的最終驗證與分析,請在儲存前確保所有影片流均已連線。",
+ "validationTitle": "影片流驗證",
+ "connectAllStreams": "連線所有影片流",
+ "reconnectionSuccess": "重新連線成功。",
+ "reconnectionPartial": "部分影片流重新連線失敗。",
+ "streamUnavailable": "影片流預覽不可用",
+ "reload": "重新載入",
+ "connecting": "連線中……",
+ "streamTitle": "影片流 {{number}}",
+ "valid": "透過",
+ "failed": "失敗",
+ "notTested": "未測試",
+ "connectStream": "連線",
+ "connectingStream": "連線中",
+ "disconnectStream": "斷開連線",
+ "estimatedBandwidth": "預估頻寬",
+ "roles": "功能",
+ "ffmpegModule": "使用影片流相容模式",
+ "ffmpegModuleDescription": "若多次嘗試後仍無法載入影片流,可嘗試啟用此功能。啟用後,Frigate 將透過 go2rtc 呼叫 ffmpeg 模組。這可能會提升與部分攝影機影片流的相容性。",
+ "none": "無",
+ "error": "錯誤",
+ "streamValidated": "影片流 {{number}} 驗證成功",
+ "streamValidationFailed": "影片流 {{number}} 驗證失敗",
+ "saveAndApply": "儲存新攝影機",
+ "saveError": "配置無效,請檢查您的設定。",
+ "issues": {
+ "title": "影片流驗證",
+ "videoCodecGood": "影片編解碼器為 {{codec}}。",
+ "audioCodecGood": "音訊編解碼器為 {{codec}}。",
+ "resolutionHigh": "使用 {{resolution}} 解析度可能導致資源使用率增加。",
+ "resolutionLow": "{{resolution}} 解析度可能過低,難以可靠偵測小型目標或物體。",
+ "resolutionUnknown": "無法偵測此影片流的解析度。你需要在設定或設定檔中手動指定偵測解析度。",
+ "noAudioWarning": "偵測到該影片流無音訊訊號,錄製影片將沒有聲音。",
+ "audioCodecRecordError": "錄製功能需要 AAC 音訊編解碼器以實現音訊支援。",
+ "audioCodecRequired": "要實現音訊偵測功能,必須要有音訊流。",
+ "restreamingWarning": "為錄製流開啟“減少與攝影機的連線數”可能會略微增加 CPU 使用率。",
+ "brands": {
+ "reolink-rtsp": "不建議使用 Reolink 的 RTSP 協議。請在攝影機後臺設定中啟用 HTTP協議,並重新啟動向導。",
+ "reolink-http": "Reolink HTTP 影片流應該使用 FFmpeg 以獲得更好的相容性,為此影片流啟用“使用流相容模式”。"
+ },
+ "dahua": {
+ "substreamWarning": "子碼流1當前被鎖定為低解析度。多數大華、安訊士、EmpireTech品牌的攝影機都支援額外的子碼流,這些子碼流需要在攝影機設定中手動啟用。如果你的裝置支援,建議你檢查並使用這些高解析度子碼流。"
+ },
+ "hikvision": {
+ "substreamWarning": "子碼流1當前被鎖定為低解析度。多數海康威視的攝影機都支援額外的子碼流,這些子碼流需要在攝影機設定中手動啟用。如果你的裝置支援,建議你檢查並使用這些高解析度子碼流。"
+ }
+ }
}
},
"triggers": {
"toast": {
"error": {
"deleteTriggerFailed": "刪除觸發器失敗:{{errorMessage}}",
- "updateTriggerFailed": "更新觸發器失敗:{{errorMessage}}"
+ "updateTriggerFailed": "更新觸發器失敗:{{errorMessage}}",
+ "createTriggerFailed": "建立觸發器失敗:{{errorMessage}}"
+ },
+ "success": {
+ "createTrigger": "觸發器 {{name}} 建立成功。",
+ "updateTrigger": "觸發器 {{name}} 更新成功。",
+ "deleteTrigger": "觸發器 {{name}} 已刪除。"
+ }
+ },
+ "documentTitle": "觸發器",
+ "semanticSearch": {
+ "title": "語意搜尋已關閉",
+ "desc": "必須啟用語意搜尋功能才能使用觸發器。"
+ },
+ "management": {
+ "title": "觸發器",
+ "desc": "管理 {{camera}} 的觸發器。你可以選擇“縮圖”型別,將透過與追蹤目標相似的縮圖來觸發;也可以透過“描述”型別,與你描述的文字相似來觸發(中文描述需要使用 jina v2模型,對配置要求更高)。"
+ },
+ "addTrigger": "新增觸發器",
+ "table": {
+ "name": "名稱",
+ "type": "型別",
+ "content": "觸發內容",
+ "threshold": "閾值",
+ "actions": "動作",
+ "noTriggers": "此攝影機未配置任何觸發器。",
+ "edit": "編輯",
+ "deleteTrigger": "刪除觸發器",
+ "lastTriggered": "最後一個觸發項"
+ },
+ "type": {
+ "thumbnail": "縮圖",
+ "description": "描述"
+ },
+ "actions": {
+ "notification": "傳送通知",
+ "sub_label": "新增子標籤",
+ "attribute": "新增屬性"
+ },
+ "dialog": {
+ "createTrigger": {
+ "title": "建立觸發器",
+ "desc": "為攝影機 {{camera}} 建立觸發器"
+ },
+ "editTrigger": {
+ "title": "編輯觸發器",
+ "desc": "編輯攝影機 {{camera}} 的觸發器設定"
+ },
+ "deleteTrigger": {
+ "title": "刪除觸發器",
+ "desc": "你確定要刪除觸發器 {{triggerName}} 嗎?此操作不可撤銷。"
+ },
+ "form": {
+ "name": {
+ "title": "名稱",
+ "placeholder": "觸發器名稱",
+ "description": "請輸入用於辨識此觸發器的唯一名稱或描述",
+ "error": {
+ "minLength": "該欄位至少需要兩個字元。",
+ "invalidCharacters": "該欄位只能包含字母、數字、下劃線和連字元。",
+ "alreadyExists": "此攝影機已存在同名觸發器。"
+ }
+ },
+ "enabled": {
+ "description": "開啟/關閉此觸發器"
+ },
+ "type": {
+ "title": "型別",
+ "placeholder": "選擇觸發型別",
+ "description": "當偵測到相似的追蹤目標描述時觸發",
+ "thumbnail": "當偵測到相似的追蹤目標縮圖時觸發"
+ },
+ "content": {
+ "title": "內容",
+ "imagePlaceholder": "選擇圖片",
+ "textPlaceholder": "輸入文字內容",
+ "imageDesc": "僅顯示最近的 100 張縮圖。如果找不到需要的圖片,請前往“瀏覽”頁面檢視更早的目標,並從選單中設定觸發器。",
+ "textDesc": "輸入文字,當偵測到相似的追蹤目標描述時觸發此操作。",
+ "error": {
+ "required": "內容為必填項。"
+ }
+ },
+ "threshold": {
+ "title": "閾值",
+ "desc": "設定此觸發器的相似度閾值。閾值越高,觸發所需的匹配就越精確。",
+ "error": {
+ "min": "閾值必須大於 0",
+ "max": "閾值必須小於 1"
+ }
+ },
+ "actions": {
+ "title": "動作",
+ "desc": "預設情況下,Frigate 會為所有觸發器傳送 MQTT 訊息。子標籤會將觸發器名稱新增到目標標籤中。屬性是可搜尋的元資料,獨立儲存在追蹤目標的元資料中。",
+ "error": {
+ "min": "必須至少選擇一項動作。"
+ }
+ }
+ }
+ },
+ "wizard": {
+ "title": "建立觸發器",
+ "step1": {
+ "description": "配置觸發器的基礎設定。"
+ },
+ "step2": {
+ "description": "設定觸發此操作的內容。"
+ },
+ "step3": {
+ "description": "配置此觸發器的相似度閾值與執行動作。"
+ },
+ "steps": {
+ "nameAndType": "名稱與型別",
+ "configureData": "配置資料",
+ "thresholdAndActions": "閾值與動作"
}
}
+ },
+ "button": {
+ "overriddenGlobal": "已覆蓋全域性通用配置",
+ "overriddenGlobalTooltip": "當前攝影機配置,將優先覆蓋全域性通用設定",
+ "overriddenGlobalHeading_other": "此攝影機覆蓋了全域性設定中的 {{count}} 個欄位:",
+ "overriddenGlobalNoDeltas": "此攝影機覆蓋了全域性設定,但所有欄位值都相同。",
+ "overriddenBaseConfig": "已覆蓋預設配置",
+ "overriddenBaseConfigTooltip": "當前 {{profile}} 設定檔會覆蓋本節所有設定",
+ "overriddenBaseConfigHeading_other": "{{profile}} 設定檔覆蓋了基礎設定中的 {{count}} 個欄位:",
+ "overriddenBaseConfigNoDeltas": "{{profile}} 設定檔覆蓋了此區段,但所有欄位值與基礎設定相同。",
+ "overriddenInCameras": {
+ "label_other": "已在 {{count}} 個攝影機中單獨配置",
+ "tooltip_other": "{{count}} 個攝影機在此項中存在單獨配置,點選檢視詳情。",
+ "heading_other": "此全域性設定項下有 {{count}} 個攝影機存在自訂單獨配置。",
+ "othersField_other": "其餘 {{count}} 個",
+ "profilePrefix": "{{profile}} 配置方案:{{fields}}"
+ }
+ },
+ "saveAllPreview": {
+ "title": "未儲存的更改",
+ "triggerLabel": "檢視待處理的更改",
+ "empty": "沒有待處理的更改。",
+ "scope": {
+ "label": "作用範圍",
+ "global": "全域性",
+ "camera": "攝影機:{{cameraName}}"
+ },
+ "profile": {
+ "label": "配置"
+ },
+ "field": {
+ "label": "欄位"
+ },
+ "value": {
+ "label": "新值",
+ "reset": "重設"
+ }
+ },
+ "cameraManagement": {
+ "title": "管理攝影機",
+ "description": "新增、編輯和刪除攝影機,控制哪些攝影機已啟用,並設定按設定檔與攝影機類型的覆蓋。若要設定串流、偵測、動作及其他攝影機特定設定,請在「攝影機設定」下選擇對應的區段。",
+ "addCamera": "新增新攝影機",
+ "deleteCamera": "刪除攝影機",
+ "deleteCameraDialog": {
+ "title": "刪除攝影機",
+ "description": "刪除攝影機將永久移除該攝影機的所有錄影、跟蹤目標以及配置。任何與該攝影機關聯的 go2rtc 流可能仍需手動刪除。",
+ "selectPlaceholder": "選擇攝影機…",
+ "confirmTitle": "你確定嗎?",
+ "confirmWarning": "刪除 {{cameraName}} 後將無法撤銷。",
+ "deleteExports": "同時刪除該攝影機匯出的影片",
+ "confirmButton": "永久刪除",
+ "success": "攝影機 {{cameraName}} 刪除完成",
+ "error": "刪除攝影機 {{cameraName}} 失敗"
+ },
+ "editCamera": "編輯攝影機:",
+ "selectCamera": "選擇攝影機",
+ "backToSettings": "返回攝影機設定",
+ "streams": {
+ "title": "開啟或關閉攝影機",
+ "enableLabel": "開啟攝影機",
+ "enableDesc": "暫時停用已開啟的攝影機,直到 Frigate 重啟。停用攝影機會完全停止 Frigate 對該攝影機影片流的處理。偵測、錄影和除錯功能將不可用。 注意:這不會停用 go2rtc 的轉推流。 ",
+ "disableLabel": "關閉攝影機",
+ "disableDesc": "開啟在當前在介面中不可見且在配置中被停用的攝影機。啟用後需要重啟 Frigate 才能生效。",
+ "enableSuccess": "已在配置中啟用 {{cameraName}}。請重啟 Frigate 以應用更改。",
+ "friendlyName": {
+ "edit": "修改攝影機顯示名稱",
+ "title": "修改顯示名稱",
+ "description": "設定該攝像機在 Frigate 使用者介面中顯示的名稱。若留空,則使用攝像機 ID。",
+ "rename": "重新命名"
+ },
+ "label": "攝影機狀態",
+ "description": "設定各攝影機的運作狀態。開啟 :正常處理串流。關閉 :暫時暫停處理。重啟 Frigate 後不會保留此狀態。停用 :停止處理並將變更儲存至您的設定中。需要重新啟動才能重新啟用已停用的攝影機。備註:停用不會影響 go2rtc 的轉流功能。 拖曳控制鍵可調整現用攝影機在整個 UI 中顯示的順序,包括即時儀表板和攝影機選擇下拉選單。",
+ "disabledSubheading": "在設定中禁用",
+ "status": {
+ "on": "開啟",
+ "off": "關閉",
+ "disabled": "已停用"
+ },
+ "disableSuccess": "已停用 {{cameraName}} 並儲存至設定。",
+ "reorderHandle": "拖曳以重新排序",
+ "saving": "儲存中…",
+ "saved": "已儲存",
+ "details": {
+ "edit": "編輯攝影機詳細資料",
+ "title": "編輯攝影機詳細資料",
+ "description": "更新此攝影機在整個 Frigate UI 中使用的顯示名稱、外部 URL 和可見性。",
+ "friendlyNameLabel": "顯示名稱",
+ "friendlyNameHelp": "在整個 Frigate UI 中顯示此攝影機的顯示名稱。留空則會使用攝影機 ID。",
+ "webuiUrlLabel": "攝影機網頁介面網址",
+ "webuiUrlHelp": "可從「偵錯」檢視中直接造訪攝影機網頁介面的網址。留空以停用此連結。",
+ "webuiUrlInvalid": "必須是有效的網址(例如:https://example.com)。",
+ "dashboardLabel": "在即時儀表板上顯示",
+ "dashboardHelp": "在即時儀表板上顯示此攝影機。",
+ "reviewLabel": "在審查中顯示",
+ "reviewHelp": "在「審查」中顯示此攝影機,包括攝影機篩選器、移動審查以及歷史檢視。"
+ }
+ },
+ "cameraConfig": {
+ "add": "新增攝影機",
+ "edit": "編輯攝影機",
+ "description": "配置攝影機設定,包括影片流輸入和功能選擇。",
+ "name": "攝影機名稱",
+ "nameRequired": "攝影機名稱為必填項",
+ "nameLength": "攝影機名稱必須少於64個字元。",
+ "namePlaceholder": "例如:大門、後院等",
+ "enabled": "開啟",
+ "ffmpeg": {
+ "inputs": "影片流輸入",
+ "path": "影片流地址",
+ "pathRequired": "影片流地址為必填項",
+ "pathPlaceholder": "rtsp://...",
+ "roles": "功能",
+ "rolesRequired": "至少選擇一個功能",
+ "rolesUnique": "每個功能(音訊audio、偵測detect、錄製record)只能分配給一個影片流",
+ "addInput": "新增輸入影片流",
+ "removeInput": "移除輸入影片流",
+ "inputsRequired": "至少需要一個輸入影片流"
+ },
+ "go2rtcStreams": "go2rtc 影片流",
+ "streamUrls": "影片流地址",
+ "addUrl": "新增地址",
+ "addGo2rtcStream": "新增 go2rtc 影片流",
+ "toast": {
+ "success": "攝影機 {{cameraName}} 已儲存"
+ }
+ },
+ "profiles": {
+ "title": "設定檔的攝影機覆蓋項",
+ "selectLabel": "選擇設定檔",
+ "description": "配置在啟用某個設定檔時,哪些攝影機應被開啟或關閉。設定為“繼承”的攝影機會沿用它原本的啟用/停用狀態。",
+ "inherit": "繼承",
+ "enabled": "開啟",
+ "disabled": "關閉",
+ "on": "開啟",
+ "off": "關閉"
+ },
+ "cameraType": {
+ "title": "攝影機型別",
+ "label": "攝影機型別",
+ "description": "為每路攝影機設定型別。專用車牌辨識(LPR)攝影機為單用途裝置,配備高倍光學變焦,可抓拍遠處車輛的車牌。絕大多數攝影機應選用通用型別;只有專為車牌辨識部署、且畫面聚焦對準車牌的攝影機,才需選擇專用 LPR 型別。",
+ "normal": "通用",
+ "dedicatedLpr": "車牌辨識專用",
+ "saveSuccess": "已更新 {{cameraName}} 的攝影機型別,請重啟 Frigate 以使更改生效。"
+ },
+ "clone": {
+ "sectionTitle": "複製設定",
+ "sectionDescription": "將攝影機的設定複製到另一台攝影機或新攝影機。",
+ "button": "複製設定",
+ "title": "複製攝影機設定",
+ "description": "將攝影機的設定複製到一或多台其他攝影機,或是新攝影機。識別資料(名稱、顯示名稱、網頁介面網址、顯示順序)將不會被複製。",
+ "source": {
+ "label": "來源攝影機",
+ "placeholder": "選擇一個來源攝影機",
+ "required": "選擇一個來源攝影機"
+ },
+ "target": {
+ "legend": "目標",
+ "newRadio": "新攝影機",
+ "newNameLabel": "攝影機名稱",
+ "newNamePlaceholder": "例如:back_door 或 Back Door",
+ "newNameRequired": "必須填寫攝影機名稱",
+ "newNameInvalid": "無效的攝影機名稱",
+ "newNameCollision": "已存在相同名稱的攝影機",
+ "newStreamsForced": "建立新攝影機時,一律會複製串流設定。",
+ "existingCamerasRadio": "現有攝影機",
+ "allCameras": "所有攝影機",
+ "existingPlaceholder": "請至少選擇一台攝影機",
+ "existingDisabled": "沒有其他攝影機可供複製"
+ },
+ "categories": {
+ "legend": "要複製的設定",
+ "description": "選擇要從來源攝影機複製哪些設定。",
+ "selectAll": "全選",
+ "selectNone": "取消全選",
+ "resetDefaults": "重設為預設值",
+ "general": "一般",
+ "spatial": "空間設定",
+ "streams": "串流",
+ "spatialWarningTitle": "解析度不符",
+ "spatialWarning": "來源攝影機 {{srcCamera}} 的偵測解析度 ({{srcWidth}}×{{srcHeight}}) 與以下攝影機不符:{{cameras}}。這些攝影機上的多邊形(偵測區域)可能無法對齊。此為預設關閉;啟用即可按原樣複製。",
+ "restartHint": "需要重新啟動",
+ "items": {
+ "record": "錄影",
+ "snapshots": "快照",
+ "review": "審核",
+ "motion": "移動偵測",
+ "objects": "物件",
+ "audio": "聲音偵測",
+ "audio_transcription": "語音轉文字",
+ "notifications": "通知",
+ "birdseye": "鳥瞰圖",
+ "mqtt": "MQTT",
+ "timestamp_style": "時間戳記樣式",
+ "onvif": "ONVIF",
+ "lpr": "車牌辨識",
+ "face_recognition": "人臉辨識",
+ "semantic_search": "語意搜尋",
+ "genai": "生成式 AI",
+ "type": "攝影機類型(一般 / 車牌識別專用)",
+ "profiles": "設定檔",
+ "detect": "偵測解析度",
+ "zones": "區域",
+ "motion_mask": "移動偵測遮罩",
+ "object_masks": "物件遮罩",
+ "ffmpeg_live": "串流網址與用途"
+ }
+ },
+ "footer": {
+ "changeCount_other": "將套用 {{count}} 項變更",
+ "restartNeeded": "部分變更需要重新啟動。",
+ "liveOnly": "所有變更將即時生效,無需重新啟動。",
+ "submit": "複製",
+ "submitting": "複製中…"
+ },
+ "toast": {
+ "success": "設定已複製到 {{cameraName}}",
+ "successWithRestart": "設定已複製到 {{cameraName}}。請重新啟動 Frigate 以套用所有變更。",
+ "successMulti_other": "設定已複製到 {{count}} 台攝影機",
+ "successMultiWithRestart_other": "設定已複製到 {{count}} 台攝影機。請重新啟動 Frigate 以套用所有變更。",
+ "partialFailure": "已套用 {{successCount}} 個區段;「{{failedSection}}」失敗:{{errorMessage}}",
+ "partialFailureMulti": "設定已複製到 {{successCount}} 台攝影機;{{failed}} 失敗:{{errorMessage}}",
+ "newCameraPartialFailure": "攝影機 {{cameraName}} 已建立,但部分設定複製失敗:{{errorMessage}}",
+ "sourceMissing": "來源攝影機已不存在",
+ "submitError": "無法複製攝影機:{{errorMessage}}"
+ }
+ }
+ },
+ "cameraReview": {
+ "title": "攝影機審閱設定",
+ "object_descriptions": {
+ "title": "生成式AI目標描述",
+ "desc": "臨時啟用或停用此攝影機的 生成式AI目標描述 功能,直到 Frigate 重啟。停用後,系統將不再請求該攝影機追蹤目標和物體的AI生成描述。"
+ },
+ "review_descriptions": {
+ "title": "生成式 AI 審閱總結",
+ "desc": "臨時開關該攝影機的 生成式 AI 審閱總結 功能,直到 Frigate 重啟。停用後,系統將不再請求 AI 生成該攝影機審閱項目的總結。"
+ },
+ "review": {
+ "title": "審閱",
+ "desc": "臨時開關該攝影機的警報與偵測項生成功能,直到 Frigate 重啟後恢復。停用期間,系統將不再生成新的審閱項目。 ",
+ "alerts": "警報 ",
+ "detections": "偵測 "
+ },
+ "reviewClassification": {
+ "title": "審閱分類",
+ "desc": "Frigate 將審查項目分類為警報與偵測。預設情況下,所有 人 與 汽車 物件都會被視為警報。您可以透過為其設定所需的區域,來進一步精確分類您的審查項目。",
+ "noDefinedZones": "此攝影機未設定任何監控區。",
+ "objectAlertsTips": "所有 {{alertsLabels}} 類目標或物體在 {{cameraName}} 下都將視為警報。",
+ "zoneObjectAlertsTips": "所有 {{alertsLabels}} 類目標或物體在 {{cameraName}} 下的 {{zone}} 區域內都將視為警報。",
+ "objectDetectionsTips": "所有在攝影機 {{cameraName}} 上,偵測到的 {{detectionsLabels}} 目標或物體,無論它位於哪個區,都將顯示為偵測。",
+ "zoneObjectDetectionsTips": {
+ "text": "所有在攝影機 {{cameraName}} 下的 {{zone}} 區域內偵測到未分類的 {{detectionsLabels}} 目標或物體,都將顯示為偵測。",
+ "notSelectDetections": "所有在攝影機 {{cameraName}}下的 {{zone}} 區域內偵測到的 {{detectionsLabels}} 目標或物體,如果它未歸類為警報,無論它位於哪個區,都將顯示為偵測。",
+ "regardlessOfZoneObjectDetectionsTips": "在攝影機 {{cameraName}} 上,所有未分類的 {{detectionsLabels}} 偵測目標或物體,無論出現在哪個區域,都將顯示為偵測。"
+ },
+ "unsavedChanges": "攝影機 {{camera}} 的審閱分類設定尚未儲存",
+ "selectAlertsZones": "選擇警報區",
+ "selectDetectionsZones": "選擇偵測區",
+ "limitDetections": "限制僅在特定區域內進行偵測",
+ "toast": {
+ "success": "審閱分類設定已儲存,重啟後生效。"
+ }
+ }
+ },
+ "masksAndZones": {
+ "filter": {
+ "all": "所有遮罩和區域"
+ },
+ "restart_required": "需要重啟(遮罩與區域已修改)",
+ "disabledInConfig": "該項目已在設定檔中被停用",
+ "addDisabledProfile": "先新增到基礎配置中,然後在設定檔中進行覆蓋",
+ "profileBase": "(基礎)",
+ "profileOverride": "(覆蓋)",
+ "toast": {
+ "success": {
+ "copyCoordinates": "已複製 {{polyName}} 的座標到剪貼簿。"
+ },
+ "error": {
+ "copyCoordinatesFailed": "無法複製座標到剪貼簿。"
+ }
+ },
+ "motionMaskLabel": "畫面變動遮罩 {{number}}",
+ "objectMaskLabel": "目標/物體遮罩 {{number}}",
+ "form": {
+ "id": {
+ "error": {
+ "mustNotBeEmpty": "ID 不能為空。",
+ "alreadyExists": "此攝影機已存在使用該 ID 的遮罩。"
+ }
+ },
+ "name": {
+ "error": {
+ "mustNotBeEmpty": "名稱不能為空。"
+ }
+ },
+ "zoneName": {
+ "error": {
+ "mustBeAtLeastTwoCharacters": "區域名稱必須至少包含 2 個字元。",
+ "mustNotBeSameWithCamera": "區域名稱不能與攝影機名稱相同。",
+ "alreadyExists": "該攝影機已有相同的區域名稱。",
+ "mustNotContainPeriod": "區域名稱不能包含句點。",
+ "hasIllegalCharacter": "區域名稱包含非法字元。",
+ "mustHaveAtLeastOneLetter": "區域名稱必須至少包含一個字母。"
+ }
+ },
+ "distance": {
+ "error": {
+ "text": "距離必須大於或等於 0.1。",
+ "mustBeFilled": "所有距離欄位必須填寫才能使用速度估算。"
+ }
+ },
+ "inertia": {
+ "error": {
+ "mustBeAboveZero": "慣性必須大於 0。"
+ }
+ },
+ "loiteringTime": {
+ "error": {
+ "mustBeGreaterOrEqualZero": "徘徊時間必須大於或等於 0。"
+ }
+ },
+ "speed": {
+ "error": {
+ "mustBeGreaterOrEqualTo": "速度閾值必須大於或等於0.1。"
+ }
+ },
+ "polygonDrawing": {
+ "type": {
+ "zone": "區域",
+ "motion_mask": "畫面變動遮罩",
+ "object_mask": "目標遮罩"
+ },
+ "removeLastPoint": "刪除最後一個點",
+ "reset": {
+ "label": "清除所有點"
+ },
+ "snapPoints": {
+ "true": "啟用點對齊",
+ "false": "停用點對齊"
+ },
+ "delete": {
+ "title": "確認刪除",
+ "desc": "你確定要刪除 {{type}} {{name}} 嗎?",
+ "success": "{{name}} 已被刪除。"
+ },
+ "revertOverride": {
+ "title": "恢復為預設配置",
+ "desc": "這將移除針對 {{type}} {{name}} 的配置覆蓋,並恢復為基礎配置。"
+ },
+ "error": {
+ "mustBeFinished": "多邊形繪製必須完成閉合後才能儲存。"
+ }
+ }
+ },
+ "zones": {
+ "label": "區域",
+ "documentTitle": "編輯區域 - Frigate",
+ "desc": {
+ "title": "該功能允許你定義特定區域,以便你可以確定特定目標或物體是否在該區域內。",
+ "documentation": "文件"
+ },
+ "add": "新增區域",
+ "edit": "編輯區域",
+ "point_other": "{{count}} 點",
+ "clickDrawPolygon": "在影像上點選新增點繪製多邊形區域。",
+ "name": {
+ "title": "區域名稱",
+ "inputPlaceHolder": "請輸入名稱…",
+ "tips": "名稱至少包含兩個字元,且不能和攝影機名或該攝影機下的其他區域同名。"
+ },
+ "enabled": {
+ "title": "開啟",
+ "description": "指示該區域在設定檔中是否處於啟用並啟用的狀態。若被停用,則無法透過 MQTT 啟用。停用的區域在執行時會被忽略。"
+ },
+ "inertia": {
+ "title": "慣性",
+ "desc": "辨識指定目標前該目標必須在這個區域內出現了多少幀。預設值:3 "
+ },
+ "loiteringTime": {
+ "title": "停留時間",
+ "desc": "設定目標必須在區域中至少要活動多少時間(單位為秒)。預設值:0 "
+ },
+ "objects": {
+ "title": "目標/物體",
+ "desc": "將在此區域應用的目標/物體類別清單。"
+ },
+ "allObjects": "所有目標/物體",
+ "speedEstimation": {
+ "title": "速度估算",
+ "desc": "啟用此區域內物體的速度估算。該區域必須恰好包含 4 個點。",
+ "lineADistance": "A線距離({{unit}})",
+ "lineBDistance": "B線距離({{unit}})",
+ "lineCDistance": "C線距離({{unit}})",
+ "lineDDistance": "D線距離({{unit}})"
+ },
+ "speedThreshold": {
+ "title": "速度閾值 ({{unit}})",
+ "desc": "指定物體在此區域內被視為有效的最低速度。",
+ "toast": {
+ "error": {
+ "pointLengthError": "此區域的速度估算已停用。啟用速度估算的區域必須恰好包含 4 個點。",
+ "loiteringTimeError": "徘徊時間大於 0 的區域不應與速度估算一起使用。"
+ }
+ }
+ },
+ "toast": {
+ "success": "區域 ({{zoneName}}) 已儲存。"
+ }
+ },
+ "motionMasks": {
+ "label": "畫面變動遮罩",
+ "documentTitle": "編輯畫面變動遮罩 - Frigate",
+ "desc": {
+ "title": "畫面變動遮罩用於防止觸發不必要的畫面變動偵測。過度的設定遮罩將使目標更加難以被追蹤。",
+ "documentation": "文件"
+ },
+ "add": "新增畫面變動遮罩",
+ "edit": "編輯畫面變動遮罩",
+ "defaultName": "畫面變動遮罩 {{number}}",
+ "context": {
+ "title": "移動遮罩用於防止不需要的移動觸發偵測(例如:樹枝搖晃、攝影機時間戳記等)。移動遮罩應 非常謹慎地使用 ,過度的遮罩會導致物件追蹤變得更加困難。"
+ },
+ "point_other": "{{count}} 點",
+ "clickDrawPolygon": "在影像上點選新增點繪製多邊形區域。",
+ "name": {
+ "title": "名稱",
+ "description": "為該畫面變動遮罩設定別名(可選)。",
+ "placeholder": "輸入名稱…"
+ },
+ "polygonAreaTooLarge": {
+ "title": "畫面變動遮罩的大小達到了攝影機畫面的{{polygonArea}}%。不建議設定太大的畫面變動遮罩。",
+ "tips": "畫面變動遮罩並不會使該區域無法偵測到指定目標/物體,如有需要,你應該使用 區域 來限制偵測的目標/物體型別。"
+ },
+ "toast": {
+ "success": {
+ "title": "{{polygonName}} 已儲存。",
+ "noName": "畫面變動遮罩已儲存。"
+ }
+ }
+ },
+ "objectMasks": {
+ "label": "目標遮罩",
+ "documentTitle": "編輯目標遮罩 - Frigate",
+ "desc": {
+ "title": "目標過濾器用於防止特定位置出現對某個目標/物體的誤報。",
+ "documentation": "文件"
+ },
+ "add": "新增目標遮罩",
+ "edit": "編輯目標遮罩",
+ "context": "目標過濾器用於防止特定位置的指定目標會誤報。",
+ "point_other": "{{count}} 點",
+ "clickDrawPolygon": "在影像上點選新增點繪製多邊形區域。",
+ "name": {
+ "title": "名稱",
+ "description": "為該目標遮罩設定別名(可選)。",
+ "placeholder": "輸入名稱…"
+ },
+ "objects": {
+ "title": "目標/物體",
+ "desc": "將應用於此目標遮罩的目標或物體型別。",
+ "allObjectTypes": "所有目標或物體型別"
+ },
+ "toast": {
+ "success": {
+ "title": "{{polygonName}} 已儲存。",
+ "noName": "目標遮罩已儲存。"
+ }
+ }
+ },
+ "masks": {
+ "enabled": {
+ "title": "開啟",
+ "description": "指示該遮罩在設定檔中是否處於啟用並啟用的狀態。若被停用,則無法透過 MQTT 啟用。停用的遮罩在執行時會被忽略。"
+ }
+ }
+ },
+ "motionDetectionTuner": {
+ "title": "畫面變動偵測調整",
+ "unsavedChanges": "{{camera}} 的畫面變動調整設定未儲存",
+ "desc": {
+ "title": "Frigate 將首先使用畫面變動偵測來確認每一幀畫面中是否有變動的區域,然後再對該區域使用目標偵測。",
+ "documentation": "閱讀有關畫面變動偵測的文件"
+ },
+ "Threshold": {
+ "title": "閾值",
+ "desc": "閾值決定像素亮度變化達到多少時會被認為是畫面變動。預設值:30 "
+ },
+ "contourArea": {
+ "title": "輪廓面積",
+ "desc": "輪廓面積值用於判斷產生了多大的變化區域可被認定為畫面變動。預設值:10 "
+ },
+ "improveContrast": {
+ "title": "提高對比度",
+ "desc": "提高較暗場景的對比度。預設值:啟用 "
+ },
+ "toast": {
+ "success": "畫面變動設定已儲存。"
+ }
+ },
+ "debug": {
+ "title": "除錯",
+ "detectorDesc": "Frigate 將使用偵測器({{detectors}})來偵測攝影機影片流中的目標或物體。",
+ "desc": "除錯介面將即時顯示被追蹤的目標以及統計資訊,目標清單將顯示偵測到的目標和延遲顯示的概覽。",
+ "openCameraWebUI": "開啟 {{camera}} 的管理頁面",
+ "debugging": "除錯選項",
+ "objectList": "目標清單",
+ "noObjects": "沒有目標",
+ "audio": {
+ "title": "音訊",
+ "noAudioDetections": "未偵測到音訊事件",
+ "score": "分值",
+ "currentRMS": "當前均方根值(RMS)",
+ "currentdbFS": "當前滿量程相對分貝值(dbFS)"
+ },
+ "boundingBoxes": {
+ "title": "邊界框",
+ "desc": "將在被追蹤的目標周圍顯示邊界框",
+ "colors": {
+ "label": "目標邊界框顏色定義",
+ "info": "啟動時,系統會為每個物件標籤指派不同的顏色 深藍色細線代表該物件目前未被偵測到 灰色細線代表該物件被偵測為靜止狀態 粗線代表該物件正被自動追蹤(若已啟用) "
+ }
+ },
+ "timestamp": {
+ "title": "時間戳",
+ "desc": "在影像上顯示時間戳"
+ },
+ "zones": {
+ "title": "區域",
+ "desc": "顯示已定義的區域圖層"
+ },
+ "mask": {
+ "title": "畫面變動遮罩",
+ "desc": "顯示畫面變動遮罩圖層"
+ },
+ "motion": {
+ "title": "畫面變動區域框",
+ "desc": "在偵測到畫面變動的區域顯示區域框",
+ "tips": "畫面變動區域框
將在當前偵測到畫面變動的區域內顯示紅色區域框。
"
+ },
+ "regions": {
+ "title": "範圍",
+ "desc": "顯示傳送給目標偵測器感興趣的區域框",
+ "tips": "範圍框
將在幀中傳送到目標偵測器的感興趣範圍上疊加綠色框。
"
+ },
+ "paths": {
+ "title": "行動軌跡",
+ "desc": "顯示被追蹤目標的行動軌跡關鍵點",
+ "tips": "行動軌跡
將使用線條 和點 來標示被追蹤目標在其活動週期內移動的關鍵位置點。
"
+ },
+ "objectShapeFilterDrawing": {
+ "title": "允許繪製“目標形狀過濾器”",
+ "desc": "在影像上繪製矩形,以檢視區域和比例詳細資訊",
+ "tips": "啟用此選項,能夠在攝影機畫面上繪製矩形,將顯示其區域和比例。你可以透過使用這些值在配置中設定目標形狀過濾器的引數。",
+ "score": "分數",
+ "ratio": "比例",
+ "area": "區域"
+ }
+ },
+ "timestampPosition": {
+ "tl": "左上角",
+ "tr": "右上角",
+ "bl": "左下角",
+ "br": "右下角"
+ },
+ "users": {
+ "title": "使用者",
+ "management": {
+ "title": "使用者管理",
+ "desc": "管理此 Frigate 例項的使用者帳戶。"
+ },
+ "addUser": "新增使用者",
+ "updatePassword": "修改密碼",
+ "toast": {
+ "success": {
+ "createUser": "使用者 {{user}} 建立成功",
+ "deleteUser": "使用者 {{user}} 刪除成功",
+ "updatePassword": "已成功修改密碼。",
+ "roleUpdated": "已更新 {{user}} 的權限組"
+ },
+ "error": {
+ "setPasswordFailed": "儲存密碼出現錯誤:{{errorMessage}}",
+ "createUserFailed": "建立使用者失敗:{{errorMessage}}",
+ "deleteUserFailed": "刪除使用者失敗:{{errorMessage}}",
+ "roleUpdateFailed": "更新權限組失敗:{{errorMessage}}"
+ }
+ },
+ "table": {
+ "username": "使用者名稱",
+ "actions": "操作",
+ "role": "權限組",
+ "noUsers": "未找到使用者。",
+ "changeRole": "更改使用者權限組",
+ "password": "修改密碼",
+ "deleteUser": "刪除使用者"
+ },
+ "dialog": {
+ "form": {
+ "user": {
+ "title": "使用者名稱",
+ "desc": "僅允許使用字母、數字、句點和下劃線。",
+ "placeholder": "請輸入使用者名稱"
+ },
+ "password": {
+ "title": "密碼",
+ "placeholder": "請輸入密碼",
+ "show": "顯示密碼",
+ "hide": "隱藏密碼",
+ "confirm": {
+ "title": "確認密碼",
+ "placeholder": "請再次輸入密碼"
+ },
+ "strength": {
+ "title": "密碼強度: ",
+ "weak": "弱",
+ "medium": "中等",
+ "strong": "強",
+ "veryStrong": "非常強"
+ },
+ "requirements": {
+ "title": "密碼要求:",
+ "length": "至少需要 12 位字元"
+ },
+ "match": "密碼匹配",
+ "notMatch": "密碼不匹配"
+ },
+ "newPassword": {
+ "title": "新密碼",
+ "placeholder": "請輸入新密碼",
+ "confirm": {
+ "placeholder": "請再次輸入新密碼"
+ }
+ },
+ "currentPassword": {
+ "title": "當前密碼",
+ "placeholder": "請輸入當前密碼"
+ },
+ "usernameIsRequired": "使用者名稱為必填項",
+ "passwordIsRequired": "必須輸入密碼"
+ },
+ "createUser": {
+ "title": "建立新使用者",
+ "desc": "建立一個新使用者帳戶,並指定一個權限組以控制存取 Frigate 頁面的權限。",
+ "usernameOnlyInclude": "使用者名稱只能包含字母、數字和 _",
+ "confirmPassword": "請確認你的密碼"
+ },
+ "deleteUser": {
+ "title": "刪除該使用者",
+ "desc": "此操作無法撤銷。這將永久刪除使用者帳戶並移除所有相關資料。",
+ "warn": "你確定要刪除 {{username}} 嗎?"
+ },
+ "passwordSetting": {
+ "cannotBeEmpty": "密碼不能為空",
+ "doNotMatch": "兩次輸入密碼不匹配",
+ "currentPasswordRequired": "當前密碼為必填",
+ "incorrectCurrentPassword": "當前密碼錯誤",
+ "passwordVerificationFailed": "驗證密碼失敗",
+ "updatePassword": "更新 {{username}} 的密碼",
+ "setPassword": "設定密碼",
+ "desc": "建立一個強密碼來保護此帳戶。",
+ "multiDeviceWarning": "其他已登入的裝置將需要在 {{refresh_time}} 內重新登入。",
+ "multiDeviceAdmin": "你也可以透過輪換你的 JWT 金鑰,強制所有使用者立即重新登入驗證。"
+ },
+ "changeRole": {
+ "title": "更改使用者權限組",
+ "select": "選擇權限組",
+ "desc": "更新 {{username}} 的權限",
+ "roleInfo": {
+ "intro": "為該使用者選擇一個合適的權限組:",
+ "admin": "管理員",
+ "adminDesc": "完全功能與存取權限。",
+ "viewer": "成員",
+ "viewerDesc": "僅能夠檢視即時監控面板、審閱、瀏覽和匯出功能。",
+ "customDesc": "自訂特定攝影機的存取規則。"
+ }
+ }
+ }
+ },
+ "roles": {
+ "management": {
+ "title": "成員權限組管理",
+ "desc": "管理此 Frigate 例項的自訂權限組及其攝影機存取權限。"
+ },
+ "addRole": "新增權限組",
+ "table": {
+ "role": "權限組",
+ "cameras": "攝影機",
+ "actions": "操作",
+ "noRoles": "沒有找到自訂權限組。",
+ "editCameras": "編輯攝影機",
+ "deleteRole": "刪除權限組"
+ },
+ "toast": {
+ "success": {
+ "createRole": "權限組 {{role}} 建立成功",
+ "updateCameras": "已更新攝影機至 {{role}} 權限組",
+ "deleteRole": "已刪除 {{role}} 權限組",
+ "userRolesUpdated_other": "已將分配到此權限組的 {{count}} 位使用者更新為 “成員”,該權限組可存取所有攝影機。"
+ },
+ "error": {
+ "createRoleFailed": "建立權限組失敗:{{errorMessage}}",
+ "updateCamerasFailed": "更新攝影機失敗:{{errorMessage}}",
+ "deleteRoleFailed": "刪除權限組失敗:{{errorMessage}}",
+ "userUpdateFailed": "更新使用者權限組失敗:{{errorMessage}}"
+ }
+ },
+ "dialog": {
+ "createRole": {
+ "title": "建立新權限組",
+ "desc": "新增新權限組並分配攝影機存取權限。"
+ },
+ "editCameras": {
+ "title": "編輯權限組的攝影機",
+ "desc": "為權限組 {{role}} 更新攝影機存取權限。"
+ },
+ "deleteRole": {
+ "title": "刪除權限組",
+ "desc": "此操作無法撤銷。這將永久刪除該權限組,並將所有擁有此權限組的使用者分配到 “成員” (view)權限組,該權限組將賦予使用者檢視所有攝影機的權限。",
+ "warn": "你確定要刪除權限組 {{role}} 嗎?",
+ "deleting": "刪除中…"
+ },
+ "form": {
+ "role": {
+ "title": "權限組名稱",
+ "placeholder": "輸入權限組名稱",
+ "desc": "僅允許使用字母、數字、句點和下劃線。",
+ "roleIsRequired": "必須輸入權限組名稱",
+ "roleOnlyInclude": "權限組名稱僅支援字母、數字、英文句號和下劃線",
+ "roleExists": "該權限組名稱已存在。"
+ },
+ "cameras": {
+ "title": "攝影機",
+ "desc": "請選擇該權限組能夠存取的攝影機。至少需要選擇一個攝影機。",
+ "required": "至少要選擇一個攝影機。"
+ }
+ }
+ }
+ },
+ "notification": {
+ "title": "通知",
+ "notificationSettings": {
+ "title": "通知設定",
+ "desc": "Frigate 在瀏覽器中執行或作為 PWA 安裝時,可以原生向您的裝置傳送推送通知。"
+ },
+ "notificationUnavailable": {
+ "title": "通知功能不可用",
+ "desc": "網頁推送通知需要安全連線(https://…)。這是瀏覽器的限制。請透過安全方式存取 Frigate 以使用通知功能。",
+ "descPwa": "在 iOS 上,只有將 Frigate 加到主畫面後,才能使用網頁推播通知。請開啟 分享 選單,選擇 加入主畫面 ,然後從新圖示開啟 Frigate,即可為此裝置註冊通知功能。"
+ },
+ "globalSettings": {
+ "title": "全域性設定",
+ "desc": "臨時暫停所有已註冊裝置上特定攝影機的通知。"
+ },
+ "email": {
+ "title": "電子郵箱",
+ "placeholder": "例如:example@email.com",
+ "desc": "需要輸入有效的電子郵件,在推送服務出現問題時,將使用此電子郵件進行通知。"
+ },
+ "cameras": {
+ "title": "攝影機",
+ "noCameras": "沒有可用的攝影機",
+ "desc": "選擇要啟用通知的攝影機。"
+ },
+ "deviceSpecific": "裝置專用設定",
+ "registerDevice": "註冊該裝置",
+ "unregisterDevice": "取消註冊該裝置",
+ "sendTestNotification": "傳送測試通知",
+ "unsavedRegistrations": "未儲存通知註冊",
+ "unsavedChanges": "未儲存通知設定更改",
+ "active": "通知已啟用",
+ "suspended": "通知已暫停 {{time}}",
+ "suspendTime": {
+ "suspend": "暫停",
+ "5minutes": "暫停 5 分鐘",
+ "10minutes": "暫停 10 分鐘",
+ "30minutes": "暫停 30 分鐘",
+ "1hour": "暫停 1 小時",
+ "12hours": "暫停 12 小時",
+ "24hours": "暫停 24 小時",
+ "untilRestart": "暫停直到重啟"
+ },
+ "cancelSuspension": "取消暫停",
+ "toast": {
+ "success": {
+ "registered": "已成功註冊通知。需要重啟 Frigate 才能傳送任何通知(包括測試通知)。",
+ "settingSaved": "通知設定已儲存。"
+ },
+ "error": {
+ "registerFailed": "通知註冊失敗。"
+ }
+ }
+ },
+ "frigatePlus": {
+ "title": "Frigate+ 設定",
+ "description": "Frigate+ 是一項訂閱服務,可為你的 Frigate 例項提供額外的功能和能力,包括使用基於你自己的資料訓練的自訂目標偵測模型。你可以在此管理 Frigate+ 的模型設定。",
+ "cardTitles": {
+ "api": "API",
+ "currentModel": "當前模型",
+ "otherModels": "其他模型",
+ "configuration": "配置"
+ },
+ "apiKey": {
+ "title": "Frigate+ API 金鑰",
+ "validated": "Frigate+ API 金鑰已偵測並驗證透過",
+ "notValidated": "未偵測到 Frigate+ API 金鑰或驗證未透過",
+ "desc": "Frigate+ API 金鑰用於啟用與 Frigate+ 服務的整合。",
+ "plusLink": "瞭解更多關於 Frigate+"
+ },
+ "snapshotConfig": {
+ "title": "快照配置",
+ "desc": "提交到 Frigate+ 需要同時在配置中開啟快照功能。",
+ "cleanCopyWarning": "部分攝影機未開啟快照功能",
+ "table": {
+ "camera": "攝影機",
+ "snapshots": "快照"
+ }
+ },
+ "modelInfo": {
+ "title": "模型資訊",
+ "modelType": "模型型別",
+ "trainDate": "訓練日期",
+ "baseModel": "基礎模型",
+ "plusModelType": {
+ "baseModel": "基礎模型",
+ "userModel": "定向調優"
+ },
+ "supportedDetectors": "支援的偵測器",
+ "cameras": "攝影機",
+ "loading": "正在載入模型資訊…",
+ "error": "載入模型資訊失敗",
+ "noModelLoaded": "目前未載入 Frigate+ 模型。",
+ "availableModels": "可用模型",
+ "loadingAvailableModels": "正在載入可用模型…",
+ "selectModel": "選擇模型",
+ "noModelsAvailable": "無可用模型",
+ "filter": {
+ "ariaLabel": "依類型篩選模型",
+ "baseModels": "基礎模型",
+ "fineTunedModels": "微調模型"
+ },
+ "modelSelect": "您可以在Frigate+上選擇可用的模型。請注意,只能選擇與當前偵測器配置相容的模型。"
+ },
+ "changeInDetectorsAndModel": "變更模型",
+ "unsavedChanges": "未儲存Frigate+變更設定",
+ "restart_required": "需要重啟(Frigate+模型已修改)",
+ "toast": {
+ "success": "Frigate+ 設定已儲存。請重啟 Frigate 以應用更改。",
+ "error": "配置更改儲存失敗:{{errorMessage}}"
+ }
+ },
+ "detectorsAndModel": {
+ "title": "偵測器與模型",
+ "description": "設定執行物件偵測的偵測器後端及其使用的模型。變更會一起儲存以確保偵測器與模型保持同步。",
+ "cardTitles": {
+ "detector": "偵測器硬體",
+ "model": "偵測模型"
+ },
+ "tabs": {
+ "plus": "Frigate+",
+ "custom": "自訂模型"
+ },
+ "mismatch": {
+ "warning": "目前的 Frigate+ 模型「{{model}}」需要 {{required}} 偵測器。請在下方選擇相容的模型,或在儲存前切換到「自訂模型」。"
+ },
+ "plusModel": {
+ "requiresDetector": "需要:{{detector}}",
+ "noModelSelected": "選擇 Frigate+ 模型"
+ },
+ "toast": {
+ "saveSuccess": "偵測器與模型設定已儲存。請重新啟動 Frigate 以套用變更。",
+ "saveError": "儲存偵測器與模型設定失敗"
+ },
+ "unsavedChanges": "偵測器與模型有未儲存的變更",
+ "restartRequired": "需要重新啟動(偵測器或模型已變更)"
+ },
+ "maintenance": {
+ "title": "維護",
+ "sync": {
+ "title": "媒體同步",
+ "desc": "Frigate 會根據您的保留配置定期清理媒體檔案。出現少量孤立檔案是正常現象。使用此功能可以刪除磁碟上不再被資料庫引用的孤立媒體檔案。",
+ "started": "媒體同步已啟動。",
+ "alreadyRunning": "同步任務已在執行中",
+ "error": "啟動同步失敗",
+ "currentStatus": "狀態",
+ "jobId": "任務 ID",
+ "startTime": "開始時間",
+ "endTime": "結束時間",
+ "statusLabel": "狀態",
+ "results": "結果",
+ "errorLabel": "錯誤",
+ "mediaTypes": "媒體型別",
+ "allMedia": "所有媒體",
+ "dryRun": "試執行",
+ "dryRunEnabled": "不會刪除任何檔案",
+ "dryRunDisabled": "將刪除檔案",
+ "force": "強制執行",
+ "forceDesc": "繞過安全閾值,即使刪除超過 50% 的檔案也完成同步。",
+ "verbose": "詳細模式",
+ "verboseDesc": "將所有孤立檔案的完整清單寫入硬碟以供審閱。",
+ "running": "同步執行中…",
+ "start": "開始同步",
+ "inProgress": "同步正在進行中。此頁面已停用。",
+ "status": {
+ "queued": "已排隊",
+ "running": "執行中",
+ "completed": "已完成",
+ "failed": "失敗",
+ "notRunning": "未執行"
+ },
+ "resultsFields": {
+ "filesChecked": "已檢查檔案",
+ "orphansFound": "發現孤立檔案",
+ "orphansDeleted": "已刪除孤立檔案",
+ "aborted": "已中止。刪除操作將超過安全閾值。",
+ "error": "錯誤",
+ "totals": "總計"
+ },
+ "event_snapshots": "追蹤目標快照",
+ "event_thumbnails": "追蹤目標縮圖",
+ "review_thumbnails": "審閱縮圖",
+ "previews": "預覽",
+ "exports": "匯出",
+ "recordings": "錄影"
+ },
+ "regionGrid": {
+ "title": "區域網格",
+ "desc": "區域網格是一種最佳化功能,它會學習不同大小的目標通常出現在每個攝影機視野中的位置。Frigate 利用這些資料來高效地確定偵測區域的大小。該網格會根據追蹤目標資料自動構建。",
+ "clear": "清除區域網格",
+ "clearConfirmTitle": "清除區域網格",
+ "clearConfirmDesc": "除非你最近更改了偵測器模型大小或攝影機的物理位置,並且遇到了目標追蹤問題,否則不建議清除區域網格。網格會隨著目標的追蹤自動重建。更改需要重啟 Frigate 才能生效。",
+ "clearSuccess": "區域網格清除成功",
+ "clearError": "清除區域網格失敗",
+ "restartRequired": "需要重啟以使區域網格更改生效"
+ }
+ },
+ "configForm": {
+ "global": {
+ "title": "全域性設定",
+ "description": "這些設定適用於所有攝影機,除非在攝影機特定設定中被覆蓋。"
+ },
+ "camera": {
+ "title": "攝影機設定",
+ "description": "這些設定僅適用於此攝影機,並會覆蓋全域性設定。",
+ "noCameras": "沒有可用的攝影機"
+ },
+ "advancedSettingsCount": "高階設定 ({{count}})",
+ "advancedCount": "高階選項 ({{count}})",
+ "showAdvanced": "顯示高階設定",
+ "tabs": {
+ "sharedDefaults": "共享預設值",
+ "system": "系統",
+ "integrations": "整合"
+ },
+ "additionalProperties": {
+ "keyLabel": "鍵",
+ "valueLabel": "值",
+ "keyPlaceholder": "新鍵名",
+ "remove": "移除"
+ },
+ "knownPlates": {
+ "namePlaceholder": "例如:老婆的車",
+ "platePlaceholder": "車牌號或正則表示式"
+ },
+ "timezone": {
+ "defaultOption": "使用瀏覽器時區"
+ },
+ "roleMap": {
+ "empty": "未配置權限組對映",
+ "roleLabel": "角色",
+ "groupsLabel": "使用者組",
+ "addMapping": "新增角色對映",
+ "remove": "移除"
+ },
+ "ffmpegArgs": {
+ "preset": "預設",
+ "manual": "手動引數",
+ "inherit": "繼承攝影機設定",
+ "none": "無",
+ "useGlobalSetting": "繼承全域性設定",
+ "selectPreset": "選擇預設",
+ "manualPlaceholder": "輸入 FFmpeg 引數",
+ "presetLabels": {
+ "preset-rpi-64-h264": "樹莓派(H.264)",
+ "preset-rpi-64-h265": "樹莓派(H.265)",
+ "preset-vaapi": "VAAPI (Intel/AMD GPU)",
+ "preset-intel-qsv-h264": "Intel QuickSync (H.264)",
+ "preset-intel-qsv-h265": "Intel QuickSync (H.265)",
+ "preset-nvidia": "NVIDIA GPU",
+ "preset-jetson-h264": "NVIDIA Jetson (H.264)",
+ "preset-jetson-h265": "NVIDIA Jetson (H.265)",
+ "preset-rkmpp": "瑞芯微 RKMPP",
+ "preset-http-jpeg-generic": "HTTP JPEG(通用)",
+ "preset-http-mjpeg-generic": "HTTP MJPEG(通用)",
+ "preset-http-reolink": "HTTP - Reolink 攝影機",
+ "preset-rtmp-generic": "RTMP(通用)",
+ "preset-rtsp-generic": "RTSP(通用)",
+ "preset-rtsp-restream": "RTSP - 從 go2rtc 轉流",
+ "preset-rtsp-restream-low-latency": "RTSP - 從 go2rtc 轉流(低延遲)",
+ "preset-rtsp-udp": "RTSP - UDP協議",
+ "preset-rtsp-blue-iris": "RTSP - Blue Iris",
+ "preset-record-generic": "錄製(通用,無音訊)",
+ "preset-record-generic-audio-copy": "錄製(通用,不轉碼音訊)",
+ "preset-record-generic-audio-aac": "錄製(通用並將音訊轉碼為 AAC)",
+ "preset-record-mjpeg": "錄製 - MJPEG 流攝影機",
+ "preset-record-jpeg": "錄製 - JPEG 流攝影機",
+ "preset-record-ubiquiti": "錄製 - 優必飛攝影機"
+ }
+ },
+ "cameraInputs": {
+ "itemTitle": "影片流 {{index}}",
+ "sourceMode": {
+ "restream": "重新串流 (go2rtc)",
+ "manual": "手動輸入路徑",
+ "go2rtcStreamLabel": "go2rtc 串流",
+ "go2rtcStreamPlaceholder": "選擇一個 go2rtc 串流",
+ "noGo2rtcStreams": "未設定 go2rtc 串流",
+ "go2rtcStreamSearch": "搜尋串流...",
+ "availableStreams": "可用串流",
+ "noMatchingStreams": "沒有符合的串流"
+ }
+ },
+ "restartRequiredField": "需要重啟",
+ "restartRequiredFooter": "配置已更改 - 需要重啟",
+ "sections": {
+ "detect": "偵測",
+ "record": "錄製",
+ "snapshots": "快照",
+ "motion": "畫面變動",
+ "objects": "目標",
+ "review": "審閱",
+ "audio": "音訊",
+ "notifications": "通知",
+ "live": "即時檢視",
+ "timestamp_style": "時間戳",
+ "mqtt": "MQTT",
+ "database": "資料庫",
+ "telemetry": "遙測",
+ "auth": "身份驗證",
+ "tls": "TLS",
+ "proxy": "代理",
+ "go2rtc": "go2rtc",
+ "ffmpeg": "FFmpeg 編解碼",
+ "detectors": "偵測器",
+ "model": "模型",
+ "semantic_search": "語意搜尋",
+ "genai": "生成式 AI",
+ "face_recognition": "人臉辨識",
+ "lpr": "車牌辨識",
+ "birdseye": "鳥瞰圖",
+ "masksAndZones": "遮罩 / 區域"
+ },
+ "detect": {
+ "title": "偵測設定"
+ },
+ "detectors": {
+ "title": "偵測器設定",
+ "singleType": "只允許一個 {{type}} 偵測器。",
+ "keyRequired": "偵測器名稱為必填項。",
+ "keyDuplicate": "偵測器名稱已存在。",
+ "noSchema": "沒有可用的偵測器架構。",
+ "none": "未配置偵測器例項。",
+ "add": "新增偵測器",
+ "addCustomKey": "新增自訂鍵(Key)"
+ },
+ "record": {
+ "title": "錄製設定"
+ },
+ "snapshots": {
+ "title": "快照設定"
+ },
+ "motion": {
+ "title": "畫面變動設定"
+ },
+ "objects": {
+ "title": "目標設定"
+ },
+ "audioLabels": {
+ "summary": "已選擇 {{count}} 個音訊標籤",
+ "empty": "無可用音訊標籤"
+ },
+ "objectLabels": {
+ "summary": "已選擇 {{count}} 個目標型別",
+ "empty": "無可用目標標籤"
+ },
+ "reviewLabels": {
+ "summary": "已選擇 {{count}} 個標籤",
+ "empty": "暫無可用標籤"
+ },
+ "filters": {
+ "objectFieldLabel": "{{label}} 的 {{field}}"
+ },
+ "zoneNames": {
+ "summary": "已選擇 {{count}} 個",
+ "empty": "沒有可用的區域"
+ },
+ "inputRoles": {
+ "summary": "已選擇 {{count}} 個功能",
+ "empty": "無可用功能",
+ "options": {
+ "detect": "偵測",
+ "record": "錄製",
+ "audio": "音訊"
+ }
+ },
+ "genaiRoles": {
+ "options": {
+ "embeddings": "嵌入(Embedding)",
+ "descriptions": "描述",
+ "chat": "對話"
+ }
+ },
+ "semanticSearchModel": {
+ "placeholder": "選擇模型…",
+ "builtIn": "內建模型",
+ "genaiProviders": "生成式 AI 服務"
+ },
+ "review": {
+ "title": "審閱設定"
+ },
+ "audio": {
+ "title": "音訊設定"
+ },
+ "notifications": {
+ "title": "通知設定"
+ },
+ "live": {
+ "title": "即時檢視設定"
+ },
+ "timestamp_style": {
+ "title": "時間戳設定"
+ },
+ "searchPlaceholder": "搜尋…",
+ "addCustomLabel": "新增自訂標籤…",
+ "genaiModel": {
+ "placeholder": "選擇模型…",
+ "search": "搜尋模型…",
+ "noModels": "暫無模型",
+ "available": "可用模型",
+ "useCustom": "使用 \"{{value}}\"",
+ "refresh": "重新整理模型清單",
+ "probeFailed": "無法取得模型清單",
+ "fetchedModels": "已成功取得模型清單"
+ },
+ "liveStreams": {
+ "streamNameLabel": "串流名稱",
+ "streamNamePlaceholder": "例如:主高畫質串流",
+ "go2rtcStreamLabel": "go2rtc 串流",
+ "go2rtcStreamPlaceholder": "選擇一個 go2rtc 串流",
+ "go2rtcStreamSearch": "搜尋或輸入串流名稱…",
+ "noGo2rtcStreams": "未設定 go2rtc 串流",
+ "availableStreams": "可用串流",
+ "useCustom": "使用 \"{{value}}\"",
+ "addStream": "新增串流"
+ },
+ "semanticSearchModelSize": {
+ "notApplicable": "不適用於生成式 AI 服務"
+ },
+ "ptzPresets": {
+ "placeholder": "選擇或輸入預設...",
+ "search": "選擇或輸入預設...",
+ "noPresets": "沒有可用的預設",
+ "available": "攝影機預設",
+ "useCustom": "使用 \"{{value}}\""
+ },
+ "defaultRole": {
+ "admin": "管理員",
+ "viewer": "檢視者"
+ }
+ },
+ "globalConfig": {
+ "title": "全域性配置",
+ "description": "配置適用於所有攝影機的全域性設定,除非被單獨覆蓋。",
+ "toast": {
+ "success": "全域性設定儲存成功",
+ "error": "儲存全域性設定失敗",
+ "validationError": "驗證失敗"
+ }
+ },
+ "cameraConfig": {
+ "title": "攝影機配置",
+ "description": "配置單個攝影機的設定。這些設定會覆蓋全域性預設值。",
+ "overriddenBadge": "已覆蓋",
+ "resetToGlobal": "重設為全域性設定",
+ "toast": {
+ "success": "攝影機設定儲存成功",
+ "error": "儲存攝影機設定失敗"
+ }
+ },
+ "toast": {
+ "success": "設定儲存成功",
+ "applied": "設定應用成功",
+ "successRestartRequired": "設定儲存成功。請重啟 Frigate 以應用更改。",
+ "error": "儲存設定失敗",
+ "validationError": "驗證失敗:{{message}}",
+ "resetSuccess": "已重設為全域性預設值",
+ "resetError": "重設設定失敗",
+ "saveAllSuccess_other": "所有 {{count}} 個部分儲存成功。",
+ "saveAllPartial_other": "已儲存 {{successCount}} / {{totalCount}} 個部分。{{failCount}} 個失敗。",
+ "saveAllFailure": "儲存所有部分失敗。",
+ "saveAllSuccessRestartRequired_other": "已成功儲存 {{count}} 個區塊。請重新啟動 Frigate 以套用變更。"
+ },
+ "profiles": {
+ "title": "設定檔",
+ "activeProfile": "啟用設定檔",
+ "noActiveProfile": "無啟用的設定檔",
+ "active": "啟用",
+ "activated": "設定檔 {{profile}} 已啟用",
+ "activateFailed": "設定檔設定失敗",
+ "deactivated": "設定檔已停用",
+ "noProfiles": "未定義任何設定檔。",
+ "noOverrides": "無覆蓋項",
+ "cameraCount_other": "{{count}} 個攝影機",
+ "columnCamera": "攝影機",
+ "columnOverrides": "設定檔覆蓋",
+ "baseConfig": "基礎配置",
+ "addProfile": "新增設定檔",
+ "newProfile": "新設定檔",
+ "profileNamePlaceholder": "例如:佈防、外出、夜間模式",
+ "friendlyNameLabel": "設定檔名稱",
+ "profileIdLabel": "設定檔 ID",
+ "profileIdDescription": "用於配置和自動化的內部辨識符號",
+ "nameInvalid": "僅允許使用小寫字母、數字和下劃線",
+ "nameDuplicate": "已存在同名設定檔",
+ "error": {
+ "mustBeAtLeastTwoCharacters": "至少需要 2 個字元",
+ "mustNotContainPeriod": "不得包含英文句號(\".\")",
+ "alreadyExists": "已存在使用此 ID 的設定檔"
+ },
+ "renameProfile": "重新命名設定檔",
+ "renameSuccess": "已將設定檔重新命名為 “{{profile}}”",
+ "deleteProfile": "刪除設定檔",
+ "deleteProfileConfirm": "確定要為所有攝影機刪除設定檔“{{profile}}”嗎?該步驟無法撤銷。",
+ "deleteSuccess": "設定檔“{{profile}}”已刪除",
+ "createSuccess": "設定檔“{{profile}}”已建立",
+ "removeOverride": "移除設定檔覆蓋",
+ "deleteSection": "刪除節點覆蓋",
+ "deleteSectionConfirm": "是否要移除攝像機 {{camera}} 上針對設定檔 {{profile}} 的 {{section}} 覆蓋設定?",
+ "deleteSectionSuccess": "已移除 {{profile}} 的 {{section}} 覆蓋設定",
+ "enableSwitch": "開啟設定檔",
+ "enabledDescription": "設定檔功能已啟用。請在下方建立新的設定檔,進入攝影機配置頁面進行修改並儲存,修改即可生效。",
+ "disabledDescription": "設定檔功能可以讓你建立一組帶名稱的攝影機自訂引數(比如佈防、離家、夜間模式),並隨時切換啟用。"
+ },
+ "unsavedChanges": "您有未儲存的更改",
+ "confirmReset": "確認重設",
+ "resetToDefaultDescription": "這將把此部分的所有設定重設為預設值。此操作無法撤銷。",
+ "resetToGlobalDescription": "這將把此部分的設定重設為全域性預設值。此操作無法撤銷。",
+ "go2rtcStreams": {
+ "title": "go2rtc 影片流",
+ "description": "管理用於攝影機轉流的 go2rtc 流配置。每個影片流包含一個名稱以及一個或多個源地址 URL。",
+ "addStream": "新增影片流",
+ "addStreamDesc": "為新的影片流輸入一個名稱,該名稱將用於在攝影機配置中引用該影片流。",
+ "addUrl": "新增 URL 地址",
+ "streamName": "影片流名稱",
+ "streamNamePlaceholder": "例如:front_door,此處只能使用英文",
+ "streamUrlPlaceholder": "例如:rtsp://user:pass@192.168.1.100/stream",
+ "deleteStream": "刪除影片流",
+ "deleteStreamConfirm": "確定要刪除影片流 “{{streamName}}” 嗎?引用該影片流的攝影機可能會停止工作。",
+ "noStreams": "未配置任何 go2rtc 流。請新增一個影片流以開始使用。",
+ "validation": {
+ "nameRequired": "影片流名稱為必填",
+ "nameDuplicate": "已存在同名的影片流",
+ "nameInvalid": "影片流名稱只能使用字母、數字、下劃線和連字元",
+ "urlRequired": "至少需要填寫一個 URL 地址"
+ },
+ "renameStream": "重新命名影片流",
+ "renameStreamDesc": "為此影片流輸入新名稱。重新命名影片流可能會導致透過名稱引用它的攝影機或其他流無法正常工作。",
+ "newStreamName": "新影片流名稱",
+ "ffmpeg": {
+ "useFfmpegModule": "使用相容模式(ffmpeg)",
+ "video": "影片",
+ "audio": "音訊",
+ "hardware": "硬體加速",
+ "videoCopy": "直接複製",
+ "videoH264": "轉碼為 H.264",
+ "videoH265": "轉碼為 H.265",
+ "videoExclude": "排除",
+ "audioCopy": "直接複製",
+ "audioAac": "轉碼為 AAC",
+ "audioOpus": "轉碼為 Opus",
+ "audioPcmu": "轉碼為 PCM μ-law",
+ "audioPcma": "轉碼為 PCM A-law",
+ "audioPcm": "轉碼為 PCM",
+ "audioMp3": "轉碼為 MP3",
+ "audioExclude": "排除",
+ "hardwareNone": "無硬體加速",
+ "hardwareAuto": "自動選擇硬體加速",
+ "hardwareVaapi": "VAAPI",
+ "hardwareCuda": "CUDA",
+ "hardwareV4l2m2m": "V4L2 M2M",
+ "hardwareDxva2": "DXVA2",
+ "hardwareVideotoolbox": "VideoToolbox",
+ "addVideoCodec": "新增視訊編碼器",
+ "addAudioCodec": "新增音訊編碼器",
+ "removeCodec": "移除編碼器"
+ },
+ "sourceNumber": "來源 {{index}}"
+ },
+ "birdseye": {
+ "trackingMode": {
+ "objects": "目標",
+ "motion": "動作",
+ "continuous": "持續"
+ },
+ "cameraOrder": {
+ "label": "攝影機排序",
+ "description": "拖曳攝影機以設定它們在鳥瞰佈局中的順序。",
+ "reorderHandle": "拖曳以重新排序",
+ "saving": "儲存中…",
+ "saved": "已儲存"
+ }
+ },
+ "retainMode": {
+ "all": "全部",
+ "motion": "動作",
+ "active_objects": "活動目標"
+ },
+ "previewQuality": {
+ "very_high": "極高",
+ "high": "高",
+ "medium": "中",
+ "low": "低",
+ "very_low": "極低"
+ },
+ "ui": {
+ "timeFormat": {
+ "browser": "瀏覽器",
+ "12hour": "12 小時",
+ "24hour": "24 小時"
+ },
+ "TimeOrDateStyle": {
+ "full": "完整",
+ "long": "長",
+ "medium": "中",
+ "short": "短"
+ },
+ "unitSystem": {
+ "metric": "公制",
+ "imperial": "英制"
+ }
+ },
+ "review": {
+ "imageSource": {
+ "recordings": "錄影",
+ "previews": "預覽"
+ }
+ },
+ "logger": {
+ "logLevel": {
+ "debug": "Debug",
+ "info": "Info",
+ "warning": "警告",
+ "error": "Error",
+ "critical": "嚴重"
+ }
+ },
+ "onvif": {
+ "profileAuto": "自動",
+ "profileLoading": "正在載入設定檔…",
+ "autotracking": {
+ "zooming": {
+ "disabled": "停用",
+ "absolute": "絕對",
+ "relative": "相對"
+ }
+ }
+ },
+ "modelSize": {
+ "small": "小",
+ "large": "大"
+ },
+ "configMessages": {
+ "review": {
+ "recordDisabled": "錄製已停用,不會生成審閱記錄項。",
+ "detectDisabled": "目標偵測已停用。審閱記錄需要依靠偵測到的目標來對警報和偵測事件進行分類。",
+ "allNonAlertDetections": "所有非警報類活動都將被記錄為偵測事件。",
+ "genaiImageSourceRecordingsRecordDisabled": "影像源雖然設定為“錄製”,但錄製功能已關閉。Frigate 將自動降級使用預覽圖片。"
+ },
+ "audio": {
+ "noAudioRole": "暫無任何流已開啟音訊(audio)功能(role)。必須在影片流上啟用音訊功能,音訊偵測才能正常工作。"
+ },
+ "audioTranscription": {
+ "audioDetectionDisabled": "該攝影機未開啟音訊偵測功能。音訊轉錄需要先開啟音訊偵測。"
+ },
+ "detect": {
+ "fpsGreaterThanFive": "不建議設定偵測幀率高於 5,數值設定過高可能引發效能問題,且不會帶來任何增益。",
+ "disabled": "目標偵測已停用。快照、回放條目以及人臉辨識、車牌辨識、生成式 AI 等增強功能都將無法使用。",
+ "resolutionShouldBeMultipleOfFour": "為了獲得最佳效果,偵測的寬度與高度應為 4 的倍數。其他偶數值可能會在偵測串流中產生畫面殘影或輕微失真。",
+ "aspectRatioMismatch": "您輸入的寬度與高度與目前的偵測解析度長寬比不符。這可能會導致影像被拉伸或失真。",
+ "maxFramesSet": "設定最大影格數將會覆蓋預設行為,並停用靜態物件追蹤。極少有情況需要使用此功能,請謹慎使用。",
+ "squareResolution": "正方形的偵測解析度並不常見。偵測的寬度與高度應符合攝影機的長寬比(例如 16:9),而不是物件偵測模型的尺寸。不相符的長寬比可能會拉伸影像並降低偵測準確率度。",
+ "resolutionHigh": "此偵測解析度高於建議值,可能會增加資源消耗,但不會提高偵測準確度。對於大多數攝影機,建議使用 1080p 或更低的偵測解析度。",
+ "globalResolutionMultipleCameras": "在設定多台攝影機的情況下設定了全域偵測解析度。除非所有攝影機都具有相同的解析度和長寬比,否則應針對每台攝影機單獨定義偵測寬度與高度,以符合各台攝影機的原生長寬比。"
+ },
+ "objects": {
+ "genaiNoDescriptionsProvider": "必須配置具備“描述”功能的生成式 AI 服務商,才能自動生成事件描述。"
+ },
+ "faceRecognition": {
+ "globalDisabled": "必須開啟人臉辨識增強功能,此攝影機的人臉辨識相關功能才能正常使用。",
+ "personNotTracked": "人臉辨識需要偵測到 “人”(person) 後才能工作。請在該攝影機的偵測目標設定中新增“人”。",
+ "modelSizeLarge": "大型模型需要 GPU 或 NPU 才能執行正常。僅使用 CPU 的裝置請選用小型模型。"
+ },
+ "lpr": {
+ "globalDisabled": "要讓該攝影機的車牌辨識功能正常使用,必須先開啟車牌辨識增強功能。",
+ "vehicleNotTracked": "車牌辨識需要先開啟對 “汽車” 或 “摩托車” 的目標追蹤。請在該攝影機的偵測目標中新增“汽車”或“摩托車”。",
+ "modelSizeLarge": "大型模型針對多行格式車牌做了最佳化。小型模型的效能優於大型模型,而且只有小型模型才能支援中文車牌。除非你所在地區使用多行車牌格式,否則建議使用小型模型。"
+ },
+ "record": {
+ "noRecordRole": "暫無任何影片流已配置錄製功能,錄製功能將無法正常工作。"
+ },
+ "birdseye": {
+ "objectsModeDetectDisabled": "鳥瞰圖已設定為 “目標” 模式,但此攝影機未開啟目標偵測。該攝影機將不會顯示在鳥瞰畫面中。"
+ },
+ "snapshots": {
+ "detectDisabled": "目標偵測已停用。快照是根據追蹤到的目標生成的,因此將不會建立快照。"
+ },
+ "detectors": {
+ "mixedTypes": "所有偵測器必須為同一型別。若要更換為其他型別,請先移除現有的偵測器。",
+ "mixedTypesSuggestion": "所有偵測器必須使用相同型別。請移除現有偵測器,或選擇 {{type}}。"
+ },
+ "semanticSearch": {
+ "jinav2SmallModelSize": "Jina V2 的大型模型版本記憶體佔用與推理開銷較高,建議搭配獨立顯示卡使用大型模型。"
+ },
+ "model": {
+ "optimizedFor320": "Frigate 已針對 320x320 模型進行最佳化,這是大多數環境下的最佳選擇。640x640 模型速度較慢,且僅在特定情境下有所幫助。",
+ "inputDimensionsNotDetectResolution": "模型輸入的寬度與高度是物件偵測模型的輸入尺寸,而不是攝影機的偵測解析度。它們應該與您使用的模型尺寸相符 — 通常是像 320x320 或 640x640 這樣的正方形尺寸。"
+ },
+ "ffmpeg": {
+ "hwaccelManualNotRecommended": "不建議手動設定硬體加速參數。除非有特殊需求,否則請選擇符合您硬體的預設組態。"
+ },
+ "onvif": {
+ "autotrackingNoZones": "自動追蹤至少需要一個區域。請先在「遮罩 / 區域」中為此攝影機定義一個區域,然後在下方將其設定為必要區域。"
+ }
+ },
+ "menuDot": {
+ "overrideGlobal": "此區段會覆蓋全域設定",
+ "overrideProfile": "此區段已被 {{profile}} 設定檔覆蓋",
+ "unsaved": "此區段有未儲存的變更"
}
}
diff --git a/web/public/locales/zh-Hant/views/system.json b/web/public/locales/zh-Hant/views/system.json
index e956b9a42e..df8c0a7ca1 100644
--- a/web/public/locales/zh-Hant/views/system.json
+++ b/web/public/locales/zh-Hant/views/system.json
@@ -7,7 +7,8 @@
"logs": {
"frigate": "Frigate 日誌 - Frigate",
"go2rtc": "Go2RTC 日誌 - Frigate",
- "nginx": "Nginx 日誌 - Frigate"
+ "nginx": "Nginx 日誌 - Frigate",
+ "websocket": "訊息日誌 - Frigate"
}
},
"title": "系統",
@@ -33,6 +34,33 @@
"fetchingLogsFailed": "擷取日誌時出錯:{{errorMessage}}",
"whileStreamingLogs": "串流日誌時出錯:{{errorMessage}}"
}
+ },
+ "websocket": {
+ "label": "訊息",
+ "pause": "暫停",
+ "resume": "繼續",
+ "clear": "清除",
+ "filter": {
+ "all": "全部主題",
+ "topics": "主題",
+ "events": "事件",
+ "reviews": "審閱",
+ "classification": "分類",
+ "face_recognition": "人臉辨識",
+ "lpr": "車牌辨識",
+ "camera_activity": "攝影機活動",
+ "system": "系統",
+ "camera": "攝影機",
+ "all_cameras": "所有攝影機",
+ "cameras_count_one": "{{count}} 個攝影機",
+ "cameras_count_other": "{{count}} 個攝影機"
+ },
+ "empty": "未捕獲到訊息",
+ "count_one": "{{count}} 則訊息",
+ "count_other": "{{count}} 則訊息",
+ "expanded": {
+ "payload": "Payload"
+ }
}
},
"general": {
@@ -81,7 +109,10 @@
"title": "Intel GPU 狀態警告",
"message": "GPU 狀態資訊不可用",
"description": "這是一個在Intel GPU 狀態回報工具 (intel_gpu_top) 中已知的 Bug,該工具會故障並重複的回報 GPU占用率為 0%,甚至在硬體加速與物件偵測在 (i)GPU上正確運作時也是如此。這不是 Frigate 的 Bug。您可以透過重新啟動主機來暫時修復此問題以確認 GPU 運作正常。這不會影響效能。"
- }
+ },
+ "gpuCompute": "GPU 計算 / 編碼",
+ "gpuTemperature": "GPU 溫度",
+ "npuTemperature": "NPU 溫度"
},
"otherProcesses": {
"title": "其他行程",
@@ -118,7 +149,11 @@
},
"shm": {
"title": "SHM(共享記憶體)配置",
- "warning": "目前的 SHM 大小為 {{total}}MB,過小。請將其增加至至少 {{min_shm}}MB。"
+ "warning": "目前的 SHM 大小為 {{total}}MB,過小。請將其增加至至少 {{min_shm}}MB。",
+ "frameLifetime": {
+ "title": "幀保留時間",
+ "description": "每個攝影機在共享記憶體中擁有 {{frames}} 個幀槽位。在最快攝影機的幀率下,每一幀在被覆蓋前大約可保留 {{lifetime}} 秒。"
+ }
}
},
"cameras": {
@@ -127,7 +162,7 @@
"info": {
"aspectRatio": "長寬比",
"cameraProbeInfo": "{{camera}} 的詳細資訊",
- "streamDataFromFFPROBE": "串流資料是透過 ffprobe取得。",
+ "streamDataFromFFPROBE": "串流資料是透過 ffprobe 取得。",
"fetching": "正在讀取鏡頭資訊",
"stream": "串流 {{idx}}",
"video": "影片:",
@@ -139,6 +174,21 @@
"error": "錯誤:{{error}}",
"tips": {
"title": "鏡頭詳細資訊"
+ },
+ "keyframes": {
+ "title": "關鍵幀分析",
+ "analyzing": "正在分析關鍵幀... 剩餘 {{seconds}} 秒",
+ "stillAnalyzing": "仍在分析關鍵幀...",
+ "recordStream": "錄影串流:",
+ "keyframeCount": "偵測到的關鍵幀:",
+ "observedDuration": "觀測持續時間:",
+ "gap": "關鍵幀間隔 (最小 / 平均 / 最大):",
+ "segmentLength": "錄影片段長度:",
+ "ok": "每約 {{seconds}} 秒一個關鍵幀,適合錄影與回放。",
+ "warning": "關鍵幀稀疏或不固定(最長間隔約 {{seconds}} 秒),可能使用了智慧編碼(H.264+/H.265+),不建議這樣設定。",
+ "error": "關鍵幀間隔(~{{seconds}} 秒)超過了錄影片段長度({{segmentTime}} 秒)。某些片段可能沒有關鍵幀,這會導致回放中斷。請停用攝影機上的智慧/+編碼,或縮短其關鍵幀間隔。",
+ "unknown": "無法確定關鍵幀間隔。",
+ "recordDisabled": "此攝影機已停用錄影功能。"
}
},
"framesAndDetections": "幀數 / 偵測數",
@@ -156,7 +206,8 @@
"cameraDetect": "{{camName}} 偵測",
"cameraFramesPerSecond": "{{camName}} 幀率",
"cameraDetectionsPerSecond": "{{camName}} 每秒偵測幀率",
- "cameraSkippedDetectionsPerSecond": "{{camName}} 每秒跳過偵測幀率"
+ "cameraSkippedDetectionsPerSecond": "{{camName}} 每秒跳過偵測幀率",
+ "cameraGpu": "{{camName}} GPU"
},
"toast": {
"success": {
@@ -165,6 +216,20 @@
"error": {
"unableToProbeCamera": "無法檢測鏡頭:{{errorMessage}}"
}
+ },
+ "noCameras": {
+ "title": "沒有找到攝影機"
+ },
+ "connectionQuality": {
+ "title": "連線品質",
+ "excellent": "優秀",
+ "fair": "一般",
+ "poor": "較差",
+ "unusable": "不可用",
+ "fps": "幀率",
+ "expectedFps": "預期幀率",
+ "reconnectsLastHour": "最近一小時重連次數",
+ "stallsLastHour": "最近一小時卡頓次數"
}
},
"lastRefreshed": "最後更新: ",
@@ -176,7 +241,8 @@
"cameraIsOffline": "{{camera}} 已離線",
"detectIsSlow": "{{detect}} 偵測速度較慢({{speed}} 毫秒)",
"detectIsVerySlow": "{{detect}} 偵測速度緩慢({{speed}} 毫秒)",
- "shmTooLow": "/dev/shm 配置({{total}} MB)應增加至至少{{min}} MB。"
+ "shmTooLow": "/dev/shm 配置({{total}} MB)應增加至至少{{min}} MB。",
+ "debugReplayActive": "除錯回放工作階段正在進行"
},
"enrichments": {
"title": "進階功能",
diff --git a/web/public/locales/zun/audio.json b/web/public/locales/zun/audio.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/audio.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/common.json b/web/public/locales/zun/common.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/common.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/components/auth.json b/web/public/locales/zun/components/auth.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/components/auth.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/components/camera.json b/web/public/locales/zun/components/camera.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/components/camera.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/components/dialog.json b/web/public/locales/zun/components/dialog.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/components/dialog.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/components/filter.json b/web/public/locales/zun/components/filter.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/components/filter.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/components/icons.json b/web/public/locales/zun/components/icons.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/components/icons.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/components/input.json b/web/public/locales/zun/components/input.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/components/input.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/components/player.json b/web/public/locales/zun/components/player.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/components/player.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/config/cameras.json b/web/public/locales/zun/config/cameras.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/config/cameras.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/config/global.json b/web/public/locales/zun/config/global.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/config/global.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/config/groups.json b/web/public/locales/zun/config/groups.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/config/groups.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/config/validation.json b/web/public/locales/zun/config/validation.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/config/validation.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/objects.json b/web/public/locales/zun/objects.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/objects.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/views/chat.json b/web/public/locales/zun/views/chat.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/views/chat.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/views/classificationModel.json b/web/public/locales/zun/views/classificationModel.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/views/classificationModel.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/views/configEditor.json b/web/public/locales/zun/views/configEditor.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/views/configEditor.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/views/events.json b/web/public/locales/zun/views/events.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/views/events.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/views/explore.json b/web/public/locales/zun/views/explore.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/views/explore.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/views/exports.json b/web/public/locales/zun/views/exports.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/views/exports.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/views/faceLibrary.json b/web/public/locales/zun/views/faceLibrary.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/views/faceLibrary.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/views/live.json b/web/public/locales/zun/views/live.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/views/live.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/views/motionSearch.json b/web/public/locales/zun/views/motionSearch.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/views/motionSearch.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/views/recording.json b/web/public/locales/zun/views/recording.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/views/recording.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/views/replay.json b/web/public/locales/zun/views/replay.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/views/replay.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/views/search.json b/web/public/locales/zun/views/search.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/views/search.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/views/settings.json b/web/public/locales/zun/views/settings.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/views/settings.json
@@ -0,0 +1 @@
+{}
diff --git a/web/public/locales/zun/views/system.json b/web/public/locales/zun/views/system.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/web/public/locales/zun/views/system.json
@@ -0,0 +1 @@
+{}
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 01c415de47..550bc36c81 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -78,7 +78,9 @@ function DefaultAppView() {
className={cn(
"absolute right-0 top-0 overflow-hidden",
isMobile
- ? `bottom-${isPWA ? 16 : 12} left-0 md:bottom-16 landscape:bottom-14 landscape:md:bottom-16`
+ ? isPWA
+ ? "bottom-[calc(3rem+env(safe-area-inset-bottom))] left-0 pt-[env(safe-area-inset-top)] md:bottom-[calc(4rem+env(safe-area-inset-bottom))] landscape:pl-[env(safe-area-inset-left)] landscape:pr-[env(safe-area-inset-right)]"
+ : "bottom-12 left-0 md:bottom-16"
: "bottom-8 left-[52px]",
)}
>
diff --git a/web/src/api/WsProvider.tsx b/web/src/api/WsProvider.tsx
index 4e4f72490b..eb0d76397f 100644
--- a/web/src/api/WsProvider.tsx
+++ b/web/src/api/WsProvider.tsx
@@ -2,7 +2,11 @@ import { baseUrl } from "./baseUrl";
import { ReactNode, useCallback, useEffect, useRef } from "react";
import { WsSendContext } from "./wsContext";
import type { Update } from "./wsContext";
-import { processWsMessage, resetWsStore } from "./ws";
+import {
+ invalidateCameraActivityCache,
+ processWsMessage,
+ resetWsStore,
+} from "./ws";
export function WsProvider({ children }: { children: ReactNode }) {
const wsUrl = `${baseUrl.replace(/^http/, "ws")}ws`;
@@ -10,15 +14,21 @@ export function WsProvider({ children }: { children: ReactNode }) {
const reconnectTimer = useRef | null>(null);
const reconnectAttempt = useRef(0);
const unmounted = useRef(false);
+ const pendingSends = useRef>(new Map());
const sendJsonMessage = useCallback((msg: unknown) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify(msg));
+ } else if (msg && typeof msg === "object" && "topic" in msg) {
+ // Sends issued before the socket reaches OPEN (or during a reconnect
+ // window) are buffered here and flushed in onopen
+ pendingSends.current.set(String((msg as { topic: unknown }).topic), msg);
}
}, []);
useEffect(() => {
unmounted.current = false;
+ const queue = pendingSends.current;
function connect() {
if (unmounted.current) return;
@@ -28,9 +38,16 @@ export function WsProvider({ children }: { children: ReactNode }) {
ws.onopen = () => {
reconnectAttempt.current = 0;
+ // events may have been missed while disconnected — the snapshot
+ // requested below must fully apply even if byte-identical
+ invalidateCameraActivityCache();
ws.send(
JSON.stringify({ topic: "onConnect", message: "", retain: false }),
);
+ for (const queued of queue.values()) {
+ ws.send(JSON.stringify(queued));
+ }
+ queue.clear();
};
ws.onmessage = (event: MessageEvent) => {
@@ -56,7 +73,15 @@ export function WsProvider({ children }: { children: ReactNode }) {
if (reconnectTimer.current) {
clearTimeout(reconnectTimer.current);
}
- wsRef.current?.close();
+ const ws = wsRef.current;
+ if (ws) {
+ ws.onopen = null;
+ ws.onmessage = null;
+ ws.onclose = null;
+ ws.onerror = null;
+ ws.close();
+ }
+ queue.clear();
resetWsStore();
};
}, [wsUrl]);
diff --git a/web/src/api/ws.ts b/web/src/api/ws.ts
index 909a1bb5d9..0a91a88da7 100644
--- a/web/src/api/ws.ts
+++ b/web/src/api/ws.ts
@@ -82,12 +82,15 @@ export function processWsMessage(raw: string) {
function applyTopicUpdate(topic: string, newVal: unknown) {
const oldVal = wsState[topic];
+ // camera_activity snapshots always re-notify: consumers reconcile local
+ // state that may have diverged from an unchanged snapshot
+ const isActivitySnapshot = topic.startsWith("camera_activity/");
// Fast path: === for primitives ("ON"/"OFF", numbers).
// Fall back to isEqual for objects/arrays.
const unchanged =
oldVal === newVal ||
(typeof newVal === "object" && newVal !== null && isEqual(oldVal, newVal));
- if (unchanged) return;
+ if (unchanged && !isActivitySnapshot) return;
wsState[topic] = newVal;
// Snapshot the Set — a listener may trigger unmount that modifies it.
@@ -131,6 +134,25 @@ let wsMessageIdCounter = 0;
// traversals) on every flush — critical with many cameras.
let lastCameraActivityPayload: string | null = null;
+// Make the next camera_activity snapshot fully apply even when byte-identical
+// to the previous one — local state may have diverged while no messages flowed
+export function invalidateCameraActivityCache() {
+ lastCameraActivityPayload = null;
+}
+
+// Collapse same-task resync requests (one hook per camera card) into a
+// single onConnect round-trip
+let resyncScheduled = false;
+function requestCameraActivityResync(sendOnConnect: () => void) {
+ if (resyncScheduled) return;
+ resyncScheduled = true;
+ queueMicrotask(() => {
+ resyncScheduled = false;
+ invalidateCameraActivityCache();
+ sendOnConnect();
+ });
+}
+
function applyCameraActivity(payload: string) {
// Fast path: if the raw JSON string is identical, nothing changed.
if (payload === lastCameraActivityPayload) return;
@@ -509,15 +531,16 @@ export function useInitialCameraState(
// camera_activity sub-topic payload is already parsed by expandCameraActivity
const data = payload as FrigateCameraState | undefined;
- // onConnect is sent once in WsProvider.onopen — no need to re-request on
- // every component mount. Components read cached wsState immediately via
- // useSyncExternalStore. Only re-request when the user tabs back in.
+ // the cached snapshot is only written on onConnect and can be stale by the
+ // time this hook mounts — re-request on mount and when the user tabs back in
useEffect(() => {
if (!revalidateOnFocus) return;
+ requestCameraActivityResync(() => sendCommand("onConnect"));
+
const listener = () => {
if (document.visibilityState === "visible") {
- sendCommand("onConnect");
+ requestCameraActivityResync(() => sendCommand("onConnect"));
}
};
addEventListener("visibilitychange", listener);
diff --git a/web/src/components/auth/AuthForm.tsx b/web/src/components/auth/AuthForm.tsx
index 8798b5d002..e621d120cc 100644
--- a/web/src/components/auth/AuthForm.tsx
+++ b/web/src/components/auth/AuthForm.tsx
@@ -107,7 +107,7 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
{t("form.user")}
{t("form.password")}
diff --git a/web/src/components/auth/ProtectedRoute.tsx b/web/src/components/auth/ProtectedRoute.tsx
index bcfa8fdf36..c10a94a020 100644
--- a/web/src/components/auth/ProtectedRoute.tsx
+++ b/web/src/components/auth/ProtectedRoute.tsx
@@ -6,6 +6,7 @@ import {
isRedirectingToLogin,
setRedirectingToLogin,
} from "@/api/auth-redirect";
+import { baseUrl } from "@/api/baseUrl";
export default function ProtectedRoute({
requiredRoles,
@@ -24,7 +25,7 @@ export default function ProtectedRoute({
!isRedirectingToLogin()
) {
setRedirectingToLogin(true);
- window.location.href = "/login";
+ window.location.href = `${baseUrl}login`;
}
}, [auth.isLoading, auth.isAuthenticated, auth.user]);
diff --git a/web/src/components/button/BlurredIconButton.tsx b/web/src/components/button/BlurredIconButton.tsx
index 8fe17f869b..90ffa36eec 100644
--- a/web/src/components/button/BlurredIconButton.tsx
+++ b/web/src/components/button/BlurredIconButton.tsx
@@ -14,8 +14,8 @@ const BlurredIconButton = forwardRef(
)}
{...rest}
>
-
-
diff --git a/web/src/components/card/AnimatedEventCard.tsx b/web/src/components/card/AnimatedEventCard.tsx
index 74495ddc70..1b7b11f3d7 100644
--- a/web/src/components/card/AnimatedEventCard.tsx
+++ b/web/src/components/card/AnimatedEventCard.tsx
@@ -17,6 +17,9 @@ import { useUserPersistence } from "@/hooks/use-user-persistence";
import { Skeleton } from "../ui/skeleton";
import { Button } from "../ui/button";
import { FaCircleCheck } from "react-icons/fa6";
+import { FaExclamationTriangle } from "react-icons/fa";
+import { MdOutlinePersonSearch } from "react-icons/md";
+import { ThreatLevel } from "@/types/review";
import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next";
import { getTranslatedLabel } from "@/utils/i18n";
@@ -127,6 +130,11 @@ export function AnimatedEventCard({
true,
);
+ const threatLevel = useMemo(
+ () => (event.data.metadata?.potential_threat_level ?? 0) as ThreatLevel,
+ [event],
+ );
+
const aspectRatio = useMemo(() => {
if (
!config ||
@@ -152,7 +160,15 @@ export function AnimatedEventCard({
{
@@ -160,7 +176,13 @@ export function AnimatedEventCard({
updateEvents();
}}
>
-
+ {threatLevel === ThreatLevel.SECURITY_CONCERN ? (
+
+ ) : threatLevel === ThreatLevel.NEEDS_REVIEW ? (
+
+ ) : (
+
+ )}
{t("markAsReviewed")}
diff --git a/web/src/components/card/EmptyCard.tsx b/web/src/components/card/EmptyCard.tsx
index b9943b31a4..5495a6e50c 100644
--- a/web/src/components/card/EmptyCard.tsx
+++ b/web/src/components/card/EmptyCard.tsx
@@ -12,6 +12,7 @@ type EmptyCardProps = {
description?: string;
buttonText?: string;
link?: string;
+ onClick?: () => void;
};
export function EmptyCard({
className,
@@ -21,6 +22,7 @@ export function EmptyCard({
description,
buttonText,
link,
+ onClick,
}: EmptyCardProps) {
let TitleComponent;
@@ -39,11 +41,16 @@ export function EmptyCard({
{description}
)}
- {buttonText?.length && (
-
- {buttonText}
-
- )}
+ {buttonText?.length &&
+ (onClick ? (
+
+ {buttonText}
+
+ ) : (
+
+ {buttonText}
+
+ ))}
);
}
diff --git a/web/src/components/card/ExportCard.tsx b/web/src/components/card/ExportCard.tsx
index 966aab4dcc..958135d29c 100644
--- a/web/src/components/card/ExportCard.tsx
+++ b/web/src/components/card/ExportCard.tsx
@@ -32,6 +32,9 @@ import { FaFolder, FaVideo } from "react-icons/fa";
import { HiSquare2Stack } from "react-icons/hi2";
import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name";
import useContextMenu from "@/hooks/use-contextmenu";
+import axios from "axios";
+import { toast } from "sonner";
+import { useNavigate } from "react-router-dom";
type CaseCardProps = {
className: string;
@@ -123,11 +126,63 @@ export function ExportCard({
onAssignToCase,
onRemoveFromCase,
}: ExportCardProps) {
- const { t } = useTranslation(["views/exports"]);
+ const { t } = useTranslation(["views/exports", "views/replay"]);
+ const navigate = useNavigate();
const isAdmin = useIsAdmin();
const [loading, setLoading] = useState(
exportedRecording.thumb_path.length > 0,
);
+ const [isStartingReplay, setIsStartingReplay] = useState(false);
+
+ const handleDebugReplay = useCallback(() => {
+ setIsStartingReplay(true);
+
+ axios
+ .post("debug_replay/start_from_export", {
+ export_id: exportedRecording.id,
+ })
+ .then((response) => {
+ if (response.status === 202 || response.status === 200) {
+ navigate("/replay");
+ }
+ })
+ .catch((error) => {
+ const errorMessage =
+ error.response?.data?.message ||
+ error.response?.data?.detail ||
+ "Unknown error";
+
+ if (error.response?.status === 409) {
+ toast.error(t("dialog.toast.alreadyActive", { ns: "views/replay" }), {
+ position: "top-center",
+ closeButton: true,
+ dismissible: false,
+ action: (
+
+
+ {t("dialog.toast.goToReplay", { ns: "views/replay" })}
+
+
+ ),
+ });
+ } else {
+ toast.error(
+ t("dialog.toast.error", {
+ ns: "views/replay",
+ error: errorMessage,
+ }),
+ { position: "top-center" },
+ );
+ }
+ })
+ .finally(() => {
+ setIsStartingReplay(false);
+ });
+ }, [exportedRecording.id, navigate, t]);
// Resync the skeleton state whenever the backing export changes. The
// list keys by id now, so in practice the component remounts instead
@@ -202,7 +257,7 @@ export function ExportCard({
{editName && (
<>
submitRename()}
@@ -266,7 +320,7 @@ export function ExportCard({
)}
{!exportedRecording.in_progress && !selectionMode && (
-
+
+ {isAdmin && (
+ {
+ e.stopPropagation();
+ handleDebugReplay();
+ }}
+ >
+ {isStartingReplay
+ ? t("dialog.starting", { ns: "views/replay" })
+ : t("title", { ns: "views/replay" })}
+
+ )}
{isAdmin && onAssignToCase && (
{
- if (response.status == 200) {
+ if (response.status < 300) {
toast.success(t("export.toast.success"), {
position: "top-center",
action: (
-
+
{t("export.toast.view")}
),
@@ -275,7 +284,7 @@ export default function ReviewCard({
-
+
{content}
diff --git a/web/src/components/card/SettingsGroupCard.tsx b/web/src/components/card/SettingsGroupCard.tsx
index 4bfaa14021..819ec212aa 100644
--- a/web/src/components/card/SettingsGroupCard.tsx
+++ b/web/src/components/card/SettingsGroupCard.tsx
@@ -14,7 +14,7 @@ type SettingsGroupCardProps = {
export function SettingsGroupCard({ title, children }: SettingsGroupCardProps) {
return (
-
+
{title}
{children}
diff --git a/web/src/components/chat/ChatAttachmentChip.tsx b/web/src/components/chat/ChatAttachmentChip.tsx
index 5894efaa77..ef4a114555 100644
--- a/web/src/components/chat/ChatAttachmentChip.tsx
+++ b/web/src/components/chat/ChatAttachmentChip.tsx
@@ -1,4 +1,5 @@
import { useApiHost } from "@/api";
+import { baseUrl } from "@/api/baseUrl";
import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name";
import { useTranslation } from "react-i18next";
import useSWR from "swr";
@@ -79,7 +80,7 @@ export function ChatAttachmentChip({
void;
+ sendMessage: (textOverride?: string) => void;
+ placeholder: string;
+
+ supportsThinking: boolean;
+ thinkingEnabled: boolean;
+ setThinkingEnabled: (value: boolean | undefined) => void;
+
+ isLoading?: boolean;
+ onStop?: () => void;
+
+ attachedEventId?: string | null;
+ onClearAttachment?: () => void;
+ onAttach?: (eventId: string) => void;
+ recentEventIds?: string[];
+
+ large?: boolean;
+};
+
+export function ChatComposer({
+ input,
+ setInput,
+ sendMessage,
+ placeholder,
+ supportsThinking,
+ thinkingEnabled,
+ setThinkingEnabled,
+ isLoading = false,
+ onStop,
+ attachedEventId,
+ onClearAttachment,
+ onAttach,
+ recentEventIds,
+ large = false,
+}: ChatComposerProps) {
+ const { t } = useTranslation(["views/chat"]);
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ sendMessage();
+ }
+ };
+
+ const showPaperclip = !!onAttach;
+ const showStop = isLoading && !!onStop;
+
+ return (
+