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/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/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..61b8373a82 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,439 @@ +# 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 + +### 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 +``` + +### 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 +``` + +### 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/docker/main/Dockerfile b/docker/main/Dockerfile index c512ceb848..1a475a650c 100644 --- a/docker/main/Dockerfile +++ b/docker/main/Dockerfile @@ -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 ad0fea91a8..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 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 5796a58aad..2b0fe3c925 100644 --- a/docker/main/rootfs/usr/local/go2rtc/create_config.py +++ b/docker/main/rootfs/usr/local/go2rtc/create_config.py @@ -11,12 +11,10 @@ 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_restricted_go2rtc_source sys.path.remove("/opt/frigate") @@ -81,12 +79,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} diff --git a/docker/main/rootfs/usr/local/nginx/conf/nginx.conf b/docker/main/rootfs/usr/local/nginx/conf/nginx.conf index d954bdcd52..d0b18ff805 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; 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..5c07e98a96 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,6 +206,8 @@ 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 @@ -257,7 +269,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) @@ -469,6 +481,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 +514,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 +588,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). @@ -638,6 +656,11 @@ snapshots: retain: # Required: Default retention days (default: shown below) default: 10 + # Optional: Mode for retention. (default: shown below) + # all - save all snapshots regardless of activity + # motion - save snapshots for any detected motion + # active_objects - save snapshots for active/moving objects + mode: motion # Optional: Per object retention days objects: person: 15 @@ -714,28 +737,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 @@ -840,8 +877,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 +945,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 +985,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 +1126,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 93% rename from docs/docs/configuration/advanced.md rename to docs/docs/configuration/advanced/system.md index e6de72593b..71ef01234c 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"; @@ -172,7 +171,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 +201,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 +233,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 +250,7 @@ Navigate to and expand **IPv6 ```yaml networking: ipv6: - enabled: True + enabled: true ``` @@ -300,6 +289,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. diff --git a/docs/docs/configuration/autotracking.md b/docs/docs/configuration/autotracking.md index 27312eaa92..7d0f8359d4 100644 --- a/docs/docs/configuration/autotracking.md +++ b/docs/docs/configuration/autotracking.md @@ -167,7 +167,7 @@ A fast [detector](object_detectors.md) is recommended. CPU detectors will not pe 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 diff --git a/docs/docs/configuration/cameras.md b/docs/docs/configuration/cameras.md index 8094c9f1c7..e711c8ad56 100644 --- a/docs/docs/configuration/cameras.md +++ b/docs/docs/configuration/cameras.md @@ -67,7 +67,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 +143,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 +179,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. | diff --git a/docs/docs/configuration/index.md b/docs/docs/configuration/config.md similarity index 90% rename from docs/docs/configuration/index.md rename to docs/docs/configuration/config.md index 84f9780784..b2572efda7 100644 --- a/docs/docs/configuration/index.md +++ b/docs/docs/configuration/config.md @@ -1,5 +1,5 @@ --- -id: index +id: config title: Frigate Configuration --- @@ -57,7 +57,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 +92,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 +110,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 +189,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 +266,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..05b110bdaf 100644 --- a/docs/docs/configuration/custom_classification/object_classification.md +++ b/docs/docs/configuration/custom_classification/object_classification.md @@ -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/genai/config.md b/docs/docs/configuration/genai/config.md index a512943c90..9f396d3ccc 100644 --- a/docs/docs/configuration/genai/config.md +++ b/docs/docs/configuration/genai/config.md @@ -49,15 +49,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 diff --git a/docs/docs/configuration/go2rtc.md b/docs/docs/configuration/go2rtc.md new file mode 100644 index 0000000000..08cc944a6f --- /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.13#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 share 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 617d735395..66ee65545f 100644 --- a/docs/docs/configuration/hardware_acceleration_video.md +++ b/docs/docs/configuration/hardware_acceleration_video.md @@ -72,7 +72,7 @@ Frigate can utilize most Intel integrated GPUs and Arc GPUs to accelerate video :::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. @@ -169,7 +169,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 @@ -193,7 +193,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). diff --git a/docs/docs/configuration/live.md b/docs/docs/configuration/live.md index 5749379c63..effe89de3c 100644 --- a/docs/docs/configuration/live.md +++ b/docs/docs/configuration/live.md @@ -11,7 +11,7 @@ Frigate intelligently displays your camera streams on the Live view dashboard. B ### 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 +88,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 +267,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 diff --git a/docs/docs/configuration/motion_detection.md b/docs/docs/configuration/motion_detection.md index 3f31d27dba..7a4a70d027 100644 --- a/docs/docs/configuration/motion_detection.md +++ b/docs/docs/configuration/motion_detection.md @@ -197,3 +197,7 @@ This option is handy when you want to prevent large transient changes from trigg 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 767f70ab9e..e4a5082322 100644 --- a/docs/docs/configuration/object_detectors.md +++ b/docs/docs/configuration/object_detectors.md @@ -72,7 +72,7 @@ This does not affect using hardware for accelerating other tasks such as [semant # Officially Supported Detectors -Frigate provides a number of builtin detector types. By default, Frigate will use a single OpenVINO detector running on the CPU. Other detectors may require additional configuration as described below. When using multiple detectors they will run in dedicated processes, but pull from a common queue of detection requests from across all cameras. +Frigate provides a number of builtin detector types. By default, Frigate will use a single CPU detector. Other detectors may require additional configuration as described below. When using multiple detectors they will run in dedicated processes, but pull from a common queue of detection requests from across all cameras. ## Edge TPU Detector @@ -91,7 +91,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 +111,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 +136,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 +156,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 +176,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 +199,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`). @@ -246,7 +246,7 @@ After placing the downloaded files for the tflite model and labels in your confi -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: +Navigate to 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 | | ---------------------------------------- | ----------------------------------------------------------------- | @@ -309,7 +309,7 @@ Use this configuration for YOLO-based models. When no custom model path or URL i -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: +Navigate to 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 | | ---------------------------------------- | ----------------------- | @@ -365,7 +365,7 @@ For SSD-based models, provide either a model path or URL to your compiled SSD mo -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: +Navigate to 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 | | --------------------------------------- | ------ | @@ -410,7 +410,7 @@ The Hailo detector supports all YOLO models compiled for Hailo hardware that inc -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. +Navigate to 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 to match your custom model dimensions and format. @@ -465,7 +465,7 @@ When using many cameras one detector may not be enough to keep up. Multiple dete -Navigate to and select **OpenVINO** from the detector type dropdown and click **Add** to add multiple detectors, each targeting `GPU` or `NPU`. +Navigate to and select **OpenVINO** from the detector type dropdown and click **Add** to add multiple detectors, each targeting `GPU` or `NPU`. @@ -508,7 +508,7 @@ Use the model configuration shown below when using the OpenVINO detector with th -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: +Navigate to 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 | | ---------------------------------------- | ------------------------------------------ | @@ -558,7 +558,7 @@ After placing the downloaded onnx model in your config folder, use the following -Navigate to and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU`. Then navigate to and configure: +Navigate to 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 | | ---------------------------------------- | ------------------------------------------------- | @@ -620,7 +620,7 @@ After placing the downloaded onnx model in your config folder, use the following -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: +Navigate to 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 | | ---------------------------------------- | -------------------------------------------------------- | @@ -660,7 +660,7 @@ Note that the labelmap uses a subset of the complete COCO label set that has onl #### 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. +[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. :::warning @@ -676,7 +676,7 @@ After placing the downloaded onnx model in your `config/model_cache` folder, use -Navigate to and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU`. Then navigate to and configure: +Navigate to 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 | | --------------------------------------- | --------------------------------- | @@ -728,7 +728,7 @@ After placing the downloaded onnx model in your config/model_cache folder, use t -Navigate to and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `CPU`. Then navigate to and configure: +Navigate to 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 | | ---------------------------------------- | ---------------------------------- | @@ -807,7 +807,7 @@ 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`. +Navigate to and select **ZMQ IPC** from the detector type dropdown and click **Add**, then set the endpoint to `tcp://host.docker.internal:5555`. @@ -841,7 +841,7 @@ When Frigate is started with the following config it will connect to the detecto -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: +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 on the same page, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | -------------------------------------------------------- | @@ -1002,7 +1002,7 @@ When using many cameras one detector may not be enough to keep up. Multiple dete -Navigate to and select **ONNX** from the detector type dropdown and click **Add** to add multiple detectors. +Navigate to and select **ONNX** from the detector type dropdown and click **Add** to add multiple detectors. @@ -1050,7 +1050,7 @@ After placing the downloaded onnx model in your config folder, use the following -Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then navigate to and configure: +Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | ------------------------------------------------- | @@ -1109,7 +1109,7 @@ After placing the downloaded onnx model in your config folder, use the following -Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then navigate to and configure: +Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | -------------------------------------------------------- | @@ -1158,7 +1158,7 @@ After placing the downloaded onnx model in your config folder, use the following -Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then navigate to and configure: +Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | -------------------------------------------------------- | @@ -1207,7 +1207,7 @@ After placing the downloaded onnx model in your `config/model_cache` folder, use -Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then navigate to and configure: +Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: | Field | Value | | --------------------------------------- | --------------------------------- | @@ -1252,7 +1252,7 @@ After placing the downloaded onnx model in your `config/model_cache` folder, use -Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then navigate to and configure: +Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | ------------------------------------------- | @@ -1328,7 +1328,7 @@ A TensorFlow Lite model is provided in the container at `/cpu_model.tflite` and -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). +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). @@ -1364,7 +1364,7 @@ To integrate CodeProject.AI into Frigate, configure the detector as follows: -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`). +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`). @@ -1403,7 +1403,7 @@ To configure the MemryX detector, use the following example configuration: -Navigate to and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. +Navigate to and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. @@ -1423,7 +1423,7 @@ detectors: -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. +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. @@ -1467,7 +1467,7 @@ Below is the recommended configuration for using the **YOLO-NAS** (small) model -Navigate to and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then navigate to and configure: +Navigate to 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 | | ---------------------------------------- | ------------------------------------------------- | @@ -1515,7 +1515,7 @@ Below is the recommended configuration for using the **YOLOv9** (small) model wi -Navigate to and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then navigate to and configure: +Navigate to 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 | | ---------------------------------------- | ------------------------------------------------- | @@ -1562,7 +1562,7 @@ Below is the recommended configuration for using the **YOLOX** (small) model wit -Navigate to and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then navigate to and configure: +Navigate to 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 | | ---------------------------------------- | ----------------------- | @@ -1609,7 +1609,7 @@ Below is the recommended configuration for using the **SSDLite MobileNet v2** mo -Navigate to and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then navigate to and configure: +Navigate to 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 | | ---------------------------------------- | ----------------------- | @@ -1768,7 +1768,7 @@ 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: +Navigate to 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 | | ---------------------------------------- | ------------------------------------------------------------ | @@ -1825,7 +1825,7 @@ Use the model configuration shown below when using the synaptics detector with t -Navigate to and select **Synaptics** from the detector type dropdown and click **Add**. Then navigate to and configure: +Navigate to and select **Synaptics** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | ---------------------------- | @@ -1879,7 +1879,7 @@ When using many cameras one detector may not be enough to keep up. Multiple dete -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. +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. @@ -1921,7 +1921,7 @@ This `config.yml` shows all relevant options to configure the detector and expla -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). +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). @@ -1958,7 +1958,7 @@ The inference time was determined on a rk3588 with 3 NPU cores. -Navigate to and configure: +Navigate to and, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | ----------------------------------------------------------------------- | @@ -2004,7 +2004,7 @@ The pre-trained YOLO-NAS weights from DeciAI are subject to their license and ca -Navigate to and configure: +Navigate to and, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | -------------------------------------------------- | @@ -2044,7 +2044,7 @@ model: # required -Navigate to and configure: +Navigate to and, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | ---------------------------------------------- | @@ -2138,7 +2138,7 @@ 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. +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. @@ -2181,7 +2181,7 @@ It is also possible to eliminate the need for an AI server and run the hardware -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. +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. @@ -2218,7 +2218,7 @@ If you do not possess whatever hardware you want to run, there's also the option -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. +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. @@ -2274,7 +2274,7 @@ Use the model configuration shown below when using the axengine detector with th -Navigate to and select **AXEngine NPU** from the detector type dropdown and click **Add**. Then navigate to and configure: +Navigate to 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 | | ---------------------------------------- | ----------------------- | 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..4d93168f81 100644 --- a/docs/docs/configuration/profiles.md +++ b/docs/docs/configuration/profiles.md @@ -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). +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. +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,27 @@ 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. diff --git a/docs/docs/configuration/record.md b/docs/docs/configuration/record.md index 3d5ef35ba1..4ce55e2ee1 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 diff --git a/docs/docs/configuration/review.md b/docs/docs/configuration/review.md index 4f39611dbe..199212602d 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 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..dc7c3a4d03 100644 --- a/docs/docs/configuration/semantic_search.md +++ b/docs/docs/configuration/semantic_search.md @@ -222,12 +222,7 @@ 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 diff --git a/docs/docs/frigate/installation.md b/docs/docs/frigate/installation.md index 485a735d81..f69218f780 100644 --- a/docs/docs/frigate/installation.md +++ b/docs/docs/frigate/installation.md @@ -600,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 @@ -749,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) @@ -768,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..2d26afbf6f 100644 --- a/docs/docs/frigate/network_requirements.md +++ b/docs/docs/frigate/network_requirements.md @@ -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 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 9619f5a318..aa52a3e3e5 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) @@ -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,7 +301,7 @@ 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 @@ -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..e40ce45196 100644 --- a/docs/docs/guides/reverse_proxy.md +++ b/docs/docs/guides/reverse_proxy.md @@ -12,7 +12,7 @@ Before setting up a reverse proxy, check if any of the built-in functionality in |-|-| |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." diff --git a/docs/docs/integrations/home-assistant.md b/docs/docs/integrations/home-assistant.md index 5b9c014377..14ee795289 100644 --- a/docs/docs/integrations/home-assistant.md +++ b/docs/docs/integrations/home-assistant.md @@ -195,7 +195,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 +204,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 +213,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..44d6ecee0b 100644 --- a/docs/docs/integrations/mqtt.md +++ b/docs/docs/integrations/mqtt.md @@ -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/troubleshooting/cpu.md b/docs/docs/troubleshooting/cpu.md index a9f449ad88..18ae8b7013 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 diff --git a/docs/docs/troubleshooting/dummy-camera.md b/docs/docs/troubleshooting/dummy-camera.md index e24c821290..4443d11e81 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,8 @@ 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. +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,11 +53,25 @@ 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. diff --git a/docs/docs/troubleshooting/faqs.md b/docs/docs/troubleshooting/faqs.md index e11ae63159..8fee36b83b 100644 --- a/docs/docs/troubleshooting/faqs.md +++ b/docs/docs/troubleshooting/faqs.md @@ -55,7 +55,7 @@ If you see repeated "On connect called" messages in your logs, check for another ### 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 diff --git a/docs/docs/troubleshooting/go2rtc.md b/docs/docs/troubleshooting/go2rtc.md new file mode 100644 index 0000000000..4a9a151aad --- /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.13#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 add-on, 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/usage/explore.md b/docs/docs/usage/explore.md new file mode 100644 index 0000000000..a371c79979 --- /dev/null +++ b/docs/docs/usage/explore.md @@ -0,0 +1,95 @@ +--- +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). + +## 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 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..91b901c78c --- /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..457f9f951b --- /dev/null +++ b/docs/docs/usage/history.md @@ -0,0 +1,69 @@ +--- +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. + +## 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, annotated with a motion line, review-item markers, and gaps where 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..2cd3da01ed --- /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. + +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 per camera or per 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 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. + +A **Low-bandwidth mode** notice may also appear in the context menu with a **Reset** option appears 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 — 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..6a5f05b265 --- /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 **recording is 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. + +## 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/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..14fb5ed413 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.13#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/static/frigate-api.yaml b/docs/static/frigate-api.yaml index 9c4e44051a..146fce4e64 100644 --- a/docs/static/frigate-api.yaml +++ b/docs/static/frigate-api.yaml @@ -2058,6 +2058,47 @@ paths: application/json: schema: $ref: "#/components/schemas/HTTPValidationError" + /genai/models: + get: + tags: + - App + summary: List available GenAI models + description: Returns available models for each configured GenAI provider. + operationId: genai_models_genai_models_get + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + /genai/probe: + post: + tags: + - App + 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. Requires admin role. + 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" /vainfo: get: tags: @@ -7031,6 +7072,39 @@ components: "john_doe": ["face1.webp", "face2.jpg"], "jane_smith": ["face3.png"] } + GenAIProbeBody: + properties: + provider: + type: string + enum: + - openai + - azure_openai + - gemini + - ollama + - llamacpp + title: Provider + description: GenAI provider to probe + api_key: + anyOf: + - type: string + - type: "null" + title: API Key + description: API key for the provider (when applicable) + base_url: + anyOf: + - type: string + - type: "null" + title: Base URL + description: Base URL for self-hosted or compatible providers + provider_options: + type: object + title: Provider Options + description: Additional provider-specific options + default: {} + type: object + required: + - provider + title: GenAIProbeBody GenerateObjectExamplesBody: properties: model_name: @@ -7214,13 +7288,6 @@ components: 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 parallel: type: boolean title: Parallel @@ -7306,6 +7373,16 @@ components: anyOf: - $ref: "#/components/schemas/MotionSearchMetricsResponse" - type: "null" + scanning_timestamp: + anyOf: + - type: number + - type: "null" + title: Scanning Timestamp + progress: + anyOf: + - type: number + - type: "null" + title: Progress type: object required: - success diff --git a/frigate/api/app.py b/frigate/api/app.py index 9ff24ed7e8..8ed365fce8 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -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())] @@ -170,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 asyncio.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 @@ -185,26 +289,24 @@ def config(request: Request): if request.headers.get("remote-role") != "admin": config.pop("environment_vars", None) - # remove mqtt credentials - config["mqtt"].pop("password", None) - config["mqtt"].pop("user", None) + # redact mqtt credentials + redact_credential(config["mqtt"], "password") - # remove the proxy secret - config["proxy"].pop("auth_secret", None) + # redact proxy secret + redact_credential(config["proxy"], "auth_secret") - # remove genai api keys - for genai_name, genai_cfg in config.get("genai", {}).items(): + # 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", []): @@ -581,6 +683,10 @@ def _config_set_in_memory(request: Request, body: AppConfigSetBody) -> JSONRespo _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( @@ -644,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, @@ -691,6 +831,13 @@ def config_set(request: Request, body: AppConfigSetBody): 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( @@ -761,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 @@ -774,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/"): diff --git a/frigate/api/camera.py b/frigate/api/camera.py index 9eb4bec9e2..339ee33a16 100644 --- a/frigate/api/camera.py +++ b/frigate/api/camera.py @@ -529,6 +529,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"]))], @@ -605,34 +667,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", @@ -644,10 +682,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 ( @@ -685,10 +722,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 @@ -720,10 +756,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: @@ -876,10 +911,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 []: diff --git a/frigate/api/chat.py b/frigate/api/chat.py index 8b2af8b265..4e6bdbd3b4 100644 --- a/frigate/api/chat.py +++ b/frigate/api/chat.py @@ -35,9 +35,13 @@ 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.ui import UnitSystemEnum +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, @@ -68,390 +72,6 @@ class VLMMonitorRequest(BaseModel): zones: List[str] = [] -def get_tool_definitions( - semantic_search_enabled: bool = False, -) -> 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. - """ - 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 semantic_search_enabled: - search_objects_properties["semantic_query"] = { - "type": "string", - "description": ( - "Optional natural-language description of a PHYSICAL " - "CHARACTERISTIC, APPEARANCE, or ACTIVITY the user mentioned, " - "used to semantically narrow results. Only set this when the " - "user describes something beyond what label and sub_label can " - "express on their own.\n" - "USE for descriptive phrases like: 'riding a lawn mower', " - "'wearing a red jacket', 'carrying a package', 'walking a " - "dog', 'on a bicycle', 'holding an umbrella'.\n" - "DO NOT USE for:\n" - "- specific named people, pets, or delivery companies → use sub_label\n" - "- animal species or breed names like 'blue jay', 'cardinal', " - "'golden retriever' → use sub_label\n" - "- license plate strings → use sub_label\n" - "- generic object queries like 'all cars today' or 'every " - "person' → use label alone with no semantic_query\n" - "When set, combine with label/time/camera/zone filters as " - "usual (e.g. label='person', semantic_query='riding a lawn " - "mower', after='2024-05-01T00:00:00Z')." - ), - } - - search_objects_description = ( - "Search the historical record of detected objects in Frigate. " - "Use this ONLY for questions about the PAST — e.g. 'did anyone come by today?', " - "'when was the last car?', 'show me detections from yesterday'. " - "Do NOT use this for monitoring or alerting requests about future events — " - "use start_camera_watch instead for those. " - "An 'object' in Frigate represents a tracked detection (e.g., a person, package, car).\n\n" - "Choose filters based on what the user is asking for:\n" - "- Generic class query ('show me all cars today'): set `label` only.\n" - "- Specific NAMED entity (known person, delivery company, animal " - "species/breed like 'blue jay' or 'golden retriever', license " - "plate): set `sub_label` only and leave `label` unset.\n" - ) - if semantic_search_enabled: - search_objects_description += ( - "- Physical CHARACTERISTIC, APPEARANCE, or ACTIVITY that is not a " - "discrete name ('person riding a lawn mower', 'someone in a red " - "jacket', 'person carrying a package'): set `semantic_query` with " - "the descriptive phrase, optionally alongside `label` for the " - "object class. Do NOT put descriptive phrases in sub_label." - ) - - return [ - { - "type": "function", - "function": { - "name": "search_objects", - "description": search_objects_description, - "parameters": { - "type": "object", - "properties": search_objects_properties, - }, - "required": [], - }, - }, - { - "type": "function", - "function": { - "name": "find_similar_objects", - "description": ( - "Find tracked objects that are visually and semantically similar " - "to a specific past event. Use this when the user references a " - "particular object they have seen and wants to find other " - "sightings of the same or similar one ('that green car', 'the " - "person in the red jacket', 'the package that was delivered'). " - "Prefer this over search_objects whenever the user's intent is " - "'find more like this specific one.' Use search_objects first " - "only if you need to locate the anchor event. Requires semantic " - "search to be enabled." - ), - "parameters": { - "type": "object", - "properties": { - "event_id": { - "type": "string", - "description": "The id of the anchor event to find similar objects to.", - }, - "after": { - "type": "string", - "description": "Start time in ISO 8601 format (e.g., '2024-01-01T00:00:00Z').", - }, - "before": { - "type": "string", - "description": "End time in ISO 8601 format (e.g., '2024-01-01T23:59:59Z').", - }, - "cameras": { - "type": "array", - "items": {"type": "string"}, - "description": "Optional list of cameras to restrict to. Defaults to all.", - }, - "labels": { - "type": "array", - "items": {"type": "string"}, - "description": "Optional list of labels to restrict to. Defaults to the anchor event's label.", - }, - "sub_labels": { - "type": "array", - "items": {"type": "string"}, - "description": "Optional list of sub_labels (names) to restrict to.", - }, - "zones": { - "type": "array", - "items": {"type": "string"}, - "description": "Optional list of zones. An event matches if any of its zones overlap.", - }, - "similarity_mode": { - "type": "string", - "enum": ["visual", "semantic", "fused"], - "description": "Which similarity signal(s) to use. 'fused' (default) combines visual and semantic.", - "default": "fused", - }, - "min_score": { - "type": "number", - "description": "Drop matches with a similarity score below this threshold (0.0-1.0).", - }, - "limit": { - "type": "integer", - "description": "Maximum number of matches to return (default: 10).", - "default": 10, - }, - }, - "required": ["event_id"], - }, - }, - }, - { - "type": "function", - "function": { - "name": "set_camera_state", - "description": ( - "Change a camera's feature state (e.g., turn detection on/off, enable/disable recordings). " - "Use camera='*' to apply to all cameras at once. " - "Only call this tool when the user explicitly asks to change a camera setting. " - "Requires admin privileges." - ), - "parameters": { - "type": "object", - "properties": { - "camera": { - "type": "string", - "description": "Camera name to target, or '*' to target all cameras.", - }, - "feature": { - "type": "string", - "enum": [ - "detect", - "record", - "snapshots", - "audio", - "motion", - "enabled", - "birdseye", - "birdseye_mode", - "improve_contrast", - "ptz_autotracker", - "motion_contour_area", - "motion_threshold", - "notifications", - "audio_transcription", - "review_alerts", - "review_detections", - "object_descriptions", - "review_descriptions", - "profile", - ], - "description": ( - "The feature to change. Most features accept ON or OFF. " - "birdseye_mode accepts CONTINUOUS, MOTION, or OBJECTS. " - "motion_contour_area and motion_threshold accept a number. " - "profile accepts a profile name or 'none' to deactivate (requires camera='*')." - ), - }, - "value": { - "type": "string", - "description": "The value to set. ON or OFF for toggles, a number for thresholds, a profile name or 'none' for profile.", - }, - }, - "required": ["camera", "feature", "value"], - }, - }, - }, - { - "type": "function", - "function": { - "name": "get_live_context", - "description": ( - "Get the current live image and detection information for a 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"], - }, - }, - }, - ] - - @router.get( "/chat/tools", dependencies=[Depends(allow_any_authenticated())], @@ -460,10 +80,13 @@ def get_tool_definitions( ) def get_tools(request: Request) -> JSONResponse: """Get list of available tools for LLM function calling.""" - semantic_search_enabled = bool( - getattr(request.app.frigate_config.semantic_search, "enabled", False) + 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, ) - tools = get_tool_definitions(semantic_search_enabled=semantic_search_enabled) return JSONResponse(content={"tools": tools}) @@ -554,11 +177,14 @@ async def _execute_search_objects( 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, @@ -626,6 +252,7 @@ async def _execute_search_objects_semantic( label = arguments.get("label") sub_label = arguments.get("sub_label") + attribute = arguments.get("attribute") zones = arguments.get("zones") if isinstance(zones, list) and zones: @@ -668,6 +295,10 @@ async def _execute_search_objects_semantic( 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)) @@ -916,9 +547,21 @@ async def _execute_get_live_context( camera: str, allowed_cameras: List[str], ) -> Dict[str, Any]: + # Reject wildcards explicitly so models retry with a real camera name + # instead of silently fanning out across every camera. + if camera in ("*", "all"): + return { + "error": ( + "get_live_context requires a single camera name; wildcards " + "are not supported. Call this tool once per camera." + ), + "available_cameras": allowed_cameras, + } + if camera not in allowed_cameras: return { "error": f"Camera '{camera}' not found or access denied", + "available_cameras": allowed_cameras, } if camera not in request.app.frigate_config.cameras: @@ -1090,7 +733,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) @@ -1481,72 +1131,19 @@ async def chat_completion( config = request.app.frigate_config semantic_search_enabled = bool(getattr(config.semantic_search, "enabled", False)) - tools = get_tool_definitions(semantic_search_enabled=semantic_search_enabled) + attribute_classifications = get_attribute_classifications(config) + tools = get_tool_definitions( + semantic_search_enabled=semantic_search_enabled, + attribute_classifications=attribute_classifications, + ) 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 = [] - has_speed_zone = False - for camera_id in allowed_cameras: - if camera_id not in config.cameras: - continue - camera_config = config.cameras[camera_id] - friendly_name = ( - camera_config.friendly_name - if camera_config.friendly_name - else camera_id.replace("_", " ").title() - ) - zone_names = list(camera_config.zones.keys()) - if not has_speed_zone: - has_speed_zone = any( - zone.distances for zone in camera_config.zones.values() - ) - if zone_names: - cameras_info.append( - f" - {friendly_name} (ID: {camera_id}, zones: {', '.join(zone_names)})" - ) - else: - cameras_info.append(f" - {friendly_name} (ID: {camera_id})") - - cameras_section = "" - if cameras_info: - cameras_section = ( - "\n\nAvailable cameras:\n" - + "\n".join(cameras_info) - + "\n\nWhen users refer to cameras by their friendly name (e.g., 'Back Deck Camera'), use the corresponding camera ID (e.g., 'back_deck_cam') in tool calls." - ) - - speed_units_section = "" - if has_speed_zone: - speed_unit = ( - "mph" if config.ui.unit_system == UnitSystemEnum.imperial else "km/h" - ) - speed_units_section = f"\n\nReport object speeds to the user in {speed_unit}." - - semantic_search_section = "" - if semantic_search_enabled: - semantic_search_section = ( - "\n\nWhen routing a search_objects call, pick filters by the shape of the user's request:\n" - "- Generic class ('show me all cars today'): set `label` only.\n" - "- Specific named entity — a known person ('John'), delivery company ('Amazon'), animal species/breed ('blue jay', 'cardinal', 'golden retriever'), or license plate: set `sub_label` only and leave `label` unset.\n" - "- Physical characteristic, appearance, or activity that is NOT a discrete name ('find me people riding a lawn mower', 'someone in a red jacket', 'a person carrying a package'): set `semantic_query` with the descriptive phrase, optionally combined with `label` for the object class. Never put descriptive phrases in `sub_label`." - ) - - 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.{semantic_search_section}{cameras_section}{speed_units_section}""" + system_prompt = build_chat_system_prompt( + config=config, + allowed_cameras=allowed_cameras, + semantic_search_enabled=semantic_search_enabled, + attribute_classifications=attribute_classifications, + ) conversation.append( { @@ -1595,6 +1192,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") @@ -1607,6 +1205,13 @@ 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") @@ -1682,6 +1287,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": @@ -1707,6 +1313,7 @@ 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") async def stream_body() -> Any: if tool_calls: @@ -1721,6 +1328,15 @@ When a user refers to a specific object they have seen or describe with identify ).encode("utf-8") + b"\n" ) + # Emit the full reasoning trace up front when the + # underlying client did not stream it + if final_reasoning: + yield ( + json.dumps( + {"type": "reasoning", "delta": final_reasoning} + ).encode("utf-8") + + b"\n" + ) # Stream content in word-sized chunks for smooth UX for part in chunk_content(final_content): yield ( @@ -1741,6 +1357,7 @@ 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"), 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 171bf1b98a..034da3845d 100644 --- a/frigate/api/debug_replay.py +++ b/frigate/api/debug_replay.py @@ -6,11 +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 start_debug_replay_job +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__) @@ -25,6 +32,12 @@ 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.""" @@ -73,13 +86,100 @@ class DebugReplayStopResponse(BaseModel): async def start_debug_replay(request: Request, body: DebugReplayStartBody): """Start a debug replay session asynchronously.""" replay_manager = request.app.replay_manager + internal_port = request.app.frigate_config.networking.listen.internal + if type(internal_port) is str: + internal_port = int(internal_port.split(":")[-1]) + + source = RecordingDebugReplaySource( + source_camera=body.camera, + start_ts=body.start_time, + end_ts=body.end_time, + internal_port=internal_port, + ) try: job_id = await asyncio.to_thread( start_debug_replay_job, - source_camera=body.camera, - start_ts=body.start_time, - end_ts=body.end_time, + source=source, + frigate_config=request.app.frigate_config, + config_publisher=request.app.config_publisher, + replay_manager=replay_manager, + ) + except RuntimeError: + return JSONResponse( + content={ + "success": False, + "message": "A replay session is already active", + }, + status_code=409, + ) + except ValueError: + logger.exception("Rejected debug replay start request") + return JSONResponse( + content={ + "success": False, + "message": "Invalid debug replay parameters", + }, + status_code=400, + ) + + return JSONResponse( + content={ + "success": True, + "replay_camera": replay_manager.replay_camera_name, + "job_id": job_id, + }, + status_code=202, + ) + + +@router.post( + "/debug_replay/start_from_export", + response_model=DebugReplayStartResponse, + status_code=202, + responses={ + 400: {"description": "Invalid export, time range, or no recordings"}, + 404: {"description": "Export not found"}, + 409: {"description": "A replay session is already active"}, + }, + dependencies=[Depends(require_role(["admin"]))], + summary="Start debug replay from an export", + description="Start a debug replay session covering an existing export's " + "time range. The end time is derived from the export's video duration.", +) +async def start_debug_replay_from_export( + request: Request, body: DebugReplayStartFromExportBody +): + """Start a debug replay session from an existing export.""" + try: + export: Export = Export.get(Export.id == body.export_id) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Export not found"}, + status_code=404, + ) + + properties = await get_video_properties( + request.app.frigate_config.ffmpeg, export.video_path, get_duration=True + ) + duration = properties.get("duration", -1) + + if duration is None or duration <= 0: + return JSONResponse( + content={ + "success": False, + "message": "Could not determine export duration", + }, + status_code=400, + ) + + replay_manager = request.app.replay_manager + source = ExportDebugReplaySource(export=export, duration=float(duration)) + + try: + job_id = await asyncio.to_thread( + start_debug_replay_job, + source=source, frigate_config=request.app.frigate_config, config_publisher=request.app.config_publisher, replay_manager=replay_manager, diff --git a/frigate/api/defs/request/app_body.py b/frigate/api/defs/request/app_body.py index d9d11fd019..2c37f6ae4d 100644 --- a/frigate/api/defs/request/app_body.py +++ b/frigate/api/defs/request/app_body.py @@ -2,6 +2,8 @@ from typing import Any, Dict, List, Optional from pydantic import BaseModel, Field +from frigate.config import GenAIProviderEnum + class AppConfigSetBody(BaseModel): requires_restart: int = 1 @@ -10,6 +12,13 @@ class AppConfigSetBody(BaseModel): skip_save: bool = False +class GenAIProbeBody(BaseModel): + provider: GenAIProviderEnum + api_key: Optional[str] = None + base_url: Optional[str] = None + provider_options: Dict[str, Any] = Field(default_factory=dict) + + class AppPutPasswordBody(BaseModel): password: str old_password: Optional[str] = None diff --git a/frigate/api/defs/request/chat_body.py b/frigate/api/defs/request/chat_body.py index 79ca3a6fef..228781c80b 100644 --- a/frigate/api/defs/request/chat_body.py +++ b/frigate/api/defs/request/chat_body.py @@ -36,3 +36,10 @@ class ChatCompletionRequest(BaseModel): default=False, description="If true, stream the final assistant response in the body as newline-delimited JSON.", ) + enable_thinking: Optional[bool] = 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/response/chat_response.py b/frigate/api/defs/response/chat_response.py index 0bc864ba68..c2b3e6b1f2 100644 --- a/frigate/api/defs/response/chat_response.py +++ b/frigate/api/defs/response/chat_response.py @@ -20,6 +20,10 @@ class ChatMessageResponse(BaseModel): content: Optional[str] = Field( default=None, description="Message content (None if tool calls present)" ) + reasoning: Optional[str] = Field( + default=None, + description="Separated reasoning/thinking trace if the model emitted one", + ) tool_calls: Optional[list[ToolCallInvocation]] = Field( default=None, description="Tool calls if LLM wants to call tools" ) diff --git a/frigate/api/export.py b/frigate/api/export.py index 2f4ca78da0..09ded84124 100644 --- a/frigate/api/export.py +++ b/frigate/api/export.py @@ -398,7 +398,7 @@ class _StreamingZipBuffer: 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(datetime.datetime.timestamp(export.date))}" + base = f"{export.camera}_{int(export.date)}" candidate = f"{base}.mp4" counter = 1 diff --git a/frigate/api/fastapi_app.py b/frigate/api/fastapi_app.py index f201ab7135..3f8d8a7a5f 100644 --- a/frigate/api/fastapi_app.py +++ b/frigate/api/fastapi_app.py @@ -1,3 +1,4 @@ +import asyncio import logging import re from typing import Optional @@ -36,7 +37,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 @@ -116,6 +117,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/motion_search.py b/frigate/api/motion_search.py index 09bf8026da..378e3469b7 100644 --- a/frigate/api/motion_search.py +++ b/frigate/api/motion_search.py @@ -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", @@ -97,6 +91,8 @@ class MotionSearchStatusResponse(BaseModel): total_frames_processed: Optional[int] = None error_message: Optional[str] = None metrics: Optional[MotionSearchMetricsResponse] = None + scanning_timestamp: Optional[float] = None + progress: Optional[float] = 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/record.py b/frigate/api/record.py index f6366813b6..04b81cf38a 100644 --- a/frigate/api/record.py +++ b/frigate/api/record.py @@ -299,22 +299,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 diff --git a/frigate/api/review.py b/frigate/api/review.py index cb114db2a0..ccf0be9d12 100644 --- a/frigate/api/review.py +++ b/frigate/api/review.py @@ -605,9 +605,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 +636,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 +657,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 8b5766148b..785dc24470 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -343,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(): @@ -612,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/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..f35a3eaa56 100644 --- a/frigate/camera/state.py +++ b/frigate/camera/state.py @@ -45,6 +45,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 +304,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 +348,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 +372,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/dispatcher.py b/frigate/comms/dispatcher.py index 27d5ef1255..a85e644940 100644 --- a/frigate/comms/dispatcher.py +++ b/frigate/comms/dispatcher.py @@ -3,11 +3,13 @@ import datetime import json import logging +from collections.abc import Iterable from typing import Any, Callable, Optional, 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, @@ -397,6 +400,60 @@ class Dispatcher: 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 +485,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 +510,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 +673,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 +730,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 +750,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/runtime_state.py b/frigate/comms/runtime_state.py new file mode 100644 index 0000000000..5066ed3993 --- /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, "r") 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/ws.py b/frigate/comms/ws.py index 2f16ab7141..5b555999e3 100644 --- a/frigate/comms/ws.py +++ b/frigate/comms/ws.py @@ -34,6 +34,8 @@ from frigate.const import ( 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__) @@ -66,6 +68,7 @@ _WS_VIEWER_TOPICS = frozenset( "audioTranscriptionState", "birdseyeLayout", "embeddingsReindexProgress", + "jobState", } ) @@ -102,6 +105,321 @@ def _check_ws_authorization( return topic in _WS_VIEWER_TOPICS +# ---- 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: """ @@ -183,6 +501,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( { @@ -195,14 +517,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/camera/camera.py b/frigate/config/camera/camera.py index fe2bee647f..01092d4f18 100644 --- a/frigate/config/camera/camera.py +++ b/frigate/config/camera/camera.py @@ -146,7 +146,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 diff --git a/frigate/config/camera/ffmpeg.py b/frigate/config/camera/ffmpeg.py index 05769dc66d..6341cbcd13 100644 --- a/frigate/config/camera/ffmpeg.py +++ b/frigate/config/camera/ffmpeg.py @@ -3,7 +3,7 @@ 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 @@ -49,7 +49,7 @@ 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 ("5.0" or "8.0").', ) global_args: Union[str, list[str]] = Field( default=FFMPEG_GLOBAL_ARGS_DEFAULT, @@ -90,21 +90,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): diff --git a/frigate/config/camera/genai.py b/frigate/config/camera/genai.py index 902c94c42b..5b94755723 100644 --- a/frigate/config/camera/genai.py +++ b/frigate/config/camera/genai.py @@ -37,7 +37,7 @@ class GenAIConfig(FrigateBaseModel): 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.", ) 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 95092da08b..b475f42157 100644 --- a/frigate/config/camera/updater.py +++ b/frigate/config/camera/updater.py @@ -26,6 +26,7 @@ class CameraConfigUpdateEnum(str, Enum): 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" @@ -84,8 +85,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) diff --git a/frigate/config/config.py b/frigate/config/config.py index 6873e6b880..1a12c20e51 100644 --- a/frigate/config/config.py +++ b/frigate/config/config.py @@ -80,12 +80,12 @@ logger = logging.getLogger(__name__) yaml = YAML() -DEFAULT_DETECTORS = { - "ov": { - "type": "openvino", - "device": "CPU", - } -} +# 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, @@ -94,6 +94,7 @@ DEFAULT_MODEL = { "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} @@ -109,7 +110,7 @@ DEFAULT_CONFIG = f""" mqtt: enabled: False -{_render_default_yaml({"detectors": DEFAULT_DETECTORS, "model": DEFAULT_MODEL})} +{_render_default_yaml({"detectors": NEW_CONFIG_DETECTORS, "model": DEFAULT_MODEL})} cameras: {{}} # No cameras defined, UI wizard should be used version: {CURRENT_CONFIG_VERSION} """ @@ -325,6 +326,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 ( @@ -628,15 +670,23 @@ 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() + # 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: @@ -950,6 +1000,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) diff --git a/frigate/config/profile_manager.py b/frigate/config/profile_manager.py index d109bdecbc..e0a40ee353 100644 --- a/frigate/config/profile_manager.py +++ b/frigate/config/profile_manager.py @@ -5,7 +5,7 @@ import json import logging from datetime import datetime, timezone from pathlib import Path -from typing import Optional +from typing import Any, Callable, Optional from frigate.config.camera.updater import ( CameraConfigUpdateEnum, @@ -34,6 +34,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 +163,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: Optional[str], + clear_runtime_overrides: bool = True, + ) -> Optional[str]: """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 +208,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", @@ -292,6 +349,15 @@ class ProfileManager: settings, ) + # republish MQTT switch states + if self.dispatcher is not None: + for suffix, get_payload in SECTION_STATE_TOPICS.get(section, ()): + self.dispatcher.publish( + f"{cam_name}/{suffix}/state", + get_payload(cam_config), + retain=True, + ) + def _persist_active_profile(self, profile_name: Optional[str]) -> None: """Persist the active profile state to disk as JSON.""" try: diff --git a/frigate/config/proxy.py b/frigate/config/proxy.py index 2426fcf104..8c20c6e6dd 100644 --- a/frigate/config/proxy.py +++ b/frigate/config/proxy.py @@ -45,7 +45,7 @@ class ProxyConfig(FrigateBaseModel): default_role: Optional[str] = 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( default=",", diff --git a/frigate/config/ui.py b/frigate/config/ui.py index 2c3104bbc9..057a2b3336 100644 --- a/frigate/config/ui.py +++ b/frigate/config/ui.py @@ -5,7 +5,7 @@ from pydantic import Field from .base import FrigateBaseModel -__all__ = ["TimeFormatEnum", "DateTimeStyleEnum", "UnitSystemEnum", "UIConfig"] +__all__ = ["TimeFormatEnum", "UnitSystemEnum", "UIConfig"] class TimeFormatEnum(str, Enum): @@ -14,13 +14,6 @@ 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" @@ -37,16 +30,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 07537ea5f7..dac04c4f1a 100644 --- a/frigate/const.py +++ b/frigate/const.py @@ -21,6 +21,8 @@ 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/debug_replay.py b/frigate/debug_replay.py index ac04090e47..956bc20012 100644 --- a/frigate/debug_replay.py +++ b/frigate/debug_replay.py @@ -5,10 +5,12 @@ frigate.jobs.debug_replay. This module owns only session presence (active), session metadata, and post-session cleanup. """ +import asyncio import logging import os import shutil import threading +import time from ruamel.yaml import YAML @@ -25,12 +27,23 @@ from frigate.const import ( REPLAY_DIR, THUMB_DIR, ) -from frigate.jobs.debug_replay import cancel_debug_replay_job, wait_for_runner +from frigate.jobs.debug_replay import ( + JOB_TYPE as DEBUG_REPLAY_JOB_TYPE, +) +from frigate.jobs.debug_replay import ( + cancel_debug_replay_job, + wait_for_runner, +) +from frigate.jobs.export import JobStatePublisher +from frigate.types import JobStatusTypesEnum from frigate.util.camera_cleanup import cleanup_camera_db, cleanup_camera_files from frigate.util.config import find_config_file logger = logging.getLogger(__name__) +MAX_SESSION_DURATION_SECONDS = 12 * 60 * 60 +AUTO_STOP_CHECK_INTERVAL_SECONDS = 60 + class DebugReplayManager: """Owns the lifecycle pointers for a single debug replay session. @@ -49,6 +62,8 @@ 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: @@ -73,6 +88,7 @@ class DebugReplayManager: self.start_ts = start_ts self.end_ts = end_ts self.clip_path = None + self.session_started_at = time.time() def mark_session_ready(self, clip_path: str) -> None: """Record the on-disk clip path after the camera has been published.""" @@ -94,6 +110,7 @@ class DebugReplayManager: self.clip_path = None self.start_ts = None self.end_ts = None + self.session_started_at = None def publish_camera( self, @@ -150,6 +167,7 @@ class DebugReplayManager: return replay_name = self.replay_camera_name + source_camera = self.source_camera # Only publish remove if the camera was actually added to the live # config (i.e. the runner reached the starting_camera phase). @@ -158,11 +176,27 @@ class DebugReplayManager: CameraConfigUpdateTopic(CameraConfigUpdateEnum.remove, replay_name), frigate_config.cameras[replay_name], ) + frigate_config.cameras.pop(replay_name, None) if replay_name is not None: self._cleanup_db(replay_name) self._cleanup_files(replay_name) + self._job_state_publisher.publish( + { + "id": "stopped", + "job_type": DEBUG_REPLAY_JOB_TYPE, + "status": JobStatusTypesEnum.cancelled, + "start_time": None, + "end_time": time.time(), + "error_message": None, + "results": { + "source_camera": source_camera, + "replay_camera_name": replay_name, + }, + } + ) + self._clear_locked() logger.info("Debug replay stopped and cleaned up: %s", replay_name) @@ -211,6 +245,10 @@ class DebugReplayManager: 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: @@ -219,11 +257,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": { @@ -248,8 +298,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: @@ -308,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_runners.py b/frigate/detectors/detection_runners.py index 39ebbf5783..ee465b3d51 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.""" @@ -282,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 @@ -310,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() @@ -369,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]: diff --git a/frigate/embeddings/maintainer.py b/frigate/embeddings/maintainer.py index 96a44a8c6c..52bdf5d915 100644 --- a/frigate/embeddings/maintainer.py +++ b/frigate/embeddings/maintainer.py @@ -98,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/") @@ -232,7 +239,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() ): diff --git a/frigate/events/audio.py b/frigate/events/audio.py index c5e35a8d52..d44521787c 100644 --- a/frigate/events/audio.py +++ b/frigate/events/audio.py @@ -94,13 +94,28 @@ class AudioProcessor(FrigateProcess): self.camera_metrics = camera_metrics self.config = config + def __stop_audio_thread(self, camera: str) -> None: + thread = self.audio_threads.pop(camera, None) + if thread is None: + return + + thread.stop() + thread.join(10) + if thread.is_alive(): + self.logger.warning(f"Audio maintainer thread for {camera} is still alive") + else: + self.logger.info(f"Audio maintainer stopped for {camera}") + def run(self) -> None: self.pre_run_setup(self.config.logger) - audio_threads: dict[str, 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", @@ -117,12 +132,13 @@ class AudioProcessor(FrigateProcess): CameraConfigUpdateEnum.add, CameraConfigUpdateEnum.audio, CameraConfigUpdateEnum.ffmpeg, + CameraConfigUpdateEnum.remove, ], ) def spawn_if_needed(camera: CameraConfig) -> None: name = camera.name - if name is None or name in audio_threads: + if name is None or name in self.audio_threads: return if not camera.enabled or not camera.audio.enabled: return @@ -136,7 +152,7 @@ class AudioProcessor(FrigateProcess): self.transcription_model_runner, self.stop_event, # type: ignore[arg-type] ) - audio_threads[name] = thread + self.audio_threads[name] = thread thread.start() self.logger.info(f"Audio maintainer started for {name}") @@ -145,21 +161,31 @@ class AudioProcessor(FrigateProcess): self.logger.info(f"Audio processor started (pid: {self.pid})") - # poll for newly added cameras or cameras flipped to audio.enabled at runtime + # poll for newly added/removed cameras or cameras flipped to + # audio.enabled at runtime while not self.stop_event.wait(timeout=1.0): - config_subscriber.check_for_updates() + updated_topics = config_subscriber.check_for_updates() + + # stop maintainers for removed cameras so their ffmpeg process is + # torn down and they stop touching camera_metrics (which the camera + # maintainer has already popped for the removed camera) + for removed_camera in updated_topics.get( + CameraConfigUpdateEnum.remove.name, [] + ): + self.__stop_audio_thread(removed_camera) + for camera in self.config.cameras.values(): spawn_if_needed(camera) config_subscriber.stop() - for thread in audio_threads.values(): + 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.values(): + for thread in self.audio_threads.values(): if thread.is_alive(): self.logger.warning(f"Thread {thread.name} is still alive") @@ -181,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)) @@ -206,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 @@ -230,7 +259,11 @@ class AudioEventMaintainer(threading.Thread): 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) @@ -349,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() diff --git a/frigate/ffmpeg_presets.py b/frigate/ffmpeg_presets.py index c314b30eaf..4ebbd6c801 100644 --- a/frigate/ffmpeg_presets.py +++ b/frigate/ffmpeg_presets.py @@ -465,16 +465,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 +476,8 @@ PRESETS_RECORD_OUTPUT = { "1", "-strftime", "1", - "-c:v", + "-c", "copy", - "-c:a", - "aac", ], "preset-record-mjpeg": [ "-f", diff --git a/frigate/genai/__init__.py b/frigate/genai/__init__.py index ce0034670d..bca5e6d691 100644 --- a/frigate/genai/__init__.py +++ b/frigate/genai/__init__.py @@ -1,21 +1,25 @@ """Generative AI module for Frigate.""" -import datetime import importlib import json import logging import os import re -from typing import Any, Callable, Optional +from typing import Any, AsyncGenerator, Callable, Optional 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__) @@ -46,9 +50,15 @@ def register_genai_provider(key: GenAIProviderEnum) -> Callable: class GenAIClient: """Generative AI client for Frigate.""" - def __init__(self, genai_config: GenAIConfig, timeout: int = 120) -> None: + 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() def generate_review_description( @@ -61,75 +71,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: -- `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. - -## 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']}" ) @@ -143,25 +92,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") - - 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) @@ -240,61 +171,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( @@ -326,10 +205,7 @@ Guidelines: ) -> Optional[str]: """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 @@ -346,8 +222,15 @@ Guidelines: prompt: str, images: list[bytes], response_format: Optional[dict] = None, + enable_thinking: bool = False, ) -> Optional[str]: - """Submit a request to the provider.""" + """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 @@ -359,6 +242,11 @@ Guidelines: """ return True + @property + def supports_toggleable_thinking(self) -> bool: + """Whether the configured model exposes a per-request thinking toggle.""" + return False + def list_models(self) -> list[str]: """Return the list of model names available from this provider. @@ -402,6 +290,7 @@ Guidelines: messages: list[dict[str, Any]], tools: Optional[list[dict[str, Any]]] = None, tool_choice: Optional[str] = "auto", + enable_thinking: Optional[bool] = None, ) -> dict[str, Any]: """ Send chat messages to LLM with optional tool definitions. @@ -425,11 +314,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 @@ -441,6 +336,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. """ @@ -451,14 +354,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: Optional[list[dict[str, Any]]] = None, + tool_choice: Optional[str] = "auto", + enable_thinking: Optional[bool] = 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 04a2b8d556..0000000000 --- a/frigate/genai/azure-openai.py +++ /dev/null @@ -1,315 +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 -from frigate.genai.openai import _stats_from_openai_usage - -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, - "stream_options": {"include_usage": 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" - usage_stats: Optional[dict[str, Any]] = None - - stream = self.provider.chat.completions.create(**request_params) # type: ignore[call-overload] - - for chunk in stream: - chunk_usage = getattr(chunk, "usage", None) - if chunk_usage is not None: - usage_stats = _stats_from_openai_usage(chunk_usage) - - if not chunk or not chunk.choices: - continue - - choice = chunk.choices[0] - delta = choice.delta - - # Check for finish reason - if choice.finish_reason: - finish_reason = choice.finish_reason - - # Extract 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" - - if usage_stats is not None: - yield ("stats", usage_stats) - - 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..a1325d3279 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, Optional from frigate.config import FrigateConfig from frigate.config.camera.genai import GenAIConfig, GenAIRoleEnum @@ -108,11 +108,16 @@ class GenAIClientManager: 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, + } 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 82% rename from frigate/genai/gemini.py rename to frigate/genai/plugins/gemini.py index c1046428e6..9efd241893 100644 --- a/frigate/genai/gemini.py +++ b/frigate/genai/plugins/gemini.py @@ -1,5 +1,7 @@ """Gemini Provider for Frigate AI.""" +import base64 +import binascii import json import logging from typing import Any, AsyncGenerator, Optional @@ -14,6 +16,27 @@ from frigate.genai import GenAIClient, register_genai_provider logger = logging.getLogger(__name__) +def _decode_thought_signature(value: Any) -> Optional[bytes]: + """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: Optional[bytes]) -> Optional[str]: + """Encode bytes thought_signature as base64 so it survives JSON-friendly transport.""" + if not signature: + return None + return base64.b64encode(signature).decode("ascii") + + def _stats_from_gemini_usage(usage: Any) -> Optional[dict[str, Any]]: """Build a stats dict from a Gemini usage_metadata object.""" prompt_tokens = getattr(usage, "prompt_token_count", None) @@ -62,6 +85,7 @@ class GeminiClient(GenAIClient): prompt: str, images: list[bytes], response_format: Optional[dict] = None, + enable_thinking: bool = False, ) -> Optional[str]: """Submit a request to Gemini.""" contents = [prompt] + [ @@ -119,11 +143,14 @@ class GeminiClient(GenAIClient): messages: list[dict[str, Any]], tools: Optional[list[dict[str, Any]]] = None, tool_choice: Optional[str] = "auto", + enable_thinking: Optional[bool] = 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 @@ -165,11 +192,17 @@ class GeminiClient(GenAIClient): if not isinstance(tc_args, dict): tc_args = {} if tc_name: - parts.append( - types.Part.from_function_call( - name=tc_name, args=tc_args - ) + 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)) @@ -248,6 +281,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) @@ -262,19 +302,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: @@ -294,9 +339,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: @@ -322,6 +372,7 @@ class GeminiClient(GenAIClient): return { "content": content, + "reasoning": reasoning, "tool_calls": tool_calls, "finish_reason": finish_reason, } @@ -330,6 +381,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", } @@ -339,6 +391,7 @@ class GeminiClient(GenAIClient): ) return { "content": None, + "reasoning": None, "tool_calls": None, "finish_reason": "error", } @@ -348,11 +401,14 @@ class GeminiClient(GenAIClient): messages: list[dict[str, Any]], tools: Optional[list[dict[str, Any]]] = None, tool_choice: Optional[str] = "auto", + enable_thinking: Optional[bool] = 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 @@ -394,11 +450,17 @@ class GeminiClient(GenAIClient): if not isinstance(tc_args, dict): tc_args = {} if tc_name: - parts.append( - types.Part.from_function_call( - name=tc_name, args=tc_args - ) + 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)) @@ -477,12 +539,19 @@ 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] = [] + reasoning_parts: list[str] = [] tool_calls_by_index: dict[int, dict[str, Any]] = {} finish_reason = "stop" usage_stats: Optional[dict[str, Any]] = None @@ -519,12 +588,16 @@ 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 try: @@ -553,6 +626,7 @@ class GeminiClient(GenAIClient): "id": tool_call_id, "name": tool_call_name, "arguments": "", + "thought_signature": None, } # Accumulate arguments @@ -563,8 +637,16 @@ class GeminiClient(GenAIClient): else str(arguments) ) + # Capture latest thought_signature for this call + chunk_sig = getattr(part, "thought_signature", None) + if chunk_sig: + tool_calls_by_index[found_index][ + "thought_signature" + ] = chunk_sig + # Build final message full_content = "".join(content_parts).strip() or None + full_reasoning = "".join(reasoning_parts).strip() or None # Convert tool calls to list format tool_calls_list = None @@ -582,6 +664,9 @@ class GeminiClient(GenAIClient): "id": tc["id"], "name": tc["name"], "arguments": parsed_args, + "thought_signature": _encode_thought_signature( + tc.get("thought_signature") + ), } ) finish_reason = "tool_calls" @@ -593,6 +678,7 @@ class GeminiClient(GenAIClient): "message", { "content": full_content, + "reasoning": full_reasoning, "tool_calls": tool_calls_list, "finish_reason": finish_reason, }, @@ -604,6 +690,7 @@ class GeminiClient(GenAIClient): "message", { "content": None, + "reasoning": None, "tool_calls": None, "finish_reason": "error", }, @@ -616,6 +703,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 83% rename from frigate/genai/llama_cpp.py rename to frigate/genai/plugins/llama_cpp.py index c935207bfe..d5458cf8f9 100644 --- a/frigate/genai/llama_cpp.py +++ b/frigate/genai/plugins/llama_cpp.py @@ -4,7 +4,7 @@ import base64 import io import json import logging -from typing import Any, AsyncGenerator, Optional +from typing import Any, AsyncGenerator, Optional, cast import httpx import numpy as np @@ -75,6 +75,29 @@ def _parse_launch_arg(args: list[str], flag: str) -> str | None: return args[idx + 1] +def _fetch_llama_props(base_url: str, model: str) -> dict[str, Any]: + """Fetch /props from a llama.cpp server, with llama-swap fallback. + + Raises the underlying RequestException if both endpoints fail; callers + decide how to surface the failure. + """ + try: + response = requests.get( + f"{base_url}/props", + params={"model": model}, + timeout=10, + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + except Exception: + response = requests.get( + f"{base_url}/upstream/{model}/props", + timeout=10, + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + def _to_jpeg(img_bytes: bytes) -> bytes | None: """Convert image bytes to JPEG. llama.cpp/STB does not support WebP.""" try: @@ -99,6 +122,7 @@ 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 @@ -112,6 +136,7 @@ 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__>" @@ -127,6 +152,10 @@ class LlamaCppClient(GenAIClient): 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) @@ -137,15 +166,17 @@ class LlamaCppClient(GenAIClient): 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", + "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 @@ -173,6 +204,7 @@ class LlamaCppClient(GenAIClient): "supports_vision": False, "supports_audio": False, "supports_tools": False, + "supports_reasoning": False, "media_marker": "<__media__>", } @@ -239,21 +271,7 @@ class LlamaCppClient(GenAIClient): info["supports_tools"] = True 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() + props = _fetch_llama_props(base_url, configured_model) if info["context_size"] is None: default_settings = props.get("default_generation_settings", {}) @@ -266,10 +284,17 @@ class LlamaCppClient(GenAIClient): 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"]: - chat_caps = props.get("chat_template_caps", {}) 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 @@ -287,6 +312,7 @@ class LlamaCppClient(GenAIClient): prompt: str, images: list[bytes], response_format: Optional[dict] = None, + enable_thinking: bool = False, ) -> Optional[str]: """Submit a request to llama.cpp server.""" if self.provider is None: @@ -314,7 +340,7 @@ class LlamaCppClient(GenAIClient): ) # Build request payload with llama.cpp native options - payload = { + payload: dict[str, Any] = { "model": self.genai_config.model, "messages": [ { @@ -328,6 +354,9 @@ class LlamaCppClient(GenAIClient): if response_format: payload["response_format"] = response_format + if self.supports_toggleable_thinking: + payload["chat_template_kwargs"] = {"enable_thinking": enable_thinking} + response = requests.post( f"{self.provider}/v1/chat/completions", json=payload, @@ -364,6 +393,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 ( @@ -491,6 +524,7 @@ class LlamaCppClient(GenAIClient): tools: Optional[list[dict[str, Any]]], tool_choice: Optional[str], stream: bool = False, + enable_thinking: Optional[bool] = None, ) -> dict[str, Any]: """Build request payload for chat completions (sync or stream).""" openai_tool_choice = None @@ -506,31 +540,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, } @@ -559,6 +609,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 = _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, @@ -583,30 +658,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 the server's media marker placeholder. - # The marker is randomized per server startup (read from /props). - content.append( - { - "prompt_string": f"{self._media_marker}\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 requests.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 @@ -669,6 +760,7 @@ class LlamaCppClient(GenAIClient): messages: list[dict[str, Any]], tools: Optional[list[dict[str, Any]]] = None, tool_choice: Optional[str] = "auto", + enable_thinking: Optional[bool] = None, ) -> dict[str, Any]: """ Send chat messages to llama.cpp server with optional tool definitions. @@ -686,7 +778,13 @@ class LlamaCppClient(GenAIClient): "finish_reason": "error", } try: - payload = self._build_payload(messages, tools, tool_choice, stream=False) + payload = self._build_payload( + messages, + tools, + tool_choice, + stream=False, + enable_thinking=enable_thinking, + ) response = requests.post( f"{self.provider}/v1/chat/completions", json=payload, @@ -734,6 +832,7 @@ class LlamaCppClient(GenAIClient): messages: list[dict[str, Any]], tools: Optional[list[dict[str, Any]]] = None, tool_choice: Optional[str] = "auto", + enable_thinking: Optional[bool] = None, ) -> AsyncGenerator[tuple[str, Any], None]: """Stream chat with tools via OpenAI-compatible streaming API.""" if self.provider is None: @@ -750,8 +849,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" @@ -781,6 +887,15 @@ class LlamaCppClient(GenAIClient): 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"]) @@ -806,6 +921,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" @@ -813,6 +929,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 88% rename from frigate/genai/ollama.py rename to frigate/genai/plugins/ollama.py index fe286f64de..08176f524b 100644 --- a/frigate/genai/ollama.py +++ b/frigate/genai/plugins/ollama.py @@ -98,6 +98,22 @@ class OllamaClient(GenAIClient): provider: ApiClient | None provider_options: dict[str, Any] + _supports_thinking_cache: Optional[bool] = 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: @@ -118,6 +134,9 @@ class OllamaClient(GenAIClient): 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"): @@ -175,6 +194,7 @@ class OllamaClient(GenAIClient): prompt: str, images: list[bytes], response_format: Optional[dict] = None, + enable_thinking: bool = False, ) -> Optional[str]: """Submit a request to Ollama""" if self.provider is None: @@ -191,6 +211,8 @@ 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", @@ -271,6 +293,7 @@ class OllamaClient(GenAIClient): tools: Optional[list[dict[str, Any]]], tool_choice: Optional[str], stream: bool = False, + enable_thinking: Optional[bool] = None, ) -> dict[str, Any]: """Build request_messages and params for chat (sync or stream).""" request_messages = [] @@ -309,11 +332,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]: @@ -336,6 +362,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"): @@ -348,6 +377,7 @@ class OllamaClient(GenAIClient): finish_reason = "stop" return { "content": content, + "reasoning": reasoning, "tool_calls": tool_calls, "finish_reason": finish_reason, } @@ -357,6 +387,7 @@ class OllamaClient(GenAIClient): messages: list[dict[str, Any]], tools: Optional[list[dict[str, Any]]] = None, tool_choice: Optional[str] = "auto", + enable_thinking: Optional[bool] = None, ) -> dict[str, Any]: if self.provider is None: logger.warning( @@ -369,7 +400,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) @@ -393,6 +428,7 @@ class OllamaClient(GenAIClient): messages: list[dict[str, Any]], tools: Optional[list[dict[str, Any]]] = None, tool_choice: Optional[str] = "auto", + enable_thinking: Optional[bool] = None, ) -> AsyncGenerator[tuple[str, Any], None]: """Stream chat with tools; yields content deltas then final message. @@ -422,7 +458,11 @@ 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, @@ -431,6 +471,9 @@ class OllamaClient(GenAIClient): ) 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) @@ -441,7 +484,11 @@ class OllamaClient(GenAIClient): 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, @@ -449,6 +496,7 @@ class OllamaClient(GenAIClient): 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) @@ -456,6 +504,10 @@ class OllamaClient(GenAIClient): 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) @@ -463,8 +515,10 @@ class OllamaClient(GenAIClient): 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", } @@ -481,6 +535,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 90% rename from frigate/genai/openai.py rename to frigate/genai/plugins/openai.py index 09e0cf5381..3e862f8fd5 100644 --- a/frigate/genai/openai.py +++ b/frigate/genai/plugins/openai.py @@ -38,7 +38,11 @@ class OpenAIClient(GenAIClient): context_size: Optional[int] = 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 = { @@ -57,6 +61,7 @@ class OpenAIClient(GenAIClient): prompt: str, images: list[bytes], response_format: Optional[dict] = None, + enable_thinking: bool = False, ) -> Optional[str]: """Submit a request to OpenAI.""" encoded_images = [base64.b64encode(image).decode("utf-8") for image in images] @@ -183,11 +188,14 @@ class OpenAIClient(GenAIClient): messages: list[dict[str, Any]], tools: Optional[list[dict[str, Any]]] = None, tool_choice: Optional[str] = "auto", + enable_thinking: Optional[bool] = 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 @@ -203,6 +211,7 @@ class OpenAIClient(GenAIClient): "model": self.genai_config.model, "messages": messages, "timeout": self.timeout, + **self.genai_config.runtime_options, } if tools: @@ -219,7 +228,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 @@ -235,6 +244,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: @@ -269,6 +282,7 @@ class OpenAIClient(GenAIClient): return { "content": content, + "reasoning": reasoning, "tool_calls": tool_calls, "finish_reason": finish_reason, } @@ -277,6 +291,7 @@ class OpenAIClient(GenAIClient): logger.warning("OpenAI request timed out: %s", str(e)) return { "content": None, + "reasoning": None, "tool_calls": None, "finish_reason": "error", } @@ -284,6 +299,7 @@ class OpenAIClient(GenAIClient): logger.warning("OpenAI returned an error: %s", str(e)) return { "content": None, + "reasoning": None, "tool_calls": None, "finish_reason": "error", } @@ -293,11 +309,15 @@ class OpenAIClient(GenAIClient): messages: list[dict[str, Any]], tools: Optional[list[dict[str, Any]]] = None, tool_choice: Optional[str] = "auto", + enable_thinking: Optional[bool] = 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 @@ -315,6 +335,7 @@ class OpenAIClient(GenAIClient): "timeout": self.timeout, "stream": True, "stream_options": {"include_usage": True}, + **self.genai_config.runtime_options, } if tools: @@ -333,11 +354,12 @@ 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: Optional[dict[str, Any]] = 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) @@ -354,6 +376,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) @@ -382,6 +413,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 @@ -410,6 +442,7 @@ class OpenAIClient(GenAIClient): "message", { "content": full_content, + "reasoning": full_reasoning, "tool_calls": tool_calls_list, "finish_reason": finish_reason, }, @@ -421,6 +454,7 @@ class OpenAIClient(GenAIClient): "message", { "content": None, + "reasoning": None, "tool_calls": None, "finish_reason": "error", }, @@ -431,6 +465,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..af6ddab889 --- /dev/null +++ b/frigate/genai/prompts.py @@ -0,0 +1,744 @@ +"""Prompt and response-format builders for GenAI features. + +Centralizes the per-feature prompt framing and structured-output schema +shaping so provider clients in :mod:`frigate.genai.plugins` only handle +transport. +""" + +import datetime +from typing import Any, Dict, List, Optional + +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: Optional[List[Dict[str, Any]]] = None, +) -> List[Dict[str, Any]]: + """ + Get OpenAI-compatible tool definitions for Frigate. + + Returns a list of tool definitions that can be used with OpenAI-compatible + function calling APIs. When semantic search is enabled, the search_objects + tool exposes an additional `semantic_query` parameter for descriptive + queries (e.g. "person riding a lawn mower") and find_similar_objects is + included. When attribute classification models are configured, an + `attribute` parameter is exposed for filtering by their labels. + """ + search_objects_properties: Dict[str, Any] = { + "camera": { + "type": "string", + "description": "Camera name to filter by (optional).", + }, + "label": { + "type": "string", + "description": ( + "Generic object class to filter by — one of the tracked detector " + "labels such as 'person', 'package', 'car', 'dog', 'bird'. Use " + "this for broad queries like 'show me all cars today'. Combine " + "with semantic_query when the user also describes appearance or " + "behavior (e.g. label='person', semantic_query='riding a lawn " + "mower')." + ), + }, + "sub_label": { + "type": "string", + "description": ( + "Filter by a DISCRETE NAMED entity recognized in the detection. " + "Use this for: a known person's name ('John'), a delivery " + "company ('Amazon', 'UPS'), a recognized animal species or " + "breed ('blue jay', 'cardinal', 'golden retriever'), or a " + "license plate string. When filtering by a specific name, set " + "only sub_label and leave label unset. Do NOT use sub_label " + "for descriptions of appearance, clothing, or actions — those " + "belong in semantic_query." + ), + }, + "after": { + "type": "string", + "description": "Start time in ISO 8601 format (e.g., '2024-01-01T00:00:00Z').", + }, + "before": { + "type": "string", + "description": "End time in ISO 8601 format (e.g., '2024-01-01T23:59:59Z').", + }, + "zones": { + "type": "array", + "items": {"type": "string"}, + "description": "List of zone names to filter by.", + }, + "limit": { + "type": "integer", + "description": "Maximum number of objects to return (default: 25).", + "default": 25, + }, + } + + if attribute_classifications: + model_outline = "; ".join( + f"{m['name']} (applies to {', '.join(m['objects']) or 'any object'})" + for m in attribute_classifications + ) + search_objects_properties["attribute"] = { + "type": "string", + "description": ( + "Filter by a classification attribute label produced by a " + "configured attribute classification model. Use this INSTEAD " + "of semantic_query when the user's request matches one of " + "these classifications. Configured models: " + f"{model_outline}. " + "Set the value to the attribute label that matches the user's " + "phrasing (case-sensitive)." + ), + } + + if semantic_search_enabled: + search_objects_properties["semantic_query"] = { + "type": "string", + "description": ( + "Optional natural-language description of a PHYSICAL " + "CHARACTERISTIC, APPEARANCE, or ACTIVITY the user mentioned, " + "used to semantically narrow results. Only set this when the " + "user describes something beyond what label and sub_label can " + "express on their own.\n" + "USE for descriptive phrases like: 'riding a lawn mower', " + "'wearing a red jacket', 'carrying a package', 'walking a " + "dog', 'on a bicycle', 'holding an umbrella'.\n" + "DO NOT USE for:\n" + "- specific named people, pets, or delivery companies → use sub_label\n" + "- animal species or breed names like 'blue jay', 'cardinal', " + "'golden retriever' → use sub_label\n" + "- license plate strings → use sub_label\n" + "- generic object queries like 'all cars today' or 'every " + "person' → use label alone with no semantic_query\n" + "When set, combine with label/time/camera/zone filters as " + "usual (e.g. label='person', semantic_query='riding a lawn " + "mower', after='2024-05-01T00:00:00Z')." + ), + } + + search_objects_description = ( + "Search the historical record of detected objects in Frigate. " + "Use this ONLY for questions about the PAST — e.g. 'did anyone come by today?', " + "'when was the last car?', 'show me detections from yesterday'. " + "Do NOT use this for monitoring or alerting requests about future events — " + "use start_camera_watch instead for those. " + "An 'object' in Frigate represents a tracked detection (e.g., a person, package, car).\n\n" + "Choose filters based on what the user is asking for:\n" + "- Generic class query ('show me all cars today'): set `label` only.\n" + "- Specific NAMED entity (known person, delivery company, animal " + "species/breed like 'blue jay' or 'golden retriever', license " + "plate): set `sub_label` only and leave `label` unset.\n" + ) + if semantic_search_enabled: + search_objects_description += ( + "- Physical CHARACTERISTIC, APPEARANCE, or ACTIVITY that is not a " + "discrete name ('person riding a lawn mower', 'someone in a red " + "jacket', 'person carrying a package'): set `semantic_query` with " + "the descriptive phrase, optionally alongside `label` for the " + "object class. Do NOT put descriptive phrases in sub_label." + ) + + return [ + { + "type": "function", + "function": { + "name": "search_objects", + "description": search_objects_description, + "parameters": { + "type": "object", + "properties": search_objects_properties, + }, + "required": [], + }, + }, + { + "type": "function", + "function": { + "name": "find_similar_objects", + "description": ( + "Find tracked objects that are visually and semantically similar " + "to a specific past event. Use this when the user references a " + "particular object they have seen and wants to find other " + "sightings of the same or similar one ('that green car', 'the " + "person in the red jacket', 'the package that was delivered'). " + "Prefer this over search_objects whenever the user's intent is " + "'find more like this specific one.' Use search_objects first " + "only if you need to locate the anchor event. Requires semantic " + "search to be enabled." + ), + "parameters": { + "type": "object", + "properties": { + "event_id": { + "type": "string", + "description": "The id of the anchor event to find similar objects to.", + }, + "after": { + "type": "string", + "description": "Start time in ISO 8601 format (e.g., '2024-01-01T00:00:00Z').", + }, + "before": { + "type": "string", + "description": "End time in ISO 8601 format (e.g., '2024-01-01T23:59:59Z').", + }, + "cameras": { + "type": "array", + "items": {"type": "string"}, + "description": "Optional list of cameras to restrict to. Defaults to all.", + }, + "labels": { + "type": "array", + "items": {"type": "string"}, + "description": "Optional list of labels to restrict to. Defaults to the anchor event's label.", + }, + "sub_labels": { + "type": "array", + "items": {"type": "string"}, + "description": "Optional list of sub_labels (names) to restrict to.", + }, + "zones": { + "type": "array", + "items": {"type": "string"}, + "description": "Optional list of zones. An event matches if any of its zones overlap.", + }, + "similarity_mode": { + "type": "string", + "enum": ["visual", "semantic", "fused"], + "description": "Which similarity signal(s) to use. 'fused' (default) combines visual and semantic.", + "default": "fused", + }, + "min_score": { + "type": "number", + "description": "Drop matches with a similarity score below this threshold (0.0-1.0).", + }, + "limit": { + "type": "integer", + "description": "Maximum number of matches to return (default: 10).", + "default": 10, + }, + }, + "required": ["event_id"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "set_camera_state", + "description": ( + "Change a camera's feature state (e.g., turn detection on/off, enable/disable recordings). " + "Use camera='*' to apply to all cameras at once. " + "Only call this tool when the user explicitly asks to change a camera setting. " + "Requires admin privileges." + ), + "parameters": { + "type": "object", + "properties": { + "camera": { + "type": "string", + "description": "Camera name to target, or '*' to target all cameras.", + }, + "feature": { + "type": "string", + "enum": [ + "detect", + "record", + "snapshots", + "audio", + "motion", + "enabled", + "birdseye", + "birdseye_mode", + "improve_contrast", + "ptz_autotracker", + "motion_contour_area", + "motion_threshold", + "notifications", + "audio_transcription", + "review_alerts", + "review_detections", + "object_descriptions", + "review_descriptions", + "profile", + ], + "description": ( + "The feature to change. Most features accept ON or OFF. " + "birdseye_mode accepts CONTINUOUS, MOTION, or OBJECTS. " + "motion_contour_area and motion_threshold accept a number. " + "profile accepts a profile name or 'none' to deactivate (requires camera='*')." + ), + }, + "value": { + "type": "string", + "description": "The value to set. ON or OFF for toggles, a number for thresholds, a profile name or 'none' for profile.", + }, + }, + "required": ["camera", "feature", "value"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "get_live_context", + "description": ( + "Get the current live image and detection information for a single camera: objects being tracked, " + "zones, timestamps. Use this to understand what is visible in the live view. " + "Call this when answering questions about what is happening right now on a specific camera. " + "Operates on one camera at a time; call the tool again for each additional camera. " + "Wildcards and empty values are not accepted." + ), + "parameters": { + "type": "object", + "properties": { + "camera": { + "type": "string", + "description": ( + "Exact name of a single camera to get live context for. " + "Wildcards (e.g. '*', 'all') and empty strings are not accepted." + ), + }, + }, + "required": ["camera"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "start_camera_watch", + "description": ( + "Start a continuous VLM watch job that monitors a camera and sends a notification " + "when a specified condition is met. Use this when the user wants to be alerted about " + "a future event, e.g. 'tell me when guests arrive' or 'notify me when the package is picked up'. " + "Only one watch job can run at a time. Returns a job ID." + ), + "parameters": { + "type": "object", + "properties": { + "camera": { + "type": "string", + "description": "Camera ID to monitor.", + }, + "condition": { + "type": "string", + "description": ( + "Natural-language description of the condition to watch for, " + "e.g. 'a person arrives at the front door'." + ), + }, + "max_duration_minutes": { + "type": "integer", + "description": "Maximum time to watch before giving up (minutes, default 60).", + "default": 60, + }, + "labels": { + "type": "array", + "items": {"type": "string"}, + "description": "Object labels that should trigger a VLM check (e.g. ['person', 'car']). If omitted, any detection on the camera triggers a check.", + }, + "zones": { + "type": "array", + "items": {"type": "string"}, + "description": "Zone names to filter by. If specified, only detections in these zones trigger a VLM check.", + }, + }, + "required": ["camera", "condition"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "stop_camera_watch", + "description": ( + "Cancel the currently running VLM watch job. Use this when the user wants to " + "stop a previously started watch, e.g. 'stop watching the front door'." + ), + "parameters": { + "type": "object", + "properties": {}, + "required": [], + }, + }, + }, + { + "type": "function", + "function": { + "name": "get_profile_status", + "description": ( + "Get the current profile status including the active profile and " + "timestamps of when each profile was last activated. Use this to " + "determine time periods for recap requests — e.g. when the user asks " + "'what happened while I was away?', call this first to find the relevant " + "time window based on profile activation history." + ), + "parameters": { + "type": "object", + "properties": {}, + "required": [], + }, + }, + }, + { + "type": "function", + "function": { + "name": "get_recap", + "description": ( + "Get a recap of all activity (alerts and detections) for a given time period. " + "Use this after calling get_profile_status to retrieve what happened during " + "a specific window — e.g. 'what happened while I was away?'. Returns a " + "chronological list of activity with camera, objects, zones, and GenAI-generated " + "descriptions when available. Summarize the results for the user." + ), + "parameters": { + "type": "object", + "properties": { + "after": { + "type": "string", + "description": "Start of the time period in ISO 8601 format (e.g. '2025-03-15T08:00:00').", + }, + "before": { + "type": "string", + "description": "End of the time period in ISO 8601 format (e.g. '2025-03-15T17:00:00').", + }, + "cameras": { + "type": "string", + "description": "Comma-separated camera IDs to include, or 'all' for all cameras. Default is 'all'.", + }, + "severity": { + "type": "string", + "enum": ["alert", "detection"], + "description": "Filter by severity level. Omit to include both alerts and detections.", + }, + }, + "required": ["after", "before"], + }, + }, + }, + ] + + +def build_chat_system_prompt( + config: FrigateConfig, + allowed_cameras: List[str], + semantic_search_enabled: bool, + attribute_classifications: List[Dict[str, Any]], +) -> str: + """Build the system prompt for the chat completion endpoint. + + Composes the static framing with conditional sections describing the + available cameras, speed units, semantic-search routing guidance, and + configured attribute classifications. + """ + current_datetime = datetime.datetime.now() + current_date_str = current_datetime.strftime("%Y-%m-%d") + current_time_str = current_datetime.strftime("%I:%M:%S %p") + + cameras_info: List[str] = [] + has_speed_zone = False + for camera_id in allowed_cameras: + if camera_id not in config.cameras: + continue + camera_config = config.cameras[camera_id] + friendly_name = ( + camera_config.friendly_name + if camera_config.friendly_name + else camera_id.replace("_", " ").title() + ) + zone_names = list(camera_config.zones.keys()) + if not has_speed_zone: + has_speed_zone = any( + zone.distances for zone in camera_config.zones.values() + ) + if zone_names: + cameras_info.append( + f" - {friendly_name} (ID: {camera_id}, zones: {', '.join(zone_names)})" + ) + else: + cameras_info.append(f" - {friendly_name} (ID: {camera_id})") + + cameras_section = "" + if cameras_info: + cameras_section = ( + "\n\nAvailable cameras:\n" + + "\n".join(cameras_info) + + "\n\nWhen users refer to cameras by their friendly name (e.g., 'Back Deck Camera'), use the corresponding camera ID (e.g., 'back_deck_cam') in tool calls." + ) + + speed_units_section = "" + if has_speed_zone: + speed_unit = ( + "mph" if config.ui.unit_system == UnitSystemEnum.imperial else "km/h" + ) + speed_units_section = f"\n\nReport object speeds to the user in {speed_unit}." + + semantic_search_section = "" + if semantic_search_enabled: + semantic_search_section = ( + "\n\nWhen routing a search_objects call, pick filters by the shape of the user's request:\n" + "- Generic class ('show me all cars today'): set `label` only.\n" + "- Specific named entity — a known person ('John'), delivery company ('Amazon'), animal species/breed ('blue jay', 'cardinal', 'golden retriever'), or license plate: set `sub_label` only and leave `label` unset.\n" + "- Physical characteristic, appearance, or activity that is NOT a discrete name ('find me people riding a lawn mower', 'someone in a red jacket', 'a person carrying a package'): set `semantic_query` with the descriptive phrase, optionally combined with `label` for the object class. Never put descriptive phrases in `sub_label`." + ) + + attribute_classification_section = "" + if attribute_classifications: + model_lines = "\n".join( + f"- {m['name']}: applies to {', '.join(m['objects']) or 'any object'}" + for m in attribute_classifications + ) + attribute_classification_section = ( + "\n\nAttribute classification models are configured for the following object types:\n" + f"{model_lines}\n" + "When the user's request matches one of these classifications, set the search_objects `attribute` field to the matching label rather than using `semantic_query`. Reserve `semantic_query` for descriptive phrases that fall outside the configured attribute labels." + ) + + return f"""You are a helpful assistant for Frigate, a security camera NVR system. You help users answer questions about their cameras, detected objects, and events. + +Current server local date and time: {current_date_str} at {current_time_str} + +Do not start your response with phrases like "I will check...", "Let me see...", or "Let me look...". Answer directly. + +Always present times to the user in the server's local timezone. When tool results include start_time_local and end_time_local, use those exact strings when listing or describing detection times—do not convert or invent timestamps. Do not use UTC or ISO format with Z for the user-facing answer unless the tool result only provides Unix timestamps without local time fields. +When users ask about "today", "yesterday", "this week", etc., use the current date above as reference. +When searching for objects or events, use ISO 8601 format for dates (e.g., {current_date_str}T00:00:00Z for the start of today). +Always be accurate with time calculations based on the current date provided. + +When a user refers to a specific object they have seen or describe with identifying details ("that green car", "the person in the red jacket", "a package left today"), prefer the find_similar_objects tool over search_objects. Use search_objects first only to locate the anchor event, then pass its id to find_similar_objects. For generic queries like "show me all cars today", keep using search_objects. If a user message begins with [attached_event:], treat that event id as the anchor for any similarity or "tell me more" request in the same message and call find_similar_objects with that id.{semantic_search_section}{attribute_classification_section}{cameras_section}{speed_units_section}""" diff --git a/frigate/genai/utils.py b/frigate/genai/utils.py index 44f982059b..a382647cb9 100644 --- a/frigate/genai/utils.py +++ b/frigate/genai/utils.py @@ -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 index 0616c46290..3d8b2d6b63 100644 --- a/frigate/jobs/debug_replay.py +++ b/frigate/jobs/debug_replay.py @@ -1,4 +1,4 @@ -"""Debug replay startup job: ffmpeg concat + camera config publish. +"""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 @@ -12,6 +12,7 @@ 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 @@ -23,7 +24,7 @@ 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 Recordings +from frigate.models import Export, Recordings from frigate.types import JobStatusTypesEnum from frigate.util.ffmpeg import run_ffmpeg_with_progress @@ -114,6 +115,130 @@ def query_recordings(source_camera: str, start_ts: float, end_ts: float) -> Mode 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. @@ -126,6 +251,7 @@ class DebugReplayJobRunner(threading.Thread): def __init__( self, job: DebugReplayJob, + source: DebugReplaySource, frigate_config: FrigateConfig, config_publisher: CameraConfigUpdatePublisher, replay_manager: "DebugReplayManager", @@ -133,6 +259,7 @@ class DebugReplayJobRunner(threading.Thread): ) -> 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 @@ -183,7 +310,6 @@ class DebugReplayJobRunner(threading.Thread): def run(self) -> None: replay_name = self.job.replay_camera_name os.makedirs(REPLAY_DIR, exist_ok=True) - concat_file = os.path.join(REPLAY_DIR, f"{replay_name}_concat.txt") clip_path = os.path.join(REPLAY_DIR, f"{replay_name}.mp4") self.job.status = JobStatusTypesEnum.running @@ -192,23 +318,13 @@ class DebugReplayJobRunner(threading.Thread): self._broadcast(force=True) try: - recordings = query_recordings( - self.job.source_camera, self.job.start_ts, self.job.end_ts - ) - with open(concat_file, "w") as f: - for recording in recordings: - f.write(f"file '{recording.path}'\n") + input_args = self.source.ffmpeg_input_args(REPLAY_DIR) ffmpeg_cmd = [ self.frigate_config.ffmpeg.ffmpeg_path, "-hide_banner", "-y", - "-f", - "concat", - "-safe", - "0", - "-i", - concat_file, + *input_args, "-c", "copy", "-movflags", @@ -285,7 +401,7 @@ class DebugReplayJobRunner(threading.Thread): self.replay_manager.clear_session() _remove_silent(clip_path) finally: - _remove_silent(concat_file) + self.source.cleanup(REPLAY_DIR) _set_active_runner(None) def _finalize_cancelled(self, clip_path: str) -> None: @@ -309,52 +425,43 @@ def _remove_silent(path: str) -> None: def start_debug_replay_job( *, - source_camera: str, - start_ts: float, - end_ts: float, + source: DebugReplaySource, frigate_config: FrigateConfig, config_publisher: CameraConfigUpdatePublisher, replay_manager: "DebugReplayManager", ) -> str: """Validate, create job, start runner. Returns the job id. - Raises ValueError for bad params (camera missing, time range - invalid, no recordings) and RuntimeError if a session is already - active. + 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_camera not in frigate_config.cameras: - raise ValueError(f"Camera '{source_camera}' not found") + if source.source_camera not in frigate_config.cameras: + raise ValueError(f"Camera '{source.source_camera}' not found") - if end_ts <= start_ts: - raise ValueError("End time must be after start time") + source.validate() - recordings = query_recordings(source_camera, start_ts, end_ts) - if not recordings.count(): - raise ValueError( - f"No recordings found for camera '{source_camera}' in the specified time range" - ) - - replay_name = f"{REPLAY_CAMERA_PREFIX}{source_camera}" + replay_name = f"{REPLAY_CAMERA_PREFIX}{source.source_camera}" replay_manager.mark_starting( - source_camera=source_camera, + source_camera=source.source_camera, replay_camera_name=replay_name, - start_ts=start_ts, - end_ts=end_ts, + start_ts=source.start_ts, + end_ts=source.end_ts, ) job = DebugReplayJob( - source_camera=source_camera, + source_camera=source.source_camera, replay_camera_name=replay_name, - start_ts=start_ts, - end_ts=end_ts, + 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, diff --git a/frigate/jobs/motion_search.py b/frigate/jobs/motion_search.py index 1a90f0bb9e..13fc841e99 100644 --- a/frigate/jobs/motion_search.py +++ b/frigate/jobs/motion_search.py @@ -3,6 +3,8 @@ 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 @@ -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,13 +95,16 @@ 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: Optional[float] = None + progress: float = 0.0 + # Metrics for observability metrics: Optional[MotionSearchMetrics] = None @@ -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), 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/object_detection/base.py b/frigate/object_detection/base.py index a62fe48431..f2336f3da8 100644 --- a/frigate/object_detection/base.py +++ b/frigate/object_detection/base.py @@ -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/output.py b/frigate/output/output.py index 79ef349c8f..67dba5221f 100644 --- a/frigate/output/output.py +++ b/frigate/output/output.py @@ -342,20 +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") + if loc == "clips": + src = preview_cache + dst = preview_holdover + elif loc == "cache": + src = preview_holdover + dst = preview_cache + else: + return + try: - if loc == "clips": - shutil.move(preview_cache, preview_holdover) - elif loc == "cache": - if not os.path.exists(preview_holdover): - return + if not os.path.exists(src): + return - if not os.access(preview_holdover, os.R_OK | os.W_OK): - logger.error( - "Insufficient permissions on preview restart cache at %s", - preview_holdover, - ) - return + shutil.move(src, dst) - shutil.move(preview_holdover, preview_cache) + 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/ptz/autotrack.py b/frigate/ptz/autotrack.py index 1a45f619c2..fb76f6718d 100644 --- a/frigate/ptz/autotrack.py +++ b/frigate/ptz/autotrack.py @@ -1331,6 +1331,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/record/export.py b/frigate/record/export.py index 9f571a5a5c..e89742b1ab 100644 --- a/frigate/record/export.py +++ b/frigate/record/export.py @@ -456,7 +456,7 @@ class RecordingExporter(threading.Thread): 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", @@ -579,7 +579,9 @@ class RecordingExporter(threading.Thread): else: chapters_path = self._build_chapter_metadata_file(recordings) chapter_args = ( - f" -i {chapters_path} -map 0 -map_metadata 1" if chapters_path else "" + f" -i {chapters_path} -map 0 -dn -map_metadata 1" + if chapters_path + else "" ) ffmpeg_cmd = ( f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input}{chapter_args} -c copy -movflags +faststart" diff --git a/frigate/stats/emitter.py b/frigate/stats/emitter.py index 2b34c7c4ed..13f50c5868 100644 --- a/frigate/stats/emitter.py +++ b/frigate/stats/emitter.py @@ -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 diff --git a/frigate/stats/util.py b/frigate/stats/util.py index a0141d1305..56efce5d01 100644 --- a/frigate/stats/util.py +++ b/frigate/stats/util.py @@ -1,6 +1,7 @@ """Utilities for stats.""" import asyncio +import logging import os import shutil import time @@ -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: @@ -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 = [] @@ -231,12 +240,16 @@ async def set_gpu_stats( 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() @@ -253,7 +266,7 @@ async def set_gpu_stats( else: stats["nvidia-gpu"] = {"vendor": "nvidia", "gpu": "", "mem": ""} - hwaccel_errors.append(args) + hwaccel_errors[args] = time.monotonic() elif "nvmpi" in args or "jetson" in args: # nvidia Jetson jetson_usage = get_jetson_stats() @@ -262,7 +275,7 @@ async def set_gpu_stats( stats["jetson-gpu"] = {"vendor": "nvidia", **jetson_usage} else: stats["jetson-gpu"] = {"vendor": "nvidia", "gpu": "", "mem": ""} - hwaccel_errors.append(args) + 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 @@ -280,7 +293,7 @@ async def set_gpu_stats( stats[name] = entry else: stats["intel-gpu"] = {"vendor": "intel", "gpu": "", "mem": ""} - hwaccel_errors.append(args) + hwaccel_errors[args] = time.monotonic() elif "vaapi" in args: if not config.telemetry.stats.amd_gpu_stats: continue @@ -292,7 +305,7 @@ async def set_gpu_stats( stats["amd-vaapi"] = {"vendor": "amd", **amd_usage} else: stats["amd-vaapi"] = {"vendor": "amd", "gpu": "", "mem": ""} - hwaccel_errors.append(args) + hwaccel_errors[args] = time.monotonic() elif "preset-rk" in args: rga_usage = get_rockchip_gpu_stats() @@ -328,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/test/http_api/test_debug_replay_api.py b/frigate/test/http_api/test_debug_replay_api.py index 45c2c5478f..be4e7f496f 100644 --- a/frigate/test/http_api/test_debug_replay_api.py +++ b/frigate/test/http_api/test_debug_replay_api.py @@ -15,11 +15,12 @@ class TestDebugReplayAPI(BaseTestHttp): # 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=kwargs["source_camera"], + source_camera=source.source_camera, replay_camera_name="_replay_front", - start_ts=kwargs["start_ts"], - end_ts=kwargs["end_ts"], + start_ts=source.start_ts, + end_ts=source.end_ts, ) return "job-1234" diff --git a/frigate/test/http_api/test_http_app.py b/frigate/test/http_api/test_http_app.py index 2be0e65da8..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 @@ -71,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_media.py b/frigate/test/http_api/test_http_media.py index 6af3dd9724..58f9f92477 100644 --- a/frigate/test/http_api/test_http_media.py +++ b/frigate/test/http_api/test_http_media.py @@ -403,3 +403,75 @@ 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() == [] 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_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_config.py b/frigate/test/test_config.py index c63f27430a..6490a65099 100644 --- a/frigate/test/test_config.py +++ b/frigate/test/test_config.py @@ -64,9 +64,9 @@ class TestConfig(unittest.TestCase): def test_config_class(self): frigate_config = FrigateConfig(**self.minimal) - assert "ov" in frigate_config.detectors.keys() - assert frigate_config.detectors["ov"].type == DetectorTypeEnum.openvino - assert frigate_config.detectors["ov"].model.width == 300 + assert "cpu" in frigate_config.detectors.keys() + assert frigate_config.detectors["cpu"].type == DetectorTypeEnum.cpu + assert frigate_config.detectors["cpu"].model.width == 320 @patch("frigate.detectors.detector_config.load_labels") def test_detector_custom_model_path(self, mock_labels): @@ -1673,5 +1673,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 index e7f9df42db..a91f759c44 100644 --- a/frigate/test/test_debug_replay.py +++ b/frigate/test/test_debug_replay.py @@ -71,6 +71,14 @@ class TestDebugReplayManagerSession(unittest.TestCase): 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 diff --git a/frigate/test/test_debug_replay_job.py b/frigate/test/test_debug_replay_job.py index 60997564f4..12c4be82b8 100644 --- a/frigate/test/test_debug_replay_job.py +++ b/frigate/test/test_debug_replay_job.py @@ -9,6 +9,7 @@ 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, @@ -99,9 +100,12 @@ class TestStartDebugReplayJob(unittest.TestCase): def test_rejects_unknown_camera(self) -> None: with self.assertRaises(ValueError): start_debug_replay_job( - source_camera="missing", - start_ts=100.0, - end_ts=200.0, + 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, @@ -110,9 +114,12 @@ class TestStartDebugReplayJob(unittest.TestCase): def test_rejects_invalid_time_range(self) -> None: with self.assertRaises(ValueError): start_debug_replay_job( - source_camera="front", - start_ts=200.0, - end_ts=100.0, + 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, @@ -124,9 +131,12 @@ class TestStartDebugReplayJob(unittest.TestCase): with patch("frigate.jobs.debug_replay.query_recordings", return_value=empty_qs): with self.assertRaises(ValueError): start_debug_replay_job( - source_camera="front", - start_ts=100.0, - end_ts=200.0, + 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, @@ -154,9 +164,12 @@ class TestStartDebugReplayJob(unittest.TestCase): patch("builtins.open", unittest.mock.mock_open()), ): job_id = start_debug_replay_job( - source_camera="front", - start_ts=100.0, - end_ts=200.0, + 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, @@ -191,9 +204,12 @@ class TestStartDebugReplayJob(unittest.TestCase): patch("builtins.open", unittest.mock.mock_open()), ): start_debug_replay_job( - source_camera="front", - start_ts=100.0, - end_ts=200.0, + 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, @@ -201,9 +217,12 @@ class TestStartDebugReplayJob(unittest.TestCase): with self.assertRaises(RuntimeError): start_debug_replay_job( - source_camera="front", - start_ts=100.0, - end_ts=200.0, + 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, @@ -269,9 +288,12 @@ class TestRunnerHappyPath(unittest.TestCase): patch("builtins.open", unittest.mock.mock_open()), ): start_debug_replay_job( - source_camera="front", - start_ts=100.0, - end_ts=200.0, + 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, @@ -340,9 +362,12 @@ class TestRunnerFailurePath(unittest.TestCase): patch("builtins.open", unittest.mock.mock_open()), ): start_debug_replay_job( - source_camera="front", - start_ts=100.0, - end_ts=200.0, + 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, @@ -418,9 +443,12 @@ class TestRunnerCancellation(unittest.TestCase): patch("builtins.open", unittest.mock.mock_open()), ): start_debug_replay_job( - source_camera="front", - start_ts=100.0, - end_ts=200.0, + 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, 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_media_auth.py b/frigate/test/test_media_auth.py index 80dc6fb8b0..d025fea614 100644 --- a/frigate/test/test_media_auth.py +++ b/frigate/test/test_media_auth.py @@ -230,7 +230,7 @@ class TestExportResolution(unittest.TestCase): id=export_id, camera=camera, name=f"export-{export_id}", - date=datetime.datetime.now(), + date=int(datetime.datetime.now().timestamp()), video_path=f"/media/frigate/exports/{filename}", thumb_path=f"/media/frigate/exports/{filename}.jpg", in_progress=False, 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_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_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_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/object_processing.py b/frigate/track/object_processing.py index 3f8b5e626d..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( diff --git a/frigate/util/classification.py b/frigate/util/classification.py index 66bacdeb04..30fc4c6687 100644 --- a/frigate/util/classification.py +++ b/frigate/util/classification.py @@ -394,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) @@ -566,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, diff --git a/frigate/util/config.py b/frigate/util/config.py index 71e2af809d..5a4f3816c0 100644 --- a/frigate/util/config.py +++ b/frigate/util/config.py @@ -8,7 +8,13 @@ from typing import Any, Optional, Union 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) @@ -603,6 +644,16 @@ 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 @@ -773,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/image.py b/frigate/util/image.py index 2d2133c6b8..d2832d97a0 100644 --- a/frigate/util/image.py +++ b/frigate/util/image.py @@ -1089,10 +1089,25 @@ class SharedMemoryFrameManager(FrameManager): def get(self, name: str, shape) -> Optional[np.ndarray]: 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: diff --git a/frigate/util/services.py b/frigate/util/services.py index 4a715608e2..0445053b8a 100644 --- a/frigate/util/services.py +++ b/frigate/util/services.py @@ -416,6 +416,11 @@ def get_intel_gpu_stats( 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() @@ -424,6 +429,9 @@ def get_intel_gpu_stats( 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]: @@ -464,6 +472,10 @@ def get_intel_gpu_stats( 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() @@ -478,7 +490,7 @@ def get_intel_gpu_stats( overall_pct = min(100.0, compute_pct + dec_pct) entry: dict[str, Any] = { - "name": names.get(pdev) or f"Intel GPU {pdev}", + "name": names.get(pdev) or "Intel iGPU", "vendor": "intel", "gpu": f"{round(overall_pct, 2)}%", "mem": "-%", diff --git a/generate_config_translations.py b/generate_config_translations.py index 9bc830855d..7f9c9bc504 100644 --- a/generate_config_translations.py +++ b/generate_config_translations.py @@ -364,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/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/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/replay.spec.ts b/web/e2e/specs/replay.spec.ts index c09abf10b2..51a42737f0 100644 --- a/web/e2e/specs/replay.spec.ts +++ b/web/e2e/specs/replay.spec.ts @@ -129,8 +129,14 @@ test.describe("Replay — active session @medium", () => { ); await actionGroup.first().click(); - const dialog = frigateApp.page.getByRole("dialog"); - await expect(dialog).toBeVisible({ timeout: 5_000 }); + // 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 ({ 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/index.html b/web/index.html index be6b302f5c..bb019d4263 100644 --- a/web/index.html +++ b/web/index.html @@ -3,7 +3,7 @@ - + Frigate - + Frigate 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,7 +282,7 @@ "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": { diff --git a/web/public/locales/ca/views/faceLibrary.json b/web/public/locales/ca/views/faceLibrary.json index ea19924ac2..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.", diff --git a/web/public/locales/ca/views/live.json b/web/public/locales/ca/views/live.json index 20db54905b..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", @@ -151,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 index cf41e934d1..585f6cbb69 100644 --- a/web/public/locales/ca/views/motionSearch.json +++ b/web/public/locales/ca/views/motionSearch.json @@ -26,7 +26,9 @@ "points_many": "{{count}} punts", "points_other": "{{count}} punts", "undo": "Desfés l'últim punt", - "reset": "Restableix el polígon" + "reset": "Restableix el polígon", + "drawMode": "Dibuxa", + "moveMode": "Moure" }, "motionHeatmapLabel": "Mapa de calor del moviment", "dialog": { @@ -42,11 +44,11 @@ "settings": { "title": "Configuració de la cerca", "parallelMode": "Mode paral·lel", - "parallelModeDesc": "Escaneja múltiples segments d'enregistrament al mateix temps (més ràpid, però significativament més intensiu en CPU)", + "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": "Percentatge mínim de la regió d'interès que s'ha de canviar per considerar-se significatiu", + "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", @@ -72,6 +74,9 @@ "framesDecoded": "Fotogrames descodificats", "wallTime": "Temps de cerca", "segmentErrors": "Errors del segment", - "seconds": "{{seconds}}s" - } + "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 index 36eccd8a6c..8dc6730cc3 100644 --- a/web/public/locales/ca/views/replay.json +++ b/web/public/locales/ca/views/replay.json @@ -55,5 +55,5 @@ "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 la fragata a partir del metratge de reproducció." + "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 b540b05861..5ff0242d9a 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", @@ -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", @@ -1292,19 +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", @@ -1350,18 +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 la fragata per aplicar els canvis.", + "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": { @@ -1484,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" @@ -1659,9 +1788,11 @@ "searchPlaceholder": "Cerca...", "genaiRoles": { "options": { - "embeddings": "Incrustació", + "embeddings": "Vectors", "vision": "Visió", - "tools": "Eines" + "tools": "Eines", + "descriptions": "Descripcions", + "chat": "Xat" } }, "semanticSearchModel": { @@ -1676,13 +1807,43 @@ }, "addCustomLabel": "Afegeix una etiqueta personalitzada...", "genaiModel": { - "placeholder": "Selecciona el model…", - "search": "Cerca models…", - "noModels": "No hi ha models disponibles" + "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": { @@ -1718,7 +1879,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", @@ -1743,7 +1907,15 @@ "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", @@ -1827,8 +1999,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", @@ -1838,14 +2020,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.", - "genaiImageSourceRecordingsRecordDisabled": "La font d'imatges està configurada com a 'enregistraments', però l'enregistrament està desactivat. La fragata tornarà a la vista prèvia de les imatges." + "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." @@ -1855,7 +2044,13 @@ }, "detect": { "fpsGreaterThanFive": "No es recomana establir el detect FPS superior a 5. Els valors més alts poden causar problemes de rendiment i no proporcionaran cap benefici.", - "disabled": "La detecció d'objectes està desactivada. Les instantànies, articles de revisió i enriquiments com el reconeixement de rostres, el reconeixement de matrícules i la IA Generativa no funcionaran." + "disabled": "La detecció d'objectes està desactivada. Les instantànies, articles de revisió i enriquiments com el reconeixement de rostres, el reconeixement de matrícules i la IA Generativa no funcionaran.", + "resolutionShouldBeMultipleOfFour": "Per obtenir els millors resultats, detectar l'amplada i l'alçada han de ser múltiples de 4. Altres valors parells poden produir artefactes visuals o una lleugera distorsió en el flux de detecció.", + "aspectRatioMismatch": "L'amplada i alçada que heu introduït no coincideixen amb la relació d'aspecte de la resolució de detecció actual. Això pot produir una imatge estirada o distorsionada.", + "maxFramesSet": "La configuració dels fotogrames màxims anul·la el comportament predeterminat i desactiva el seguiment d'objectes estacionaris. Hi ha molt poques situacions en què això sigui necessari, utilitzeu-lo amb precaució.", + "squareResolution": "Una resolució de detecció quadrada és inusual. L'amplada i l'alçada de la detecció han de coincidir amb la relació d'aspecte de la càmera (per exemple, 16:9), no amb les dimensions del model de detecció d'objectes. Una relació d'aspecte no coincident pot estirar la imatge i reduir la precisió de detecció.", + "resolutionHigh": "Aquesta resolució de detecció és més alta del recomanat i pot causar un ús més elevat dels recursos sense millorar la precisió de detecció. Es recomana una resolució de detecció a o per sota de 1080p per a la majoria de les càmeres.", + "globalResolutionMultipleCameras": "S'estableix una resolució de detecció global mentre es configuren diverses càmeres. Tret que totes les càmeres comparteixin la mateixa resolució i relació d'aspecte, l'amplada i l'alçada de la detecció s'haurien de definir per càmera perquè coincideixi amb la relació d'aspecte nativa de cada càmera." }, "faceRecognition": { "globalDisabled": "L'enriquiment del reconeixement facial s'ha d'habilitar perquè les funcions de reconeixement facial funcionin en aquesta càmera.", @@ -1884,7 +2079,110 @@ "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." + "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." } + }, + "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 595e7f8f60..70b5ee6fe1 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" } @@ -220,7 +220,7 @@ }, "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}}%)", @@ -234,14 +234,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/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/de/common.json b/web/public/locales/de/common.json index 7f9848fe28..00d9b8b60b 100644 --- a/web/public/locales/de/common.json +++ b/web/public/locales/de/common.json @@ -192,7 +192,9 @@ "bg": "Български (bulgarisch)", "gl": "Galego (Galicisch)", "id": "Bahasa Indonesia (Indonesisch)", - "hr": "Hrvatski (Kroatisch)" + "hr": "Hrvatski (Kroatisch)", + "bs": "Bosnisch", + "zhHant": "Traditional Chinese" }, "appearance": "Erscheinung", "theme": { @@ -325,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/player.json b/web/public/locales/de/components/player.json index ad56cf2ce4..6294301446 100644 --- a/web/public/locales/de/components/player.json +++ b/web/public/locales/de/components/player.json @@ -48,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 7080fe186a..4a479c2c23 100644 --- a/web/public/locales/de/config/cameras.json +++ b/web/public/locales/de/config/cameras.json @@ -9,7 +9,7 @@ "description": "Aktiviert" }, "audio": { - "label": "Audioerkennung", + "label": "Audioereignisse", "description": "Einstellungen für audiobasierte Ereigniserkennung für diese Kamera.", "enabled": { "label": "Aktivieren der Audioerkennung", @@ -25,7 +25,11 @@ }, "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", @@ -822,7 +826,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 9df4d44c8a..78712b1035 100644 --- a/web/public/locales/de/config/global.json +++ b/web/public/locales/de/config/global.json @@ -8,7 +8,7 @@ "description": "Wenn aktiviert, startet Frigate im abgesicherten Modus mit reduzierten Features für die Fehlersuche." }, "audio": { - "label": "Audioerkennung", + "label": "Audioereignisse", "enabled": { "label": "Aktivieren der Audioerkennung", "description": "Aktivieren oder deaktivieren Sie die Erkennung von Audioereignissen für alle Kameras; diese Einstellung kann für jede Kamera individuell überschrieben werden." @@ -23,7 +23,11 @@ }, "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", @@ -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", @@ -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": { 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..4380ef181e 100644 --- a/web/public/locales/de/objects.json +++ b/web/public/locales/de/objects.json @@ -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 index 5a87ce9e10..9f4dcb7f2d 100644 --- a/web/public/locales/de/views/chat.json +++ b/web/public/locales/de/views/chat.json @@ -42,5 +42,31 @@ "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/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 5405265314..cfee8b7233 100644 --- a/web/public/locales/de/views/live.json +++ b/web/public/locales/de/views/live.json @@ -144,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", @@ -162,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 index 3008f10d85..9ad72c4616 100644 --- a/web/public/locales/de/views/motionSearch.json +++ b/web/public/locales/de/views/motionSearch.json @@ -24,7 +24,9 @@ "points_one": "{{count}} Punkt", "points_other": "{{count}} Punkte", "undo": "Letzten Schritt rückgängig machen", - "reset": "Polygon zurücksetzen" + "reset": "Polygon zurücksetzen", + "drawMode": "ziehen", + "moveMode": "bewegen" }, "motionHeatmapLabel": "Bewegungs-Heatmap", "dialog": { diff --git a/web/public/locales/de/views/settings.json b/web/public/locales/de/views/settings.json index 6193333491..3d8e9d3b83 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", @@ -92,7 +93,8 @@ "uiSettings": "Benutzeroberfläche Einstellung", "profiles": "Profile", "systemGo2rtcStreams": "go2rtc-streams", - "maintenance": "Wartung" + "maintenance": "Wartung", + "systemDetectorsAndModel": "Detektoren und Modell" }, "dialog": { "unsavedChanges": { @@ -728,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.", @@ -800,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}}", @@ -817,7 +828,8 @@ "currentModel": "Aktuelles Modell", "otherModels": "Anderes Modell", "configuration": "Konfiguration" - } + }, + "changeInDetectorsAndModel": "Modell wechseln" }, "enrichments": { "birdClassification": { @@ -1125,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" @@ -1345,19 +1357,41 @@ "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 und die externe URL, 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)." + }, + "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", @@ -1403,18 +1437,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": "Kamerytyp", - "label": "Kameratyp", + "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": { @@ -1489,7 +1611,13 @@ "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", @@ -1726,7 +1854,9 @@ "options": { "embeddings": "Einbetten", "vision": "Vision", - "tools": "Werkzeuge" + "tools": "Werkzeuge", + "descriptions": "Beschreibung", + "chat": "Chat" } }, "semanticSearchModel": { @@ -1756,9 +1886,39 @@ "platePlaceholder": "Kennzeichen oder regulärer Ausdruck" }, "genaiModel": { - "placeholder": "Modell auswählen…", - "search": "Modell suchen…", - "noModels": "Keine Modelle verfügbar" + "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": { @@ -1792,7 +1952,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", @@ -1879,12 +2041,29 @@ "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": { @@ -1901,7 +2080,9 @@ }, "detect": { "fpsGreaterThanFive": "Es wird nicht empfohlen, den Wert für die FPS-Erkennung auf mehr als 5 zu setzen. Höhere Werte können zu Leistungseinbußen führen und bieten keinerlei Vorteile.", - "disabled": "Die Objekterkennung ist deaktiviert. Momentaufnahmen, Überprüfungselemente und Erweiterungsfunktionen wie Gesichtserkennung, Kennzeichenerkennung und generative KI funktionieren nicht." + "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." }, "faceRecognition": { "globalDisabled": "Die Gesichtserkennungserweiterung muss aktiviert sein, damit die Gesichtserkennungsfunktionen bei dieser Kamera funktionieren.", @@ -1931,6 +2112,101 @@ }, "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." } + }, + "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/en/common.json b/web/public/locales/en/common.json index c5d9c4aca3..a6e6daa1e1 100644 --- a/web/public/locales/en/common.json +++ b/web/public/locales/en/common.json @@ -178,6 +178,7 @@ "en": "English (English)", "es": "Español (Spanish)", "zhCN": "简体中文 (Simplified Chinese)", + "zhHant": "繁體中文 (Traditional Chinese)", "hi": "हिन्दी (Hindi)", "fr": "Français (French)", "ar": "العربية (Arabic)", @@ -317,5 +318,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/player.json b/web/public/locales/en/components/player.json index 6ceef7e0cd..011baaa1bf 100644 --- a/web/public/locales/en/components/player.json +++ b/web/public/locales/en/components/player.json @@ -12,7 +12,7 @@ "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 f645dd33a1..98e625abf7 100644 --- a/web/public/locales/en/config/cameras.json +++ b/web/public/locales/en/config/cameras.json @@ -682,7 +682,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)." @@ -862,6 +862,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": { @@ -950,4 +954,4 @@ "label": "Original camera state", "description": "Keep track of original state of camera." } -} \ No newline at end of file +} diff --git a/web/public/locales/en/config/global.json b/web/public/locales/en/config/global.json index b10f0a7afc..119d384e43 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", @@ -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." @@ -921,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": { @@ -1519,6 +1546,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": { @@ -1597,4 +1628,4 @@ "description": "Ignore time synchronization differences between camera and Frigate server for ONVIF communication." } } -} \ No newline at end of file +} 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/views/chat.json b/web/public/locales/en/views/chat.json index bc320c2049..363b0e68e4 100644 --- a/web/public/locales/en/views/chat.json +++ b/web/public/locales/en/views/chat.json @@ -60,5 +60,13 @@ "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/events.json b/web/public/locales/en/views/events.json index 103d93b7eb..f25e3022c5 100644 --- a/web/public/locales/en/views/events.json +++ b/web/public/locales/en/views/events.json @@ -67,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..d1087b3c96 100644 --- a/web/public/locales/en/views/explore.json +++ b/web/public/locales/en/views/explore.json @@ -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/live.json b/web/public/locales/en/views/live.json index 3aa892222a..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", @@ -153,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/settings.json b/web/public/locales/en/views/settings.json index 3b2d6561f6..5dbb01b33b 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" @@ -39,6 +40,11 @@ "profilePrefix": "{{profile}} profile: {{fields}}" } }, + "menuDot": { + "overrideGlobal": "This section overrides the global configuration", + "overrideProfile": "This section is overridden by the {{profile}} profile", + "unsaved": "This section has unsaved changes" + }, "menu": { "general": "General", "globalConfig": "Global configuration", @@ -69,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", @@ -98,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", @@ -315,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": { @@ -452,7 +457,7 @@ }, "cameraManagement": { "title": "Manage Cameras", - "description": "Add, edit, and delete cameras, control which cameras are enabled, 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.", + "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": { @@ -470,17 +475,33 @@ "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.", - "friendlyName": { - "edit": "Edit camera display name", - "title": "Edit Display Name", - "description": "Set the friendly name shown for this camera throughout the Frigate UI. Leave blank to use the camera ID.", - "rename": "Rename" + "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": { @@ -515,10 +536,10 @@ "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", @@ -527,6 +548,92 @@ "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": { @@ -1051,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", @@ -1142,7 +1250,7 @@ "loading": "Loading model information…", "error": "Failed to load model information", "noModelLoaded": "No Frigate+ model is currently loaded.", - "availableModels": "Available Models", + "availableModels": "Available Frigate+ models", "loadingAvailableModels": "Loading available models…", "selectModel": "Select a model", "noModelsAvailable": "No models available", @@ -1153,6 +1261,7 @@ }, "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": { @@ -1160,14 +1269,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", @@ -1378,6 +1503,17 @@ "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" }, @@ -1521,6 +1657,9 @@ "builtIn": "Built-in Models", "genaiProviders": "GenAI Providers" }, + "semanticSearchModelSize": { + "notApplicable": "Not applicable for GenAI providers" + }, "review": { "title": "Review Settings" }, @@ -1539,9 +1678,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": { @@ -1573,6 +1728,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." @@ -1629,6 +1786,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", @@ -1662,7 +1820,15 @@ "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": { @@ -1670,6 +1836,13 @@ "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": { @@ -1690,12 +1863,6 @@ "12hour": "12 hour", "24hour": "24 hour" }, - "TimeOrDateStyle": { - "full": "Full", - "long": "Long", - "medium": "Medium", - "short": "Short" - }, "unitSystem": { "metric": "Metric", "imperial": "Imperial" @@ -1746,7 +1913,13 @@ }, "detect": { "fpsGreaterThanFive": "Setting the detect FPS higher than 5 is not recommended. Higher values may cause performance issues and will not provide any benefit.", - "disabled": "Object detection is disabled. Snapshots, review items, and enrichments such as face recognition, license plate recognition, and Generative AI will not function." + "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." }, "objects": { "genaiNoDescriptionsProvider": "You must configure a GenAI provider with the 'descriptions' role for descriptions to be generated." @@ -1776,6 +1949,9 @@ }, "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/es/common.json b/web/public/locales/es/common.json index 8faa18fe75..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": { @@ -196,7 +198,10 @@ "uiPlayground": "Zona de pruebas de la interfaz de usuario", "faceLibrary": "Biblioteca de rostros", "classification": "Clasificación", - "profiles": "Perfiles" + "profiles": "Perfiles", + "actions": "Acciones", + "features": "Funciones", + "chat": "Chat" }, "unit": { "speed": { @@ -252,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": { @@ -260,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." }, @@ -314,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 848285dafa..c52e427bb6 100644 --- a/web/public/locales/es/components/dialog.json +++ b/web/public/locales/es/components/dialog.json @@ -71,16 +71,77 @@ "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", "case": { "label": "Caso", - "newCaseDescriptionPlaceholder": "Descripción de 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" + }, + "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": { @@ -130,7 +191,12 @@ "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." + "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 c277d9a5a2..e650046466 100644 --- a/web/public/locales/es/components/player.json +++ b/web/public/locales/es/components/player.json @@ -48,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 d6a120fdfd..aa1f09becc 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", @@ -28,14 +28,19 @@ }, "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": { @@ -50,29 +55,79 @@ }, "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." + "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." + "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)." + "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." + "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." + "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)." + "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).", @@ -106,23 +161,41 @@ "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." + "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." + "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." + "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." + "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." + "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." + "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": { @@ -142,148 +215,743 @@ }, "send_triggers": { "after_significant_updates": { - "description": "Envía una solicitud a GenAI tras un número especificado de actualizaciones significativas del objeto rastreado." + "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.)." + "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." + "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." + "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" + "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 vivo y de voz, utilizada para eventos y subtítulos en tiempo real.", + "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" + "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." + "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." + "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)." + "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." + "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)." + "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)." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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)." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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 (\"5.0\" o \"7.0\")." + "description": "Ruta al binario de FFmpeg que se va a utilizar o un alias de versión (\"5.0\" o \"7.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." + "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." + "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." + } + } + }, + "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 1fc7df1ccd..ee7210f359 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", @@ -42,15 +43,21 @@ }, "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", @@ -72,16 +79,32 @@ "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" + "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." + "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. " + "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." + "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": { @@ -91,24 +114,73 @@ }, "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." + "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." + "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)." + "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." + "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." + "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)." + "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": { @@ -128,31 +200,138 @@ }, "send_triggers": { "after_significant_updates": { - "description": "Envía una solicitud a GenAI tras un número especificado de actualizaciones significativas del objeto rastreado." + "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.)." + "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." + "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." + "description": "Lista de etiquetas de objetos a rastrear para todas las cámaras; puede anularse por cámara.", + "label": "Objetos a rastrear" + }, + "label": "Objetos", + "description": "Valores predeterminados de seguimiento de objetos, incluidas las etiquetas que se rastrean y los filtros por objeto.", + "filters": { + "label": "Filtros de objetos", + "description": "Filtros aplicados a los objetos detectados para reducir falsos positivos (área, relación, confianza).", + "min_area": { + "label": "Área mínima de objeto", + "description": "Área mínima del cuadro delimitador (píxeles o porcentaje) necesaria para este tipo de objeto. Puede ser píxeles (int) o porcentaje (float entre 0.000001 y 0.99)." + }, + "max_area": { + "label": "Área máxima de objeto", + "description": "Área máxima del cuadro delimitador (píxeles o porcentaje) permitida para este tipo de objeto. Puede ser píxeles (int) o porcentaje (float entre 0.000001 y 0.99)." + }, + "min_ratio": { + "label": "Relación de aspecto mínima", + "description": "Relación mínima anchura/altura necesaria para que el cuadro delimitador sea válido." + }, + "max_ratio": { + "label": "Relación de aspecto máxima", + "description": "Relación máxima anchura/altura permitida para que el cuadro delimitador sea válido." + }, + "threshold": { + "label": "Umbral de confianza", + "description": "Umbral medio de confianza de detección necesario para que el objeto se considere un positivo verdadero." + }, + "min_score": { + "label": "Confianza mínima", + "description": "Confianza mínima de detección en un único fotograma necesaria para que el objeto se contabilice." + }, + "mask": { + "label": "Máscara de filtro", + "description": "Coordenadas del polígono que definen dónde se aplica este filtro dentro del fotograma." + }, + "raw_mask": { + "label": "Máscara sin procesar" + } + }, + "mask": { + "label": "Máscara de objeto", + "description": "Polígono de máscara usado para evitar la detección de objetos en áreas especificadas." + }, + "filters_attribute": { + "label": "Filtros de atributos", + "description": "Filtros aplicados a los atributos detectados para reducir falsos positivos (área, proporción y confianza).", + "min_area": { + "label": "Área mínima del atributo", + "description": "Área mínima del cuadro delimitador (en píxeles o porcentaje) necesaria para este atributo. Puede expresarse en píxeles (entero) o como porcentaje (valor decimal entre 0.000001 y 0.99)." + }, + "max_area": { + "label": "Área máxima del atributo", + "description": "Área máxima del cuadro delimitador (en píxeles o porcentaje) permitida para este atributo. Puede expresarse en píxeles (entero) o como porcentaje (valor decimal entre 0.000001 y 0.99)." + }, + "min_ratio": { + "label": "Relación de aspecto mínima", + "description": "Relación mínima entre anchura y altura necesaria para que el cuadro delimitador se considere válido." + }, + "max_ratio": { + "label": "Relación de aspecto máxima", + "description": "Relación máxima entre anchura y altura permitida para que el cuadro delimitador se considere válido." + }, + "threshold": { + "label": "Umbral de confianza", + "description": "Umbral medio de confianza de detección necesario para que el atributo se considere un verdadero positivo." + }, + "min_score": { + "label": "Confianza mínima", + "description": "Confianza mínima de detección en un único fotograma necesaria para asociar este atributo con su objeto principal." + }, + "mask": { + "label": "Máscara de filtro", + "description": "Coordenadas del polígono que definen dónde se aplica este filtro dentro del fotograma." + }, + "raw_mask": { + "label": "Máscara sin procesar" + } } }, "detectors": { "deepstack": { "description": "Detector DeepStack/CodeProject.AI que envía imágenes a una API HTTP remota de DeepStack para la inferencia. No recomendado.", "api_url": { - "description": "La URL de la API de DeepStack." + "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" @@ -161,96 +340,303 @@ "cpu": { "label": "CPU", "num_threads": { - "label": "Número de hilos para detección" + "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" + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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" + "label": "Puerto MQTT", + "description": "Puerto del broker MQTT (normalmente 1883 para MQTT sin cifrar)." }, "tls_client_cert": { - "label": "Certificado cliente" + "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." + "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." + "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" - } + "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" + "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" + "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." - } + "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" + "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 por proxy cuando no se aplica ningún mapeo de roles (administrador o espectador)." + "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." + "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": { @@ -261,18 +647,28 @@ "description": "Habilitar la recopilación de estadísticas de la GPU Intel si hay una GPU Intel presente." }, "network_bandwidth": { - "label": "Ancho de banda" + "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": "Identificador de dispositivo utilizado al tratar las GPU Intel como SR-IOV para corregir las estadísticas de la GPU." - } + "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." + "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": { @@ -283,180 +679,961 @@ "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 vivo y de voz, utilizada para eventos y subtítulos en tiempo real.", + "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." + "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." + "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." + "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." + "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)." + "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." - } + "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." + "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)." + "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." + "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." + "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)." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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)." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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." + "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 (\"5.0\" o \"7.0\")." + "description": "Ruta al binario de FFmpeg que se va a utilizar o un alias de versión (\"5.0\" o \"7.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." + "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." + "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." + "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)." + "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)." + "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." + "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." + "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." + "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/validation.json b/web/public/locales/es/config/validation.json index b78ae972f3..dc35c3e1bb 100644 --- a/web/public/locales/es/config/validation.json +++ b/web/public/locales/es/config/validation.json @@ -28,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 fe4d16915e..94adda5cb9 100644 --- a/web/public/locales/es/objects.json +++ b/web/public/locales/es/objects.json @@ -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 index 0967ef424b..7fe10c75be 100644 --- a/web/public/locales/es/views/chat.json +++ b/web/public/locales/es/views/chat.json @@ -1 +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 0f5ec539b9..93bc070b77 100644 --- a/web/public/locales/es/views/classificationModel.json +++ b/web/public/locales/es/views/classificationModel.json @@ -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,7 +66,7 @@ "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": "Ninguno" }, @@ -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 cc2306da06..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}}?", @@ -23,11 +25,11 @@ "editName": "Editar nombre", "deleteExport": "Eliminar exportación", "assignToCase": "Añadir al caso", - "removeFromCase": "Remover del contenedor" + "removeFromCase": "Eliminar del caso" }, "headings": { "cases": "Casos", - "uncategorizedExports": "Exportaciones sin Categorizar" + "uncategorizedExports": "Exportaciones sin categorización" }, "caseDialog": { "title": "Añadir al caso", @@ -38,10 +40,89 @@ "descriptionLabel": "Descripción" }, "toolbar": { - "addExport": "Añadir Exportación" + "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}}?" + "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 4bc98b5cbc..877e3ef63b 100644 --- a/web/public/locales/es/views/live.json +++ b/web/public/locales/es/views/live.json @@ -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 index 0967ef424b..2d1103aeb4 100644 --- a/web/public/locales/es/views/motionSearch.json +++ b/web/public/locales/es/views/motionSearch.json @@ -1 +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 index 0967ef424b..f1b7a84f97 100644 --- a/web/public/locales/es/views/replay.json +++ b/web/public/locales/es/views/replay.json @@ -1 +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 075b7131ff..99aa74e98d 100644 --- a/web/public/locales/es/views/settings.json +++ b/web/public/locales/es/views/settings.json @@ -16,7 +16,8 @@ "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", @@ -31,7 +32,7 @@ "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", @@ -42,7 +43,7 @@ "globalDetect": "Detección de Objetos", "globalRecording": "Grabación", "globalSnapshots": "Instantáneas", - "globalFfmpeg": "FFmpeg", + "globalFfmpeg": "arguments,Introduce", "globalMotion": "Detección de Movimiento", "globalObjects": "Objetos", "globalReview": "Revisión", @@ -50,7 +51,49 @@ "globalLivePlayback": "Reproducción en Vivo", "globalTimestampStyle": "Estilo de Marca de Tiempo", "systemDatabase": "Base de Datos", - "systemAuthentication": "Autenticación" + "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": "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": { @@ -59,7 +102,7 @@ } }, "cameraSetting": { - "camera": "Cámara", + "camera": "Overrides,Sobrescrituras", "noCamera": "Sin cámara" }, "general": { @@ -303,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": { @@ -314,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": { @@ -370,7 +423,8 @@ "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." + "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": { @@ -411,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", @@ -437,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", @@ -652,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", @@ -714,7 +790,7 @@ "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.", + "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": { @@ -726,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": { @@ -741,7 +825,14 @@ }, "restart_required": "Es necesario reiniciar (se ha cambiado el modelo Frigate+)", "unsavedChanges": "Cambios en la configuración de Frigate+ no guardados", - "description": "Frigate+ es un servicio de suscripción que proporciona acceso a funciones y capacidades adicionales para su instancia de Frigate, incluida la posibilidad de utilizar modelos de detección de objetos personalizados entrenados con sus propios datos. Puede gestionar la configuración de sus modelos de Frigate+ aquí." + "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", @@ -767,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." @@ -1001,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.", @@ -1157,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", @@ -1190,9 +1282,45 @@ "selectCamera": "Seleccione una cámara", "backToSettings": "Volver a configuración de la cámara", "streams": { - "title": "Habilitar/deshabilitar cámaras", + "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 por completo el procesamiento de las transmisiones 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." + "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", @@ -1224,7 +1352,123 @@ } }, "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." + "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": { @@ -1268,33 +1512,324 @@ "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", "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." + "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." - } + "desc": "La cuadrícula de regiones es una optimización que aprende dónde suelen aparecer los objetos de diferentes tamaños en el campo de visión de cada cámara. Frigate utiliza estos datos para dimensionar de forma eficiente las regiones de detección. La cuadrícula se construye automáticamente a lo largo del tiempo a partir de los datos de los objetos rastreados.", + "title": "Cuadrícula de regiones", + "clear": "Borrar cuadrícula de regiones", + "clearConfirmTitle": "Borrar cuadrícula de regiones", + "clearSuccess": "Cuadrícula de regiones borrada correctamente", + "clearError": "No se pudo borrar la cuadrícula de regiones", + "restartRequired": "Es necesario reiniciar para que los cambios de la cuadrícula de regiones surtan efecto" + }, + "title": "Mantenimiento" }, "configForm": { "camera": { "noCameras": "No hay cámaras disponibles", - "description": "Estos ajustes se aplican únicamente a esta cámara y anulan los ajustes globales." + "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" + "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." + "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}}" + }, + "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": { @@ -1330,7 +1865,10 @@ "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." + "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", @@ -1364,26 +1902,85 @@ "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." + "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." + "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." + "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." + "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." @@ -1392,17 +1989,175 @@ "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." + "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." + "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." } }, "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+." + "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 e0a0157a1a..23ee553a12 100644 --- a/web/public/locales/es/views/system.json +++ b/web/public/locales/es/views/system.json @@ -55,7 +55,10 @@ }, "count_other": "{{count}} mensajes", "count_one": "{{count}} mensaje", - "empty": "No se han capturado mensaje aún" + "empty": "No se han capturado mensaje aún", + "expanded": { + "payload": "Carga útil" + } } }, "title": "Sistema", @@ -209,6 +212,9 @@ "unusable": "No usable", "fair": "Normal", "stallsLastHour": "Bloqueos (última hora)" + }, + "noCameras": { + "title": "No se han encontrado cámaras" } }, "lastRefreshed": "Última actualización: ", diff --git a/web/public/locales/et/common.json b/web/public/locales/et/common.json index 4455b56973..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", @@ -315,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/player.json b/web/public/locales/et/components/player.json index bbf77830d8..7553e3c6a1 100644 --- a/web/public/locales/et/components/player.json +++ b/web/public/locales/et/components/player.json @@ -48,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 6c2bc5811d..157daddb4c 100644 --- a/web/public/locales/et/config/cameras.json +++ b/web/public/locales/et/config/cameras.json @@ -18,5 +18,22 @@ "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 ab44041b5c..adb9777008 100644 --- a/web/public/locales/et/config/global.json +++ b/web/public/locales/et/config/global.json @@ -6,5 +6,51 @@ "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/validation.json b/web/public/locales/et/config/validation.json index ce014359a8..c9eb465ed5 100644 --- a/web/public/locales/et/config/validation.json +++ b/web/public/locales/et/config/validation.json @@ -28,5 +28,8 @@ "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 index cf68fe1e85..ca3f7f6e4d 100644 --- a/web/public/locales/et/views/chat.json +++ b/web/public/locales/et/views/chat.json @@ -5,5 +5,6 @@ "placeholder": "Küsi mida iganes…", "error": "Midagi läks valesti. Palun proovi uuesti.", "processing": "Töötlen…", - "toolsUsed": "Kasutatud: {{tools}}" + "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/explore.json b/web/public/locales/et/views/explore.json index b3bdbef570..dee52f6e20 100644 --- a/web/public/locales/et/views/explore.json +++ b/web/public/locales/et/views/explore.json @@ -35,7 +35,9 @@ "zones": "Tsoonid", "ratio": "Suhtarv", "area": "Ala", - "score": "Punktiskoor" + "score": "Punktiskoor", + "computedScore": "Arvutatud punktiskoor", + "topScore": "Suuremad punktiskoorid" }, "external": "{{label}} on tuvastatud", "heard": "{{label}} on kuuldud", @@ -79,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 64e8fb90e1..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": { @@ -35,7 +37,7 @@ "details": { "timestamp": "Ajatampel", "unknown": "Pole teada", - "scoreInfo": "Skoor on kõigi nägude hindete kaalutud keskmine, kus kaalukoefitsiendiks on iga pildi näo suurus." + "scoreInfo": "Punktiskoor on kõigi nägude hinnete kaalutud keskmine, kus kaalukoefitsiendiks on iga pildi näo suurus." }, "uploadFaceImage": { "title": "Laadi näopilt üles", diff --git a/web/public/locales/et/views/live.json b/web/public/locales/et/views/live.json index 27f8ff3fd8..e7f050758b 100644 --- a/web/public/locales/et/views/live.json +++ b/web/public/locales/et/views/live.json @@ -12,7 +12,8 @@ "transcription": "Heli üleskirjutus", "snapshots": "Hetkvõtted", "autotracking": "Automaatne jälgimine", - "recording": "Salvestus" + "recording": "Salvestus", + "camera": "Kaamera" }, "documentTitle": { "default": "Frigate reaalajas" @@ -73,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", diff --git a/web/public/locales/et/views/motionSearch.json b/web/public/locales/et/views/motionSearch.json index 0967ef424b..72d1bad347 100644 --- a/web/public/locales/et/views/motionSearch.json +++ b/web/public/locales/et/views/motionSearch.json @@ -1 +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 index 2f80829a43..4ef29cd2b2 100644 --- a/web/public/locales/et/views/replay.json +++ b/web/public/locales/et/views/replay.json @@ -13,5 +13,6 @@ "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 6780001fbe..6388029696 100644 --- a/web/public/locales/et/views/search.json +++ b/web/public/locales/et/views/search.json @@ -23,7 +23,13 @@ "search_type": "Otsingutüüp", "time_range": "Ajavahemik", "before": "Enne", - "after": "Pärast" + "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", @@ -32,9 +38,36 @@ "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." + "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" + "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/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/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/fr/common.json b/web/public/locales/fr/common.json index ff940a27d3..d49150bd9d 100644 --- a/web/public/locales/fr/common.json +++ b/web/public/locales/fr/common.json @@ -188,7 +188,8 @@ "gl": "Galego (Galicien)", "id": "Bahasa Indonesia (Indonésien)", "ur": "اردو (Ourdou)", - "hr": "Hrvatski (Croate)" + "hr": "Hrvatski (Croate)", + "bs": "Bosanski (Bosnien)" }, "appearance": "Apparence", "darkMode": { @@ -332,5 +333,8 @@ "separatorWithSpace": ", " }, "no_items": "Aucun élément", - "validation_errors": "Erreurs de validation" + "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 6204915d09..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", diff --git a/web/public/locales/fr/components/dialog.json b/web/public/locales/fr/components/dialog.json index 4465dcd449..7e0dda6aa6 100644 --- a/web/public/locales/fr/components/dialog.json +++ b/web/public/locales/fr/components/dialog.json @@ -67,7 +67,8 @@ "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." }, "select": "Sélectionner", "name": { @@ -112,7 +113,7 @@ "title_one": "Export {{count}} revue", "title_many": "Export {{count}} revues", "title_other": "Export {{count}} revues", - "description": "Export chaque revue sélectionnée. Tous les exports sont regroupés sous un cas unique.", + "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", @@ -120,12 +121,14 @@ "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": "", + "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é." + "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}}" } } }, diff --git a/web/public/locales/fr/config/global.json b/web/public/locales/fr/config/global.json index 1108b2d1b7..97093cc257 100644 --- a/web/public/locales/fr/config/global.json +++ b/web/public/locales/fr/config/global.json @@ -112,5 +112,8 @@ "label": "Rapport d'aspect minimal" } } + }, + "audio_transcription": { + "label": "Transcription audio" } } diff --git a/web/public/locales/fr/objects.json b/web/public/locales/fr/objects.json index afc5791aea..fdd517c84c 100644 --- a/web/public/locales/fr/objects.json +++ b/web/public/locales/fr/objects.json @@ -121,5 +121,9 @@ "royal_mail": "Poste du Royaume Uni", "school_bus": "Bus scolaire", "skunk": "Mouffette", - "kangaroo": "Kangourou" + "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 index 5d75f17db7..72736b3d91 100644 --- a/web/public/locales/fr/views/chat.json +++ b/web/public/locales/fr/views/chat.json @@ -16,5 +16,7 @@ "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" + "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/replay.json b/web/public/locales/fr/views/replay.json index e230ad3f7c..abafbe755f 100644 --- a/web/public/locales/fr/views/replay.json +++ b/web/public/locales/fr/views/replay.json @@ -3,6 +3,31 @@ "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" + "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 3a45a6ef9c..d6891dc1d7 100644 --- a/web/public/locales/fr/views/settings.json +++ b/web/public/locales/fr/views/settings.json @@ -1377,6 +1377,54 @@ "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": { diff --git a/web/public/locales/id/audio.json b/web/public/locales/id/audio.json index c7cb4475a3..b1ea21a628 100644 --- a/web/public/locales/id/audio.json +++ b/web/public/locales/id/audio.json @@ -1,98 +1,503 @@ { - "yell": "Teriakan", - "speech": "Percakapan", - "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", - "sound_effect": "Efek Suara", - "environmental_noise": "Suara Lingkungan", - "static": "Statis", - "white_noise": "Suara Derau", + "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" + "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 c5eb6634aa..455b3f4bf4 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": "Sampai", + "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,296 @@ "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" + } + }, + "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/config/cameras.json b/web/public/locales/id/config/cameras.json index 9151d7d340..3b80196174 100644 --- a/web/public/locales/id/config/cameras.json +++ b/web/public/locales/id/config/cameras.json @@ -1,3 +1,62 @@ { - "label": "Pengaturan Kamera" + "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 345b593f5c..9b8409655f 100644 --- a/web/public/locales/id/config/global.json +++ b/web/public/locales/id/config/global.json @@ -1,5 +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/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/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 79775d60bf..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": "Ubah 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 36202b238c..416d688964 100644 --- a/web/public/locales/id/views/live.json +++ b/web/public/locales/id/views/live.json @@ -1,56 +1,57 @@ { - "documentTitle.withCamera": "{{camera}} - Langsung - Frigate", + "documentTitle.withCamera": "{{camera}} - Live - Frigate", "documentTitle": { - "default": "Siaran Langsung - Frigate" + "default": "Live - Frigate" }, - "lowBandwidthMode": "Mode Bandwith-Rendah", + "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" @@ -61,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 index 0967ef424b..3a098d03ee 100644 --- a/web/public/locales/id/views/motionSearch.json +++ b/web/public/locales/id/views/motionSearch.json @@ -1 +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 index 0967ef424b..d47dbd0c90 100644 --- a/web/public/locales/id/views/replay.json +++ b/web/public/locales/id/views/replay.json @@ -1 +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 831d9bb684..d738cdd080 100644 --- a/web/public/locales/id/views/settings.json +++ b/web/public/locales/id/views/settings.json @@ -4,64 +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", - "object": "Pengawakutu - Frigate", - "enrichments": "Frigate - Pengaturan Pengayaan", - "cameraManagement": "Pengaturan Kamera - Frigate", - "cameraReview": "Pengaturan Ulasan Kamera - Frigate", + "general": "Pengaturan UI - Frigate", + "object": "Debug - Frigate", + "enrichments": "Pengaturan Pengayaan - Frigate", + "cameraManagement": "Kelola Kamera - Frigate", + "cameraReview": "Pengaturan Tinjauan Kamera - Frigate", "frigatePlus": "Pengaturan Frigate+ - Frigate", - "notifications": "Pengaturan Notifikasi - Frigate" + "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": "Pendeteksi suara tidak dinyalakan untuk kamera ini. Transkripsi suara memerlukan pendeteksi suara untuk dinyalakan." + "audioDetectionDisabled": "Deteksi audio tidak diaktifkan untuk kamera ini. Transkripsi audio memerlukan deteksi audio yang aktif." }, "detect": { - "fpsGreaterThanFive": "Pengaturan FPS untuk pendeteksian lebih dari 5 tidak disarankan." + "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": "Pendeteksi muka tidak dinyalakan dalam level global. Nyalakan pendeteksi muka dalam pengaturan global agar per-kamera deteksi muka dapat bekerja.", - "personNotTracked": "Pendeteksi muka memerlukan 'orang' sebagai objek deteksi. Pastikan 'orang' berada dalam hal yang dideteksi." + "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": "Pendeteksian plat nomor tidak dinyalakan dalam pengaturan global. Nyalakan deteksi plat nomor dalam pengaturan global agar fungsi ini dapat bekerja.", - "vehicleNotTracked": "Pendeteksian plat nomor memerlukan 'mobil' atau 'motor' untuk dideteksi." + "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/it/common.json b/web/public/locales/it/common.json index f571f11a94..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", @@ -332,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 8dc27755dc..3188fea87d 100644 --- a/web/public/locales/it/components/camera.json +++ b/web/public/locales/it/components/camera.json @@ -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/player.json b/web/public/locales/it/components/player.json index e8a1f5bbbf..0937860483 100644 --- a/web/public/locales/it/components/player.json +++ b/web/public/locales/it/components/player.json @@ -48,5 +48,6 @@ "submitFrigatePlusFailed": "Impossibile inviare il fotogramma a Frigate+" } }, - "cameraDisabled": "La telecamera è disabilita" + "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 ebf816b539..e359290754 100644 --- a/web/public/locales/it/config/cameras.json +++ b/web/public/locales/it/config/cameras.json @@ -5,8 +5,8 @@ "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": "Abilitata", @@ -33,7 +33,11 @@ }, "filters": { "label": "Filtri audio", - "description": "Impostazioni di filtro per ciascun tipo di audio, come le soglie di confidenza utilizzate per ridurre i falsi positivi." + "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", @@ -46,7 +50,8 @@ }, "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": { @@ -57,10 +62,58 @@ "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." + "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": { @@ -129,43 +182,451 @@ } }, "detect": { - "label": "Rilevamento oggetti" + "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" + "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": "Revisiona" + "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" + "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." + "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 videocamera.", + "enabled": { + "label": "Abilita Registrazione", + "description": "Attiva o disattiva la registrazione per questa telecamera." + }, + "expire_interval": { + "label": "Intervallo 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 di pre-acquisizione", + "description": "Numero di secondi precedenti l'evento di rilevamento da includere nella registrazione." + }, + "post_capture": { + "label": "Secondi di post-acquisizione", + "description": "Numero di secondi successivi l'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 di pre e post-acquisizione.", + "pre_capture": { + "label": "Secondi di pre-acquisizione", + "description": "Numero di secondi antecedenti all'evento di rilevamento da includere nella registrazione." + }, + "post_capture": { + "label": "Secondi di post-acquisizione", + "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" + "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" + } }, "motion": { "label": "Rilevamento movimento", "contour_area": { - "label": "Area di contorno" + "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" + "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." + }, + "frame_alpha": { + "description": "Valore alfa utilizzato durante la fusione dei fotogrammi per la preelaborazione del movimento." + }, + "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." + }, + "raw_mask": { + "label": "Maschera grezza" } }, "objects": { - "label": "Oggetti" + "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 del rilevamento per singolo fotogramma richiesto affinché l'oggetto venga conteggiato." + }, + "mask": { + "label": "Maschera filtro", + "description": "Coordinate del poligono che definisce l'area all'interno dell'inquadratura in cui si applica questo filtro." + }, + "raw_mask": { + "label": "Maschera grezza" + } + }, + "mask": { + "label": "Maschera oggetto", + "description": "Poligono di maschera utilizzato per impedire il rilevamento di oggetti in aree specificate." + }, + "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 di default la generazione tramite GenAI delle descrizioni degli oggetti monitorati." + }, + "use_snapshot": { + "label": "Usa istantanee", + "description": "Utilizza le istantanee degli oggetti anziché le miniature per la generazione di descrizioni con GenAI." + }, + "prompt": { + "description": "Modello di prompt predefinito utilizzato per la generazione di descrizioni con GenAI." + }, + "object_prompts": { + "description": "Prompt specifici per ogni oggetto per personalizzare i risultati di GenAI in base a etichette specifiche." + }, + "objects": { + "label": "Oggetti GenAI", + "description": "Elenco delle etichette degli oggetti da inviare a GenAI per impostazione predefinita." + }, + "required_zones": { + "label": "Zone obbligatorie", + "description": "Zone che devono essere inserite affinché gli oggetti possano essere utilizzati per la generazione di descrizioni tramite GenAI." + }, + "debug_save_thumbnails": { + "label": "Salva miniature", + "description": "Salva le miniature inviate a GenAI per il debug e la revisione." + }, + "send_triggers": { + "label": "Attivatori GenAI", + "description": "Definisce quando i frame devono essere inviati a GenAI (al termine, 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" + "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" @@ -189,16 +650,72 @@ } }, "birdseye": { - "label": "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": "Inneschi" - } + "label": "Attivatori", + "friendly_name": { + "label": "Nome descrittivo", + "description": "Nome descrittivo opzionale visualizzato nell'interfaccia utente per questo innesco." + }, + "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" + "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.", @@ -215,6 +732,19 @@ "enabled": { "label": "Abilitata" }, - "label": "Zone" + "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" + } + } + }, + "type": { + "description": "Tipo di telecamera", + "label": "Tipo di telecamera" } } diff --git a/web/public/locales/it/config/global.json b/web/public/locales/it/config/global.json index 5bb38cf431..e3045ddc56 100644 --- a/web/public/locales/it/config/global.json +++ b/web/public/locales/it/config/global.json @@ -30,7 +30,11 @@ }, "filters": { "label": "Filtri audio", - "description": "Impostazioni di filtro per ciascun tipo di audio, come le soglie di confidenza utilizzate per ridurre i falsi positivi." + "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", @@ -85,16 +89,22 @@ "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" + "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" } }, "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": { @@ -105,10 +115,58 @@ "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." + "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": { @@ -254,12 +312,76 @@ } }, "detect": { - "label": "Rilevamento oggetti" + "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": { @@ -295,41 +417,382 @@ } }, "review": { - "label": "Revisiona" + "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." }, "profiles": { - "label": "Profili" + "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." + "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" + }, + "expire_interval": { + "label": "Intervallo 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 di pre-acquisizione", + "description": "Numero di secondi precedenti l'evento di rilevamento da includere nella registrazione." + }, + "post_capture": { + "label": "Secondi di post-acquisizione", + "description": "Numero di secondi successivi l'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 di pre e post-acquisizione.", + "pre_capture": { + "label": "Secondi di pre-acquisizione", + "description": "Numero di secondi antecedenti all'evento di rilevamento da includere nella registrazione." + }, + "post_capture": { + "label": "Secondi di post-acquisizione", + "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" + "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" + } }, "motion": { "label": "Rilevamento movimento", "contour_area": { - "label": "Area di contorno" + "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" + "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." + }, + "frame_alpha": { + "description": "Valore alfa utilizzato durante la fusione dei fotogrammi per la preelaborazione del movimento." + }, + "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." + }, + "raw_mask": { + "label": "Maschera grezza" } }, "objects": { - "label": "Oggetti" + "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 del rilevamento per singolo fotogramma richiesto affinché l'oggetto venga conteggiato." + }, + "mask": { + "label": "Maschera filtro", + "description": "Coordinate del poligono che definisce l'area all'interno dell'inquadratura in cui si applica questo filtro." + }, + "raw_mask": { + "label": "Maschera grezza" + } + }, + "mask": { + "label": "Maschera oggetto", + "description": "Poligono di maschera utilizzato per impedire il rilevamento di oggetti in aree specificate." + }, + "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 di default la generazione tramite GenAI delle descrizioni degli oggetti monitorati." + }, + "use_snapshot": { + "label": "Usa istantanee", + "description": "Utilizza le istantanee degli oggetti anziché le miniature per la generazione di descrizioni con GenAI." + }, + "prompt": { + "description": "Modello di prompt predefinito utilizzato per la generazione di descrizioni con GenAI." + }, + "object_prompts": { + "description": "Prompt specifici per ogni oggetto per personalizzare i risultati di GenAI in base a etichette specifiche." + }, + "objects": { + "label": "Oggetti GenAI", + "description": "Elenco delle etichette degli oggetti da inviare a GenAI per impostazione predefinita." + }, + "required_zones": { + "label": "Zone obbligatorie", + "description": "Zone che devono essere inserite affinché gli oggetti possano essere utilizzati per la generazione di descrizioni tramite GenAI." + }, + "debug_save_thumbnails": { + "label": "Salva miniature", + "description": "Salva le miniature inviate a GenAI per il debug e la revisione." + }, + "send_triggers": { + "label": "Attivatori GenAI", + "description": "Definisce quando i frame devono essere inviati a GenAI (al termine, 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" + } + } }, "live": { - "label": "Riproduzione in diretta" + "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" @@ -351,7 +814,7 @@ }, "notifications": { "label": "Notifiche", - "description": "Impostazioni per abilitare e controllare le notifiche per tutte le telecamere; possono essere modificate per ogni singola telecamera.", + "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." @@ -400,7 +863,20 @@ "label": "Telemetria" }, "birdseye": { - "label": "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" @@ -408,7 +884,32 @@ "semantic_search": { "label": "Ricerca semantica", "triggers": { - "label": "Inneschi" + "label": "Attivatori", + "friendly_name": { + "label": "Nome descrittivo", + "description": "Nome descrittivo opzionale visualizzato nell'interfaccia utente per questo innesco." + }, + "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" @@ -418,6 +919,22 @@ "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": { @@ -445,5 +962,8 @@ "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 eaba21cb21..c6049d5f0b 100644 --- a/web/public/locales/it/config/validation.json +++ b/web/public/locales/it/config/validation.json @@ -28,5 +28,8 @@ "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 index 67b93ffdd5..dfe5166224 100644 --- a/web/public/locales/it/views/chat.json +++ b/web/public/locales/it/views/chat.json @@ -42,5 +42,31 @@ "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 45289b6452..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": { @@ -21,7 +21,7 @@ "markTheseItemsAsReviewed": "Segna questi elementi come visti", "markAsReviewed": "Segna come visto", "documentTitle": "Revisiona - Frigate", - "allCameras": "Tutte le camere", + "allCameras": "Tutte le telecamere", "timeline": { "label": "Linea temporale" }, 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 466110ad10..697a676820 100644 --- a/web/public/locales/it/views/live.json +++ b/web/public/locales/it/views/live.json @@ -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", @@ -158,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 index 06a80167d9..4421f8bcef 100644 --- a/web/public/locales/it/views/motionSearch.json +++ b/web/public/locales/it/views/motionSearch.json @@ -26,7 +26,9 @@ "points_many": "{{count}} punti", "points_other": "{{count}} punti", "undo": "Annulla ultimo punto", - "reset": "Reimposta poligono" + "reset": "Reimposta poligono", + "drawMode": "Disegna", + "moveMode": "Sposta" }, "motionHeatmapLabel": "Mappa di calore del movimento", "dialog": { @@ -42,11 +44,11 @@ "settings": { "title": "Impostazioni di ricerca", "parallelMode": "Modalità parallela", - "parallelModeDesc": "Scansiona più segmenti di registrazione contemporaneamente (più veloce, ma richiede un utilizzo della CPU significativamente maggiore)", + "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": "Percentuale minima della regione di interesse che deve cambiare per essere considerata significativa", + "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", @@ -72,6 +74,9 @@ "framesDecoded": "Fotogrammi decodificati", "wallTime": "Tempo di ricerca", "segmentErrors": "Errori di segmento", - "seconds": "{{seconds}}s" - } + "seconds": "{{seconds}}s", + "minutesSeconds": "{{minutes}}m {{seconds}}s", + "scanSummary": "{{segments}} segmenti · {{time}}" + }, + "scanning": "Scansione {{time}}" } diff --git a/web/public/locales/it/views/settings.json b/web/public/locales/it/views/settings.json index 2b4aea94e8..5c40cc9601 100644 --- a/web/public/locales/it/views/settings.json +++ b/web/public/locales/it/views/settings.json @@ -16,7 +16,8 @@ "globalConfig": "Configurazione globale - Frigate", "cameraConfig": "Configurazione telecamera - Frigate", "maintenance": "Manutenzione - Frigate", - "profiles": "Profili - Frigate" + "profiles": "Profili - Frigate", + "detectorsAndModel": "Rilevatori e modelli - Frigate" }, "frigatePlus": { "snapshotConfig": { @@ -45,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}}", @@ -67,7 +76,8 @@ "currentModel": "Modello attuale", "otherModels": "Altri modelli", "configuration": "Configurazione" - } + }, + "changeInDetectorsAndModel": "Cambia modello" }, "debug": { "timestamp": { @@ -161,7 +171,7 @@ "defaultName": "Maschera di movimento {{number}}", "name": { "title": "Nome", - "description": "Un nome amichevole opzionale per questa maschera di movimento.", + "description": "Un nome descrittivo opzionale per questa maschera di movimento.", "placeholder": "Inserisci un nome..." } }, @@ -203,6 +213,10 @@ "zone": "zona", "motion_mask": "maschera di movimento", "object_mask": "maschera di 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": { @@ -219,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": { @@ -318,7 +343,7 @@ "name": { "title": "Nome", "placeholder": "Inserisci un nome...", - "description": "Un nome amichevole facoltativo per questa maschera oggetto." + "description": "Un nome descrittivo facoltativo per questa maschera oggetto." } }, "restart_required": "Riavvio richiesto (maschere/zone modificate)", @@ -329,7 +354,11 @@ "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", @@ -420,7 +449,7 @@ "enrichments": "Miglioramenti", "triggers": "Inneschi", "roles": "Ruoli", - "cameraManagement": "Gestione", + "cameraManagement": "Gestione della telecamera", "cameraReview": "Revisiona", "profiles": "Profili", "general": "Generale", @@ -477,7 +506,11 @@ "cameraOnvif": "ONVIF", "cameraTimestampStyle": "Stile orario", "cameraUi": "Interfaccia utente telecamera", - "mediaSync": "Sincronizzazione multimediale" + "mediaSync": "Sincronizzazione multimediale", + "cameraMqtt": "MQTT telecamera", + "maintenance": "Manutenzione", + "regionGrid": "Griglia di regioni", + "systemDetectorsAndModel": "Rilevatori e modelli" }, "users": { "dialog": { @@ -777,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": { @@ -1224,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" @@ -1359,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." } } }, @@ -1370,19 +1405,45 @@ "selectCamera": "Seleziona una telecamera", "backToSettings": "Torna alle impostazioni della telecamera", "streams": { - "title": "Abilita/Disabilita telecamere", + "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.", + "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 nella configurazione. Riavvia Frigate per applicare le modifiche.", + "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", @@ -1414,7 +1475,123 @@ "addGo2rtcStream": "Aggiungi flusso go2rtc" }, "profiles": { - "enabled": "Abilitato" + "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": { @@ -1436,7 +1613,15 @@ "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", @@ -1447,8 +1632,48 @@ "videoCopy": "Copia", "hardware": "Accelerazione hardware", "hardwareNone": "Nessuna accelerazione hardware", - "hardwareAuto": "Accelerazione hardware automatica" - } + "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": { @@ -1470,7 +1695,14 @@ "face_recognition": "Riconoscimento facciale", "masksAndZones": "Maschere / Zone", "audio": "Audio", - "model": "Modello" + "model": "Modello", + "detect": "Rilevamento", + "motion": "Movimento", + "live": "Vista dal vivo", + "timestamp_style": "Orari", + "go2rtc": "go2rtc", + "detectors": "Rivelatori", + "genai": "GenAI" }, "tabs": { "system": "Sistema", @@ -1479,12 +1711,19 @@ }, "inputRoles": { "options": { - "audio": "Audio" - } + "audio": "Audio", + "detect": "Rileva", + "record": "Registra" + }, + "summary": "{{count}} ruoli selezionati", + "empty": "Nessun ruolo disponibile" }, "roleMap": { "roleLabel": "Ruolo", - "remove": "Rimuovi" + "remove": "Rimuovi", + "empty": "Nessuna mappatura dei ruoli", + "groupsLabel": "Gruppi", + "addMapping": "Aggiungi la mappatura dei ruoli" }, "notifications": { "title": "Impostazioni di notifica" @@ -1518,19 +1757,209 @@ "presetLabels": { "preset-rpi-64-h264": "Raspberry Pi (H.264)", "preset-rpi-64-h265": "Raspberry Pi (H.265)", - "preset-vaapi": "VAAPI (GPU Intel/AMD)" + "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" + "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" + "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" + "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", @@ -1564,14 +1993,37 @@ "errorLabel": "Errore", "resultsFields": { "error": "Errore", - "totals": "Totali" + "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" + "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", @@ -1583,5 +2035,207 @@ "clearError": "Impossibile pulire la griglia di regioni", "restartRequired": "È necessario riavviare il sistema affinché le modifiche alla griglia di 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 ca6a0ab9ce..ed780a51e9 100644 --- a/web/public/locales/it/views/system.json +++ b/web/public/locales/it/views/system.json @@ -175,7 +175,7 @@ "framesAndDetections": "Fotogrammi / Rilevamenti", "label": { "camera": "telecamera", - "detect": "rilevamento", + "detect": "rileva", "skipped": "saltati", "ffmpeg": "FFmpeg", "capture": "cattura", 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 ffceee4199..4b538f7faa 100644 --- a/web/public/locales/ja/common.json +++ b/web/public/locales/ja/common.json @@ -136,11 +136,23 @@ "export": "エクスポート", "deleteNow": "今すぐ削除", "next": "次へ", - "continue": "続行" + "continue": "続行", + "add": "追加", + "applying": "適用中…", + "undo": "元に戻す", + "copiedToClipboard": "クリップボードにコピーしました", + "modified": "変更あり", + "overridden": "上書き済み", + "resetToGlobal": "グローバル設定にリセット", + "resetToDefault": "デフォルトにリセット", + "saveAll": "すべて保存", + "savingAll": "すべて保存中…", + "undoAll": "すべて元に戻す", + "retry": "再試行" }, "menu": { "system": "システム", - "systemMetrics": "システムモニター", + "systemMetrics": "システムメトリクス", "configuration": "設定", "systemLogs": "システムログ", "settings": "設定", @@ -235,10 +247,15 @@ "withSystem": { "label": "システム設定に従う" }, - "hr": "Hrvatski (クロアチア語)" + "hr": "Hrvatski (クロアチア語)", + "bs": "Bosanski (ボスニア語)", + "zhHant": "繁體中文 (繁体字中国語)" }, "classification": "分類", - "profiles": "プロファイル" + "profiles": "プロファイル", + "actions": "操作", + "features": "機能", + "chat": "チャット" }, "toast": { "copyUrlToClipboard": "URLをクリップボードにコピーしました。", @@ -247,7 +264,8 @@ "error": { "title": "設定変更の保存に失敗しました: {{errorMessage}}", "noMessage": "設定変更の保存に失敗しました" - } + }, + "success": "設定変更を保存しました。" } }, "role": { @@ -290,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 9364141401..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": { @@ -62,12 +62,16 @@ "queued": "エクスポートがキューに追加されました。進捗状況はエクスポートページで確認できます。", "batchQueuedSuccess_other": "{{count}} 件のエクスポートがキューに登録されました。現在ケースをオープンしています。", "batchQueuedPartial": "{{total}} 件中 {{successful}} 件のエクスポートがキューに追加されました。失敗したカメラ: {{failedCameras}}", - "batchQueueFailed": "{{total}} 件のエクスポートをキューに追加できませんでした。失敗したカメラ: {{failedCameras}}" + "batchQueueFailed": "{{total}} 件のエクスポートをキューに追加できませんでした。失敗したカメラ: {{failedCameras}}", + "batchSuccess_other": "{{count}} 件のエクスポートを開始しました。ケースを開きます。", + "batchPartial": "{{total}} 件中 {{successful}} 件のエクスポートを開始しました。失敗したカメラ: {{failedCameras}}", + "batchFailed": "{{total}} 件のエクスポートを開始できませんでした。失敗したカメラ: {{failedCameras}}" }, "fromTimeline": { "saveExport": "エクスポートを保存", "previewExport": "エクスポートをプレビュー", - "queueingExport": "エクスポートをキューイングしています..." + "queueingExport": "エクスポートをキューイングしています...", + "useThisRange": "この範囲を使用" }, "queueing": "エクスポートをキューイングしています...", "multiCamera": { @@ -84,7 +88,7 @@ "exportButton_other": "{{count}} 台のカメラをエクスポート" }, "case": { - "newCaseOption": "新しいケースを作成する", + "newCaseOption": "新しいケースを作成", "newCaseNamePlaceholder": "新しいケース名", "newCaseDescriptionPlaceholder": "ケースの説明", "label": "ケース", @@ -96,7 +100,18 @@ "multiCamera": "マルチカメラ" }, "multi": { - "title_other": "{{count}} 件のレビューをエクスポート" + "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": { @@ -143,6 +158,14 @@ "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 e5bc120e74..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 0fa36434d7..692cde4218 100644 --- a/web/public/locales/ja/components/player.json +++ b/web/public/locales/ja/components/player.json @@ -20,7 +20,7 @@ }, "bandwidth": { "title": "帯域:", - "short": "帯域" + "short": "帯域幅" }, "latency": { "title": "遅延:", @@ -48,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 24c0782779..aa6ca74e74 100644 --- a/web/public/locales/ja/config/cameras.json +++ b/web/public/locales/ja/config/cameras.json @@ -9,35 +9,39 @@ "description": "有効" }, "audio": { - "label": "音声検出", + "label": "音声検知", "enabled": { "label": "音声検知を有効化", - "description": "このカメラのオーディオイベント検出を有効または無効にします。" + "description": "このカメラの音声イベント検知を有効または無効にします。" }, "min_volume": { "label": "最小ボリューム", - "description": "オーディオ検出を実行するために必要な最小RMS音量閾値。値を小さくすると感度が高くなります(例:200=高、500=中、1000=低)。" + "description": "音声検知を実行するために必要な最小RMS音量閾値。値を小さくすると感度が高くなります(例:200=高、500=中、1000=低)。" }, "filters": { "label": "音声フィルタ", - "description": "誤検出を減らすために使用される信頼度閾値などのフィルタ設定(オーディオタイプごと)。" + "description": "誤検知を減らすために使用される信頼度閾値などのフィルタ設定(オーディオタイプごと)。", + "threshold": { + "label": "音声の最低信頼度", + "description": "音声イベントとしてカウントするために必要な最低信頼度しきい値。" + } }, - "description": "このカメラの音声ベースのイベント検出設定。", + "description": "このカメラの音声ベースのイベント検知設定。", "max_not_heard": { "label": "タイムアウト終了", - "description": "オーディオイベントが終了するまでの残り秒数(設定されたオーディオタイプを除く)。" + "description": "音声検知が終了するまでの残り秒数(設定されたオーディオタイプを除く)。" }, "listen": { "label": "リスニングタイプ", - "description": "検出対象の音声イベントの種類一覧(例:吠え声、火災報知器、悲鳴、会話、叫び声)。" + "description": "検知対象の音声イベントの種類一覧(例:吠え声、火災報知器、悲鳴、会話、叫び声)。" }, "enabled_in_config": { "label": "元の音声状態", - "description": "静的設定ファイルで、音声検出が当初有効にされていたかどうかを示します。" + "description": "静的設定ファイルで、音声検知が当初有効にされていたかどうかを示します。" }, "num_threads": { - "label": "検出スレッド", - "description": "音声検出処理に使用するスレッド数。" + "label": "検知スレッド", + "description": "音声検知処理に使用するスレッド数。" } }, "friendly_name": { @@ -76,21 +80,874 @@ } }, "detect": { - "label": "物体検出", - "description": "物体検出の実行やトラッカーの初期化に使用される、検出や検出ロールの設定。", + "label": "物体検知", + "description": "物体検知の実行やトラッカーの初期化に使用される、検知や検知ロールの設定。", "enabled": { "label": "物体検知を有効にする", "description": "このカメラの物体検知機能を有効または無効にします。" }, "height": { - "label": "高さを検出", - "description": "検出ストリームに使用するフレーム高さ(ピクセル)。ネイティブストリーム解像度を使用する場合は、空欄のままにしてください。" + "label": "高さを検知", + "description": "検知ストリームに使用するフレーム高さ(ピクセル)。ネイティブストリーム解像度を使用する場合は、空欄のままにしてください。" }, "width": { - "label": "幅を検出" + "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" + "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 f563742e18..3ccc593271 100644 --- a/web/public/locales/ja/config/global.json +++ b/web/public/locales/ja/config/global.json @@ -8,34 +8,40 @@ "description": "Home Assistant OS の Frigate プロセスに設定する環境変数のキー/値ペア。HAOS をご利用でない場合は、代わりに Docker の環境変数設定を使用してください。" }, "audio": { - "label": "音声検出", + "label": "音声検知", "enabled": { - "label": "音声検知を有効化" + "label": "音声検知を有効化", + "description": "全カメラの音声イベント検知を有効または無効にします(カメラ別に上書き可能)。" }, "min_volume": { "label": "最小ボリューム", - "description": "オーディオ検出を実行するために必要な最小RMS音量閾値。値を小さくすると感度が高くなります(例:200=高、500=中、1000=低)。" + "description": "音声検知を実行するために必要な最小RMS音量閾値。値を小さくすると感度が高くなります(例:200=高、500=中、1000=低)。" }, "filters": { "label": "音声フィルタ", - "description": "誤検出を減らすために使用される信頼度閾値などのフィルタ設定(オーディオタイプごと)。" + "description": "誤検知を減らすために使用される信頼度閾値などのフィルタ設定(オーディオタイプごと)。", + "threshold": { + "label": "音声の最低信頼度", + "description": "音声イベントとしてカウントするために必要な最低信頼度しきい値。" + } }, "max_not_heard": { "label": "タイムアウト終了", - "description": "オーディオイベントが終了するまでの残り秒数(設定されたオーディオタイプを除く)。" + "description": "音声検知が終了するまでの残り秒数(設定されたオーディオタイプを除く)。" }, "listen": { "label": "リスニングタイプ", - "description": "検出対象の音声イベントの種類一覧(例:吠え声、火災報知器、悲鳴、会話、叫び声)。" + "description": "検知対象の音声イベントの種類一覧(例:吠え声、火災報知器、悲鳴、会話、叫び声)。" }, "enabled_in_config": { "label": "元の音声状態", - "description": "静的設定ファイルで、音声検出が当初有効にされていたかどうかを示します。" + "description": "静的設定ファイルで、音声検知が当初有効にされていたかどうかを示します。" }, "num_threads": { - "label": "検出スレッド", - "description": "音声検出処理に使用するスレッド数。" - } + "label": "検知スレッド", + "description": "音声検知処理に使用するスレッド数。" + }, + "description": "全カメラの音声ベースのイベント検知設定(カメラ別に上書き可能)。" }, "logger": { "default": { @@ -46,7 +52,7 @@ "label": "プロセス毎のログレベル", "description": "コンポーネントごとのログレベルの上書きにより、特定のモジュールのログ詳細度を増減できます。" }, - "label": "ログ記録", + "label": "ロギング", "description": "デフォルトのログ詳細度とコンポーネントごとのログレベルの上書きを制御します。" }, "auth": { @@ -99,7 +105,7 @@ }, "version": { "label": "現在の設定バージョン", - "description": "移行やフォーマット変更の検出に役立つ、アクティブな設定の数値または文字列バージョン。" + "description": "移行やフォーマット変更の検知に役立つ、アクティブな設定の数値または文字列バージョン。" }, "audio_transcription": { "label": "音声文字起こし", @@ -109,7 +115,20 @@ "description": "音声を受信した時点で、リアルタイム文字起こしを有効にします。" }, "enabled": { - "label": "音声文字起こしを有効にする" + "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": { @@ -126,6 +145,42 @@ "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": { @@ -137,17 +192,63 @@ } }, "detect": { - "label": "物体検出", - "description": "物体検出の実行やトラッカーの初期化に使用される、検出や検出ロールの設定。", + "label": "物体検知", + "description": "物体検知の実行やトラッカーの初期化に使用される、検知や検知ロールの設定。", "enabled": { - "label": "物体検知を有効にする" + "label": "物体検知を有効にする", + "description": "全カメラの物体検知を有効または無効にします(カメラ別に上書き可能)。" }, "height": { - "label": "高さを検出", - "description": "検出ストリームに使用するフレーム高さ(ピクセル)。ネイティブストリーム解像度を使用する場合は、空欄のままにしてください。" + "label": "高さを検知", + "description": "検知ストリームに使用するフレーム高さ(ピクセル)。ネイティブストリーム解像度を使用する場合は、空欄のままにしてください。" }, "width": { - "label": "幅を検出" + "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": { @@ -156,12 +257,1379 @@ }, "mqtt": { "label": "MQTT", - "description": "テレメトリー、スナップショット、およびイベントの詳細をMQTTブローカーに接続して公開するための設定。", + "description": "テレメトリ、スナップショット、およびイベントの詳細をMQTTブローカーに接続して公開するための設定。", "enabled": { - "label": "MQTTを有効にする" + "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": "テレメトリー" + "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 b09db04cd5..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": "検知", diff --git a/web/public/locales/ja/config/validation.json b/web/public/locales/ja/config/validation.json index 03073d0763..9f862a31ed 100644 --- a/web/public/locales/ja/config/validation.json +++ b/web/public/locales/ja/config/validation.json @@ -28,5 +28,8 @@ "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 index be73c63f59..4215a317c7 100644 --- a/web/public/locales/ja/views/chat.json +++ b/web/public/locales/ja/views/chat.json @@ -32,6 +32,41 @@ "send": "送信", "suggested_requests": "質問してみてください:", "starting_requests": { - "show_recent_events": "最近のイベントを表示" + "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 ccd1c2c07f..91ff04c3bb 100644 --- a/web/public/locales/ja/views/classificationModel.json +++ b/web/public/locales/ja/views/classificationModel.json @@ -12,7 +12,7 @@ }, "toast": { "success": { - "deletedImage_other": "{{count}} 件の削除された画像", + "deletedImage_other": "{{count}} 枚の画像を削除しました", "categorizedImage": "画像の分類に成功しました", "trainedModel": "モデルを正常に学習させました。", "trainingModel": "モデルのトレーニングを正常に開始しました。", @@ -35,15 +35,15 @@ } }, "train": { - "titleShort": "Classifications,最近の分類結果を選択,,False,train.aria,,", + "titleShort": "最近の分類", "title": "最近の分類結果", "aria": "最近の分類結果を選択" }, "wizard": { "step1": { - "typeObject": "Classification", - "typeState": "Classification", - "description": "状態モデルは固定カメラ領域の状態変化(例:ドアの開閉)を監視し、オブジェクトモデルは検出されたオブジェクトに分類(例:既知の動物や配達員など)を追加します。", + "typeObject": "オブジェクト", + "typeState": "状態", + "description": "状態モデルは固定カメラ領域の状態変化(例:ドアの開閉)を監視し、オブジェクトモデルは検知されたオブジェクトに分類(例:既知の動物や配達員など)を追加します。", "name": "名前", "namePlaceholder": "モデル名を入力...", "type": "タイプ", @@ -58,7 +58,7 @@ "states": "状態", "classesTip": "クラスについて", "classesStateDesc": "カメラ領域の状態を定義します。例: ガレージドアの「開」「閉」。", - "classesObjectDesc": "検出されたオブジェクトを分類するための、異なるカテゴリを定義します。例:人物の分類として「delivery_person」「resident」「stranger」など。", + "classesObjectDesc": "検知されたオブジェクトを分類するための、異なるカテゴリを定義します。例:人物の分類として「delivery_person」「resident」「stranger」など。", "classPlaceholder": "クラス名を入力...", "errors": { "nameRequired": "モデル名は必須です", @@ -113,11 +113,16 @@ "missingStatesWarning": { "title": "状態の例が不足しています", "description": "最良の結果を得るため、すべての状態の例を選択することを推奨します。すべてを選択しなくても続行できますが、全状態に画像が揃うまでモデルは学習されません。続行後、「最近の分類」から不足分を分類し、学習を行ってください。" + }, + "refreshExamples": "新しい例を生成", + "refreshConfirm": { + "title": "新しい例を生成しますか?", + "description": "この操作により、新しい画像セットが生成され、選択済みのものも含めすべての選択がクリアされます。すべてのクラスについて、再度例を選び直す必要があります。" } } }, "details": { - "scoreInfo": "このスコアは、このオブジェクトに対するすべての検出結果の分類信頼度の平均を表します。", + "scoreInfo": "このスコアは、このオブジェクトに対するすべての検知結果の分類信頼度の平均を表します。", "none": "なし", "unknown": "不明" }, @@ -172,7 +177,7 @@ "noModels": { "object": { "title": "オブジェクト分類モデルがありません", - "description": "検出されたオブジェクトを分類するためのカスタムモデルを作成します。", + "description": "検知されたオブジェクトを分類するためのカスタムモデルを作成します。", "buttonText": "オブジェクトモデルを作成" }, "state": { @@ -180,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 6e9273cefa..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": { @@ -42,7 +42,7 @@ }, "selected_one": "{{count}} 選択済み", "selected_other": "{{count}} 選択済み", - "detected": "検出", + "detected": "検知", "suspiciousActivity": "不審なアクティビティ", "threateningActivity": "脅威となるアクティビティ", "zoomIn": "ズームイン", @@ -71,5 +71,24 @@ "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 2789e800f5..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": "この追跡オブジェクトを削除すると、スナップショット、保存された埋め込み、および関連する追跡詳細項目が削除されます。履歴ビューの録画映像は削除されません。

    続行してもよろしいですか?" + }, + "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 767c05a11a..5cff546459 100644 --- a/web/public/locales/ja/views/exports.json +++ b/web/public/locales/ja/views/exports.json @@ -69,13 +69,14 @@ "noDescription": "説明がありません", "exportCount_one": "1 件のエクスポート", "exportCount_other": "{{count}} エクスポート", - "cameraCount_other": "{{count}} カメラ", + "cameraCount_other": "{{count}} 台のカメラ", "showMore": "さらに表示", "showLess": "表示を減らす", "emptyTitle": "このケースは空です", "emptyDescription": "既存の分類されていないエクスポートを追加して、ケースを整理しましょう。", "emptyDescriptionNoExports": "まだ追加可能な未分類のエクスポートはありません。", - "createdAt": "作成日 {{value}}" + "createdAt": "作成日 {{value}}", + "cameraCount_one": "1 台のカメラ" }, "caseEditor": { "createTitle": "ケースを作成", diff --git a/web/public/locales/ja/views/faceLibrary.json b/web/public/locales/ja/views/faceLibrary.json index 9446398abf..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,8 @@ "deleteNameFailed": "名前の削除に失敗しました: {{errorMessage}}", "renameFaceFailed": "顔の名前変更に失敗しました: {{errorMessage}}", "trainFailed": "学習に失敗しました: {{errorMessage}}", - "updateFaceScoreFailed": "顔スコアの更新に失敗しました: {{errorMessage}}" + "updateFaceScoreFailed": "顔スコアの更新に失敗しました: {{errorMessage}}", + "reclassifyFailed": "顔の再分類に失敗しました: {{errorMessage}}" } }, "reclassifyFaceAs": "顔を再分類する:", diff --git a/web/public/locales/ja/views/live.json b/web/public/locales/ja/views/live.json index 8fde1adb18..0fe4e7b99f 100644 --- a/web/public/locales/ja/views/live.json +++ b/web/public/locales/ja/views/live.json @@ -58,15 +58,17 @@ }, "camera": { "enable": "カメラを有効化", - "disable": "カメラを無効化" + "disable": "カメラを無効化", + "turnOn": "カメラをオンにする", + "turnOff": "カメラをオフにする" }, "muteCameras": { "enable": "全カメラをミュート", "disable": "全カメラのミュートを解除" }, "detect": { - "enable": "検出を有効化", - "disable": "検出を無効化" + "enable": "検知を有効化", + "disable": "検知を無効化" }, "recording": { "enable": "録画を有効化", @@ -78,8 +80,8 @@ "disable": "スナップショットを無効化" }, "audioDetect": { - "enable": "音声検出を有効化", - "disable": "音声検出を無効化" + "enable": "音声検知を有効化", + "disable": "音声検知を無効化" }, "transcription": { "enable": "ライブ音声文字起こしを有効化", @@ -142,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 index 6e0d6b4b64..cc648822e2 100644 --- a/web/public/locales/ja/views/motionSearch.json +++ b/web/public/locales/ja/views/motionSearch.json @@ -15,7 +15,7 @@ "searching": "検索中です。", "searchComplete": "検索完了", "noResultsYet": "選択した領域内の動きの変化を検索します", - "noChangesFound": "選択した領域でピクセルの変化は検出されませんでした", + "noChangesFound": "選択した領域でピクセルの変化は検知されませんでした", "changesFound_other": "{{count}} 件の動きの変化が見つかりました", "framesProcessed": "{{count}} フレームを処理しました", "jumpToTime": "この時間に移動", @@ -27,7 +27,9 @@ "polygonControls": { "points_other": "{{count}} ポイント", "undo": "直前のポイントを元に戻す", - "reset": "ポリゴンをリセット" + "reset": "ポリゴンをリセット", + "drawMode": "描画", + "moveMode": "移動" }, "motionHeatmapLabel": "モーションヒートマップ", "timeRange": { @@ -37,6 +39,40 @@ }, "settings": { "title": "検索設定", - "parallelMode": "並列モード" - } + "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 index d3c3a6a844..b975486100 100644 --- a/web/public/locales/ja/views/replay.json +++ b/web/public/locales/ja/views/replay.json @@ -1,12 +1,12 @@ { "title": "デバッグリプレイ", - "description": "デバッグ用にカメラの録画をリプレイします。オブジェクトリストには検出されたオブジェクトの遅延サマリーが表示され、「メッセージ」タブにはリプレイ映像からのFrigate内部メッセージのストリームが表示されます。", + "description": "デバッグ用にカメラの録画をリプレイします。オブジェクトリストには検知されたオブジェクトの遅延サマリーが表示され、「メッセージ」タブにはリプレイ映像からのFrigate内部メッセージのストリームが表示されます。", "websocket_messages": "メッセージ", "dialog": { "title": "デバッグリプレイを開始", - "description": "オブジェクトの検出やトラッキングの問題をデバッグするために、過去の映像をループ再生する一時的なリプレイカメラを作成します。このリプレイカメラは、ソースカメラ(元カメラ)と同じ検出設定を引き継ぎます。開始する時間範囲を選択してください。", + "description": "オブジェクトの検知やトラッキングの問題をデバッグするために、過去の映像をループ再生する一時的なリプレイカメラを作成します。このリプレイカメラは、ソースカメラ(元カメラ)と同じ検知設定を引き継ぎます。開始する時間範囲を選択してください。", "camera": "ソースカメラ", - "timeRange": "時間範囲", + "timeRange": "期間", "preset": { "1m": "直近1分間", "5m": "直近5分間", @@ -48,9 +48,9 @@ "cancel": "キャンセル" }, "activity": "アクティビティ", - "objects": "オブジェクトリスト", - "audioDetections": "オーディオ検出", - "noActivity": "アクティビティは検出されませんでした", + "objects": "オブジェクト一覧", + "audioDetections": "音声検知", + "noActivity": "アクティビティは検知されませんでした", "activeTracking": "アクティブトラッキング", "noActiveTracking": "アクティブトラッキングなし", "configuration": "設定", diff --git a/web/public/locales/ja/views/settings.json b/web/public/locales/ja/views/settings.json index db762c8d5d..0222eba9f1 100644 --- a/web/public/locales/ja/views/settings.json +++ b/web/public/locales/ja/views/settings.json @@ -15,13 +15,14 @@ "maintenance": "メンテナンス - Frigate", "profiles": "プロファイル - Frigate", "globalConfig": "グローバル設定 - Frigate", - "cameraConfig": "カメラ設定 - Frigate" + "cameraConfig": "カメラ設定 - Frigate", + "detectorsAndModel": "検出器とモデル - Frigate" }, "menu": { "ui": "UI", "enrichments": "高度解析", "cameras": "カメラ設定", - "masksAndZones": "マスク/ゾーン", + "masksAndZones": "マスク / ゾーン", "motionTuner": "モーションチューナー", "triggers": "トリガー", "debug": "デバッグ", @@ -30,23 +31,23 @@ "frigateplus": "Frigate+", "cameraManagement": "管理", "cameraReview": "レビュー", - "roles": "区分", - "general": "一般", + "roles": "ロール", + "general": "全般", "globalConfig": "グローバル設定", "system": "システム", - "integrations": "統合", + "integrations": "連携", "uiSettings": "UI設定", "profiles": "プロファイル", - "globalDetect": "物体検出", + "globalDetect": "物体検知", "globalRecording": "録画", "globalSnapshots": "スナップショット", "globalFfmpeg": "FFmpeg", - "globalMotion": "動体検出", + "globalMotion": "モーション検知", "globalObjects": "オブジェクト", "globalReview": "レビュー", - "globalAudioEvents": "オーディオイベント", + "globalAudioEvents": "音声検知", "globalLivePlayback": "ライブ再生", - "globalTimestampStyle": "タイムスタンプ形式", + "globalTimestampStyle": "タイムスタンプスタイル", "systemDatabase": "データベース", "systemTls": "TLS", "systemAuthentication": "認証", @@ -55,7 +56,40 @@ "systemUi": "UI", "systemLogging": "ロギング", "systemEnvironmentVariables": "環境変数", - "systemTelemetry": "テレメトリー" + "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": { @@ -85,7 +119,7 @@ }, "liveFallbackTimeout": { "label": "ライブプレイヤーのフォールバック タイムアウト", - "desc": "カメラの高画質ライブストリームが利用できない場合、指定した秒数後に低帯域モードへ切り替えます。デフォルト:3 秒" + "desc": "カメラの高画質ライブストリームが利用できない場合、指定した秒数後に低帯域モードへ切り替えます。デフォルトは 3 秒。" } }, "storedLayouts": { @@ -117,7 +151,7 @@ "toast": { "success": { "clearStoredLayout": "{{cameraName}} の保存済みレイアウトをクリアしました", - "clearStreamingSettings": "すべてのカメラグループのストリーミング設定をクリアしました。" + "clearStreamingSettings": "全カメラグループのストリーミング設定をクリアしました。" }, "error": { "clearStoredLayoutFailed": "保存済みレイアウトのクリアに失敗しました: {{errorMessage}}", @@ -176,7 +210,7 @@ }, "licensePlateRecognition": { "title": "ナンバープレート認識", - "desc": "車両のナンバープレートを認識し、検出文字列を recognized_license_plate フィールドへ、または既知の名称を car タイプのオブジェクトの sub_label として自動追加できます。一般的な用途として、私道に入ってくる車や道路を通過する車のナンバー読み取りがあります。" + "desc": "車両のナンバープレートを認識し、検知文字列を recognized_license_plate フィールドへ、または既知の名称を car タイプのオブジェクトの sub_label として自動追加できます。一般的な用途として、私道に入ってくる車や道路を通過する車のナンバー読み取りがあります。" }, "restart_required": "再起動が必要です(高度解析設定を変更)", "toast": { @@ -322,6 +356,21 @@ "zone": "ゾーン", "motion_mask": "モーションマスク", "object_mask": "オブジェクトマスク" + }, + "revertOverride": { + "title": "ベース設定に戻す", + "desc": "{{type}} {{name}} のプロファイル上書きを削除し、ベース設定に戻します。" + } + }, + "id": { + "error": { + "mustNotBeEmpty": "ID は空にできません。", + "alreadyExists": "この ID のマスクはこのカメラに既に存在します。" + } + }, + "name": { + "error": { + "mustNotBeEmpty": "名前は空にできません。" } } }, @@ -337,7 +386,7 @@ "point_other": "{{count}} 点", "clickDrawPolygon": "画像上をクリックして多角形を描画します。", "name": { - "title": "名称", + "title": "名前", "inputPlaceHolder": "名前を入力…", "tips": "名前は2文字以上で、少なくとも1文字のアルファベットを含み、このカメラ上の他のゾーン名やカメラ名と同一であってはなりません。" }, @@ -374,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": { @@ -423,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": { @@ -451,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" @@ -469,7 +543,7 @@ "desc": "追跡オブジェクトの周囲にバウンディングボックスを表示します", "colors": { "label": "オブジェクトのボックス色", - "info": "
  • 起動時に、各オブジェクトラベルへ異なる色が割り当てられます
  • 細い濃青線は、現在時点では未検出であることを示します
  • 細い灰線は、静止していると検出されたことを示します
  • 太線は、(有効時)オートトラッキングの対象であることを示します
  • " + "info": "
  • 起動時に、各オブジェクトラベルへ異なる色が割り当てられます
  • 細い濃青線は、現在時点では未検知であることを示します
  • 細い灰線は、静止していると検知されたことを示します
  • 太線は、(有効時)オートトラッキングの対象であることを示します
  • " } }, "timestamp": { @@ -486,8 +560,8 @@ }, "motion": { "title": "モーションボックス", - "desc": "モーションが検出された領域のボックスを表示します", - "tips": "

    モーションボックス


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

    " + "desc": "モーションが検知された領域のボックスを表示します", + "tips": "

    モーションボックス


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

    " }, "regions": { "title": "領域", @@ -643,7 +717,7 @@ "createRole": "ロール {{role}} を作成しました", "updateCameras": "ロール {{role}} のカメラを更新しました", "deleteRole": "ロール {{role}} を削除しました", - "userRolesUpdated_other": "このロールに割り当てられていた {{count}} ユーザーは「viewer」に更新され、すべてのカメラへの閲覧アクセスが付与されました。" + "userRolesUpdated_other": "このロールに割り当てられていた {{count}} ユーザーは「viewer」に更新され、全カメラへの閲覧アクセスが付与されました。" }, "error": { "createRoleFailed": "ロールの作成に失敗しました: {{errorMessage}}", @@ -663,7 +737,7 @@ }, "deleteRole": { "title": "ロールを削除", - "desc": "この操作は元に戻せません。ロールは完全に削除され、このロールを持っていたユーザーは「viewer」ロールに再割り当てされ、すべてのカメラへの閲覧アクセスが付与されます。", + "desc": "この操作は元に戻せません。ロールは完全に削除され、このロールを持っていたユーザーは「viewer」ロールに再割り当てされ、全カメラへの閲覧アクセスが付与されます。", "warn": "{{role}} を削除してもよろしいですか?", "deleting": "削除中…" }, @@ -692,7 +766,8 @@ }, "notificationUnavailable": { "title": "通知は利用できません", - "desc": "Web プッシュ通知にはセキュアコンテキスト(https://…)が必要です。これはブラウザの制限です。通知を利用するには、セキュアに Frigate へアクセスしてください。" + "desc": "Web プッシュ通知にはセキュアコンテキスト(https://…)が必要です。これはブラウザの制限です。通知を利用するには、セキュアに Frigate へアクセスしてください。", + "descPwa": "iOSでは、Frigateをホーム画面に追加した場合にのみ、Webプッシュ通知を利用できます。共有メニューを開き、ホーム画面に追加を選択してから、新しいアイコンからFrigateを起動し、このデバイスを通知対象として登録してください。" }, "globalSettings": { "title": "グローバル設定", @@ -748,7 +823,7 @@ }, "snapshotConfig": { "title": "スナップショット設定", - "desc": "Frigate+ への送信には、設定でスナップショットと clean_copy スナップショットの両方を有効にする必要があります。", + "desc": "Frigate+ への送信には、設定でスナップショットを有効にする必要があります。", "cleanCopyWarning": "一部のカメラではスナップショット機能が無効になっています", "table": { "camera": "カメラ", @@ -771,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": "トリガー", @@ -788,7 +879,7 @@ }, "addTrigger": "トリガーを追加", "table": { - "name": "名称", + "name": "名前", "type": "タイプ", "content": "コンテンツ", "threshold": "しきい値", @@ -823,7 +914,7 @@ }, "form": { "name": { - "title": "名称", + "title": "名前", "placeholder": "トリガー名を入力", "error": { "minLength": "この項目は2文字以上で入力してください。", @@ -838,15 +929,15 @@ "type": { "title": "タイプ", "placeholder": "トリガータイプを選択", - "description": "類似した追跡オブジェクトの説明が検出されたときにトリガー", - "thumbnail": "類似した追跡オブジェクトのサムネイルが検出されたときにトリガー" + "description": "類似した追跡オブジェクトの説明が検知されたときにトリガー", + "thumbnail": "類似した追跡オブジェクトのサムネイルが検知されたときにトリガー" }, "content": { "title": "コンテンツ", "imagePlaceholder": "サムネイルを選択", "textPlaceholder": "テキストを入力", "imageDesc": "最新のサムネイル100件のみが表示されます。目的のサムネイルが見つからない場合は、探索で過去のオブジェクトを確認し、そこのメニューからトリガーを設定してください。", - "textDesc": "類似する追跡オブジェクトの説明が検出されたときにこのアクションをトリガーするためのテキストを入力します。", + "textDesc": "類似する追跡オブジェクトの説明が検知されたときにこのアクションをトリガーするためのテキストを入力します。", "error": { "required": "コンテンツは必須です。" } @@ -860,7 +951,7 @@ "desc": "このトリガーの類似度しきい値を設定します。値が高いほど、より近い一致が必要になります。" }, "actions": { - "title": "アクション", + "title": "操作", "desc": "デフォルトでは、Frigate はすべてのトリガーに対して MQTT メッセージを送信します。サブラベルは、トリガー名をオブジェクトのラベルに追加します。属性(Attributes)は、追跡オブジェクトのメタデータとは別に保存される検索可能なメタデータです。", "error": { "min": "少なくとも1つのアクションを選択してください。" @@ -961,7 +1052,7 @@ "quality": "品質", "selectQuality": "品質を選択", "roleLabels": { - "detect": "物体検出", + "detect": "物体検知", "record": "録画", "audio": "音声" }, @@ -973,12 +1064,12 @@ "notConnected": "未接続", "featuresTitle": "機能", "go2rtc": "カメラへの接続数を削減", - "detectRoleWarning": "続行するには、少なくとも 1 つのストリームに「検出」ロールが必要です。", + "detectRoleWarning": "続行するには、少なくとも 1 つのストリームに「検知」ロールが必要です。", "rolesPopover": { "title": "ストリーム ロール", - "detect": "物体検出用のメイン フィードです。", + "detect": "物体検知用のメイン フィードです。", "record": "設定に基づいて映像フィードのセグメントを保存します。", - "audio": "音声ベース検出用のフィードです。" + "audio": "音声ベース検知用のフィードです。" }, "featuresPopover": { "title": "ストリーム機能", @@ -999,8 +1090,8 @@ }, "testResultLabels": { "resolution": "解像度", - "video": "ビデオ", - "audio": "オーディオ", + "video": "映像", + "audio": "音声", "fps": "FPS" }, "commonErrors": { @@ -1008,7 +1099,7 @@ "testFailed": "ストリームテストに失敗しました: {{error}}" }, "step1": { - "description": "カメラの詳細を入力し、カメラを自動検出するか、メーカーを手動で選択してください。", + "description": "カメラの詳細を入力し、カメラを自動検知するか、メーカーを手動で選択してください。", "cameraName": "カメラ名", "cameraNamePlaceholder": "例: front_door または Back Yard Overview", "host": "ホスト/IP アドレス", @@ -1040,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": "ストリームをさらに追加", @@ -1156,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 を有効にし、ウィザードを再起動してください。", @@ -1170,7 +1261,8 @@ "hikvision": { "substreamWarning": "サブストリーム 1 は低解像度に固定されています。多くの Hikvision カメラは追加のサブストリームをサポートしており、カメラ設定で有効化する必要があります。利用可能であればそれらのストリームを使用することを推奨します。" }, - "noAudioWarning": "このストリームでは音声が検出されていません。録画には音声が含まれません。" + "noAudioWarning": "このストリームでは音声が検知されていません。録画には音声が含まれません。", + "resolutionUnknown": "このストリームの解像度を取得できませんでした。設定画面または設定ファイルで検知解像度を手動で指定してください。" }, "ffmpegModuleDescription": "何度か試してもストリームが読み込まれない場合は、このオプションを有効にしてください。有効にすると、Frigate は go2rtc と併用して ffmpeg モジュールを使用します。一部のカメラストリームでは、互換性が向上する場合があります。" } @@ -1183,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": "カメラを追加", @@ -1213,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": { @@ -1227,26 +1453,26 @@ }, "review": { "title": "レビュー", - "desc": "Frigate を再起動するまで、このカメラのアラートと検出を一時的に有効/無効にします。無効にすると、新しいレビュー項目は生成されません。 ", + "desc": "Frigate を再起動するまで、このカメラのアラートと検知を一時的に有効/無効にします。無効にすると、新しいレビュー項目は生成されません。 ", "alerts": "アラート ", - "detections": "検出 " + "detections": "検知 " }, "reviewClassification": { "title": "レビュー分類", - "desc": "Frigate はレビュー項目をアラートと検出に分類します。既定では、すべての personcar オブジェクトはアラートとして扱われます。必須ゾーンを設定することで、分類をより細かく調整できます。", + "desc": "Frigate はレビュー項目をアラートと検知に分類します。既定では、すべての personcar オブジェクトはアラートとして扱われます。必須ゾーンを設定することで、分類をより細かく調整できます。", "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 を再起動してください。" } @@ -1255,8 +1481,61 @@ "maintenance": { "sync": { "status": { - "queued": "キューに追加済み" - } + "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": { @@ -1270,6 +1549,544 @@ "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 fd64e58a1b..2971039638 100644 --- a/web/public/locales/ja/views/system.json +++ b/web/public/locales/ja/views/system.json @@ -23,7 +23,7 @@ "error": "ログをクリップボードにコピーできませんでした" }, "type": { - "label": "種類", + "label": "タイプ", "timestamp": "タイムスタンプ", "tag": "タグ", "message": "メッセージ" @@ -70,7 +70,7 @@ "inferenceSpeed": "ディテクタ推論速度", "temperature": "ディテクタ温度", "cpuUsage": "ディテクタの CPU 使用率", - "cpuUsageInformation": "検出モデルへの入力/出力データの準備に使用される CPU。GPU やアクセラレータを使用していても、この値は推論の使用量を測定しません。", + "cpuUsageInformation": "検知モデルへの入力/出力データの準備に使用される CPU。GPU やアクセラレータを使用していても、この値は推論の使用量を測定しません。", "memoryUsage": "ディテクタのメモリ使用量" }, "hardwareInfo": { @@ -108,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": "その他のプロセス", @@ -134,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": "カメラストレージ", @@ -169,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": { @@ -193,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": "高度解析", @@ -219,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/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/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/nb-NO/common.json b/web/public/locales/nb-NO/common.json index 411463881c..00fb832e3f 100644 --- a/web/public/locales/nb-NO/common.json +++ b/web/public/locales/nb-NO/common.json @@ -216,7 +216,8 @@ "gl": "Galego (Galisisk)", "id": "Bahasa Indonesia (Indonesisk)", "ur": "اردو (Urdu)", - "hr": "Hrvatski (Kroatisk)" + "hr": "Hrvatski (Kroatisk)", + "bs": "Bosanski (Bosnisk)" }, "appearance": "Utseende", "darkMode": { @@ -241,7 +242,8 @@ "classification": "Klassifisering", "profiles": "Profiler", "chat": "Chat", - "actions": "Handlinger" + "actions": "Handlinger", + "features": "Funksjoner" }, "pagination": { "next": { diff --git a/web/public/locales/nb-NO/config/cameras.json b/web/public/locales/nb-NO/config/cameras.json index d2a44f51e5..da2f22cfe0 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." @@ -71,7 +71,11 @@ }, "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", @@ -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,10 @@ "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" } }, "preview": { @@ -619,7 +627,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", diff --git a/web/public/locales/nb-NO/config/global.json b/web/public/locales/nb-NO/config/global.json index 4c669b31db..9b288f47f3 100644 --- a/web/public/locales/nb-NO/config/global.json +++ b/web/public/locales/nb-NO/config/global.json @@ -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." } }, @@ -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", @@ -559,7 +559,11 @@ }, "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", @@ -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,10 @@ "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" } }, "preview": { @@ -1143,7 +1186,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", 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 index 0967ef424b..7d42966fd4 100644 --- a/web/public/locales/nb-NO/views/chat.json +++ b/web/public/locales/nb-NO/views/chat.json @@ -1 +1,69 @@ -{} +{ + "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" + } +} 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..da219820f5 100644 --- a/web/public/locales/nb-NO/views/live.json +++ b/web/public/locales/nb-NO/views/live.json @@ -152,7 +152,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", diff --git a/web/public/locales/nb-NO/views/motionSearch.json b/web/public/locales/nb-NO/views/motionSearch.json index 0967ef424b..c8fdb7c873 100644 --- a/web/public/locales/nb-NO/views/motionSearch.json +++ b/web/public/locales/nb-NO/views/motionSearch.json @@ -1 +1,75 @@ -{} +{ + "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" + }, + "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 opptakssegmenter samtidig (raskere, men betydelig mer CPU-intensivt)", + "threshold": "Følsomhetsterskel", + "thresholdDesc": "Lavere verdier detekterer mindre endringer (1–255)", + "minArea": "Minimum endringsområde", + "minAreaDesc": "Minimum prosentandel av interesseområdet som må endres for å anses som betydelig", + "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" + } +} diff --git a/web/public/locales/nb-NO/views/replay.json b/web/public/locales/nb-NO/views/replay.json index 0967ef424b..3cf72e2011 100644 --- a/web/public/locales/nb-NO/views/replay.json +++ b/web/public/locales/nb-NO/views/replay.json @@ -1 +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 6d907f91ea..fac229142e 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", @@ -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": { @@ -786,11 +788,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 +827,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", @@ -1341,7 +1352,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." } } }, @@ -1357,8 +1369,17 @@ "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.", "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" }, "cameraConfig": { "add": "Legg til kamera", @@ -1408,7 +1429,16 @@ "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 hvilke kameraer som er aktivert og konfigurer overstyringer for hver profil og kameratype. For å konfigurere strømmer, deteksjon, bevegelse og andre kameraspesifikke innstillinger, velg den aktuelle seksjonen under Kamerakonfigurasjon." }, "cameraReview": { "title": "Innstillinger for kamerainspeksjon", @@ -1572,7 +1602,9 @@ "options": { "embeddings": "Vektorrepresentasjoner", "tools": "Verktøy", - "vision": "Bildegjenkjenning" + "vision": "Bildegjenkjenning", + "descriptions": "Beskrivelser", + "chat": "Chat" } }, "additionalProperties": { @@ -1645,7 +1677,24 @@ "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": { @@ -1659,7 +1708,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", @@ -1676,7 +1725,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", @@ -1696,7 +1753,8 @@ "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}}" }, "profiles": { "active": "Aktiv", @@ -1744,18 +1802,21 @@ "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." }, "birdseye": { "objectsModeDetectDisabled": "Fugleperspektiv er satt til 'objekter'-modus, men objektdeteksjon er deaktivert for dette kameraet. Kameraet vil ikke vises i Fugleperspektiv." @@ -1773,8 +1834,15 @@ "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." } }, "maintenance": { @@ -1839,7 +1907,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.", @@ -1886,7 +1961,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": { @@ -1903,5 +1980,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..ef3ca18e1e 100644 --- a/web/public/locales/nb-NO/views/system.json +++ b/web/public/locales/nb-NO/views/system.json @@ -210,6 +210,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 index 474844fc05..795ce510df 100644 --- a/web/public/locales/ne/audio.json +++ b/web/public/locales/ne/audio.json @@ -6,5 +6,16 @@ "bellow": "तलतिर", "motorcycle": "मोटरसाइकल", "whoop": "हुप (Whoop)", - "whispering": "सानो बोल्दै" + "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 index f252005ef8..ec2203d22c 100644 --- a/web/public/locales/ne/common.json +++ b/web/public/locales/ne/common.json @@ -3,6 +3,16 @@ "untilForRestart": "फ्रिगेट पुनः सुरु नभएसम्म।", "untilRestart": "पुन: सुरु नभएसम्म", "never": "कहिल्यै होइन", - "ago": "{{timeAgo}} अघि" + "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 index 81ffe7c340..e61a7b778c 100644 --- a/web/public/locales/ne/components/auth.json +++ b/web/public/locales/ne/components/auth.json @@ -5,7 +5,12 @@ "login": "लगइन", "firstTimeLogin": "पहिलो पटक लग इन गर्ने प्रयास गर्दै हुनुहुन्छ? प्रमाणपत्रहरू फ्रिगेट लगहरूमा छापिएका हुन्छन्।", "errors": { - "usernameRequired": "प्रयोगकर्ता नाम आवश्यक छ" + "usernameRequired": "प्रयोगकर्ता नाम आवश्यक छ", + "passwordRequired": "पासवर्ड आवश्यक छ", + "rateLimit": "दर सीमा नाघ्यो। पछि फेरि प्रयास गर्नुहोस्।", + "loginFailed": "लगइन असफल भयो", + "unknownError": "अज्ञात त्रुटि। लगहरू जाँच गर्नुहोस्", + "webUnknownError": "अज्ञात त्रुटि। कन्सोल लगहरू जाँच गर्नुहोस्।" } } } diff --git a/web/public/locales/ne/components/camera.json b/web/public/locales/ne/components/camera.json index 98f5d8338d..59a1682243 100644 --- a/web/public/locales/ne/components/camera.json +++ b/web/public/locales/ne/components/camera.json @@ -6,8 +6,23 @@ "delete": { "label": "क्यामेरा समूह मेटाउनुहोस्", "confirm": { - "title": "मेटाउने पुष्टि गर्नुहोस्" + "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 index 634f89b00e..25cdda520d 100644 --- a/web/public/locales/ne/components/dialog.json +++ b/web/public/locales/ne/components/dialog.json @@ -5,7 +5,30 @@ "button": "पुनः सुरु", "restarting": { "title": "फ्रिगेट पुन: सुरु हुँदैछ", - "content": "यो पृष्ठ {{countdown}} सेकेन्डमा पुन: लोड हुनेछ।" + "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 index d520a85389..40a28af632 100644 --- a/web/public/locales/ne/components/filter.json +++ b/web/public/locales/ne/components/filter.json @@ -7,5 +7,24 @@ }, "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/player.json b/web/public/locales/ne/components/player.json index 64663e4518..ffb97d5402 100644 --- a/web/public/locales/ne/components/player.json +++ b/web/public/locales/ne/components/player.json @@ -5,5 +5,22 @@ "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 index 4f9f3489ad..3179ad38dd 100644 --- a/web/public/locales/ne/config/cameras.json +++ b/web/public/locales/ne/config/cameras.json @@ -7,5 +7,27 @@ "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 index 0967ef424b..fe3f978e70 100644 --- a/web/public/locales/ne/config/global.json +++ b/web/public/locales/ne/config/global.json @@ -1 +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 index e80784d181..f3ea1a4af6 100644 --- a/web/public/locales/ne/config/groups.json +++ b/web/public/locales/ne/config/groups.json @@ -12,6 +12,33 @@ "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 index 7592167cde..ead6ccf550 100644 --- a/web/public/locales/ne/config/validation.json +++ b/web/public/locales/ne/config/validation.json @@ -3,5 +3,14 @@ "maximum": "बढीमा हुनुपर्छ {{limit}}", "exclusiveMinimum": "{{limit}} भन्दा बढी हुनुपर्छ", "exclusiveMaximum": ".{{limit}} भन्दा कम हुनुपर्छ", - "minLength": "कम्तिमा {{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 index 74f59f8ad5..e1826aaad9 100644 --- a/web/public/locales/ne/objects.json +++ b/web/public/locales/ne/objects.json @@ -3,5 +3,14 @@ "bicycle": "साइकल", "car": "कार", "motorcycle": "मोटरसाइकल", - "airplane": "हवाइजहाज" + "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 index 774d1fcdb2..1afa99cc5e 100644 --- a/web/public/locales/ne/views/chat.json +++ b/web/public/locales/ne/views/chat.json @@ -3,5 +3,13 @@ "title": "फ्रिगेट च्याट", "subtitle": "क्यामेरा व्यवस्थापन र अन्तर्दृष्टिको लागि तपाईंको एआई सहायक", "placeholder": "सोध्नुहोस्...", - "error": "केही गडबड भयो। कृपया फेरि प्रयास गर्नुहोस्।" + "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 index 4b42e1ec57..e37b8ee94c 100644 --- a/web/public/locales/ne/views/classificationModel.json +++ b/web/public/locales/ne/views/classificationModel.json @@ -10,6 +10,16 @@ }, "button": { "deleteClassificationAttempts": "वर्गीकरण छविहरू मेटाउनुहोस्", - "renameCategory": "वर्गको नाम बदल्नुहोस्" + "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 index fb7d183536..ac03412677 100644 --- a/web/public/locales/ne/views/configEditor.json +++ b/web/public/locales/ne/views/configEditor.json @@ -3,5 +3,16 @@ "configEditor": "कन्फिग सम्पादक", "safeConfigEditor": "कन्फिग सम्पादक (सुरक्षित मोड)", "safeModeDescription": "कन्फिग प्रमाणीकरण त्रुटिको कारणले फ्रिगेट सुरक्षित मोडमा छ।", - "copyConfig": "कन्फिग प्रतिलिपि गर्नुहोस्" + "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 index 9dcd78594a..b04c4d7f7b 100644 --- a/web/public/locales/ne/views/events.json +++ b/web/public/locales/ne/views/events.json @@ -5,5 +5,19 @@ "label": "गति", "only": "गति मात्र" }, - "allCameras": "सबै क्यामेराहरू" + "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 index 815bcd764b..80ca127f6f 100644 --- a/web/public/locales/ne/views/explore.json +++ b/web/public/locales/ne/views/explore.json @@ -1,5 +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 index e9d15ea91d..2afa762d39 100644 --- a/web/public/locales/ne/views/exports.json +++ b/web/public/locales/ne/views/exports.json @@ -4,5 +4,21 @@ "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 index 67b7c6a9d5..1728f993cd 100644 --- a/web/public/locales/ne/views/faceLibrary.json +++ b/web/public/locales/ne/views/faceLibrary.json @@ -7,6 +7,20 @@ }, "details": { "unknown": "अज्ञात", - "timestamp": "टाइमस्ट्याम्प" + "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 index 6e2b6dbac9..2a4a191d62 100644 --- a/web/public/locales/ne/views/live.json +++ b/web/public/locales/ne/views/live.json @@ -7,5 +7,28 @@ "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 index 1cae42fd4c..22533b1412 100644 --- a/web/public/locales/ne/views/motionSearch.json +++ b/web/public/locales/ne/views/motionSearch.json @@ -3,5 +3,15 @@ "title": "गति खोज", "description": "रुचिको क्षेत्र परिभाषित गर्न बहुभुज कोर्नुहोस्, र त्यो क्षेत्र भित्र गति परिवर्तनहरू खोज्नको लागि समय दायरा निर्दिष्ट गर्नुहोस्।", "selectCamera": "गति खोज लोड हुँदैछ", - "startSearch": "खोज सुरु गर्नुहोस्" + "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 index 439507f27d..03ee1d4b3b 100644 --- a/web/public/locales/ne/views/recording.json +++ b/web/public/locales/ne/views/recording.json @@ -5,7 +5,8 @@ "filters": "फिल्टरहरू", "toast": { "error": { - "noValidTimeSelected": "कुनै मान्य समय दायरा चयन गरिएको छैन" + "noValidTimeSelected": "कुनै मान्य समय दायरा चयन गरिएको छैन", + "endTimeMustAfterStartTime": "अन्त्य समय सुरु समय पछि हुनुपर्छ" } } } diff --git a/web/public/locales/ne/views/replay.json b/web/public/locales/ne/views/replay.json index 320d74c42f..a9185bb087 100644 --- a/web/public/locales/ne/views/replay.json +++ b/web/public/locales/ne/views/replay.json @@ -5,6 +5,16 @@ "dialog": { "title": "डिबग रिप्ले सुरु गर्नुहोस्", "description": "वस्तु पत्ता लगाउने र ट्र्याकिङ समस्याहरू डिबग गर्न ऐतिहासिक फुटेज लुप गर्ने अस्थायी रिप्ले क्यामेरा सिर्जना गर्नुहोस्। रिप्ले क्यामेरामा स्रोत क्यामेरा जस्तै पत्ता लगाउने कन्फिगरेसन हुनेछ। सुरु गर्न समय दायरा छनौट गर्नुहोस्।", - "camera": "स्रोत क्यामेरा" + "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 index 065d64d379..185843f08a 100644 --- a/web/public/locales/ne/views/search.json +++ b/web/public/locales/ne/views/search.json @@ -4,6 +4,19 @@ "searchFor": "खोज्नुहोस् {{inputValue}}", "button": { "clear": "खोज खाली गर्नुहोस्", - "save": "खोज बचत गर्नुहोस्" + "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 index 6f7076bc20..0ce812e838 100644 --- a/web/public/locales/ne/views/settings.json +++ b/web/public/locales/ne/views/settings.json @@ -4,6 +4,14 @@ "authentication": "प्रमाणीकरण सेटिङहरू - फ्रिगेट", "cameraManagement": "क्यामेराहरू व्यवस्थापन गर्नुहोस् - फ्रिगेट", "cameraReview": "क्यामेरा समीक्षा सेटिङहरू - फ्रिगेट", - "enrichments": "संवर्धन सेटिङहरू - फ्रिगेट" + "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 index 86c449ac83..e399283e5f 100644 --- a/web/public/locales/ne/views/system.json +++ b/web/public/locales/ne/views/system.json @@ -6,7 +6,19 @@ "enrichments": "संवर्धन तथ्याङ्क - फ्रिगेट", "logs": { "frigate": "फ्रिगेट लगहरू - फ्रिगेट", - "go2rtc": "Go2RTC लगहरू - फ्रिगेट" + "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..d47040c3bb 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,8 @@ "gl": "Galego (Galicisch)", "id": "Bahasa Indonesia (Indonesisch)", "ur": "اردو (Urdu)", - "hr": "Hrvatski (Kroatisch)" + "hr": "Hrvatski (Kroatisch)", + "zhHant": "繁體中文 (Traditioneel Chinees)" }, "darkMode": { "label": "Donkere modus", @@ -307,5 +320,8 @@ "field": { "optional": "Optioneel", "internalID": "De interne ID die Frigate gebruikt in de configuratie en database" + }, + "credentialField": { + "savedPlaceholder": "Opgeslagen - leeg laten om huidige te behouden" } } 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..4e7fd083b8 100644 --- a/web/public/locales/nl/components/dialog.json +++ b/web/public/locales/nl/components/dialog.json @@ -77,7 +77,37 @@ "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." } }, "streaming": { 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 index 0967ef424b..d4ffad1fb8 100644 --- a/web/public/locales/nl/views/chat.json +++ b/web/public/locales/nl/views/chat.json @@ -1 +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 7d655b134a..c58fc60e92 100644 --- a/web/public/locales/nl/views/classificationModel.json +++ b/web/public/locales/nl/views/classificationModel.json @@ -12,10 +12,10 @@ }, "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.", @@ -33,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": { @@ -155,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." } } }, @@ -187,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/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 198af35fb5..e12f191e8a 100644 --- a/web/public/locales/nl/views/live.json +++ b/web/public/locales/nl/views/live.json @@ -54,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", @@ -108,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: " @@ -150,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 index 0967ef424b..b289113983 100644 --- a/web/public/locales/nl/views/motionSearch.json +++ b/web/public/locales/nl/views/motionSearch.json @@ -1 +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 index 0967ef424b..143c16ec48 100644 --- a/web/public/locales/nl/views/replay.json +++ b/web/public/locales/nl/views/replay.json @@ -1 +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..3d0c5bcc7f 100644 --- a/web/public/locales/pl/common.json +++ b/web/public/locales/pl/common.json @@ -156,7 +156,18 @@ "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" }, "menu": { "system": "System", @@ -207,7 +218,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 +273,8 @@ "help": "Pomoc", "settings": "Ustawienia", "export": "Eksportuj", - "classification": "Klasyfikacja" + "classification": "Klasyfikacja", + "profiles": "Profile" }, "role": { "viewer": "Przeglądający", @@ -298,7 +312,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ę", 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..06408f81bf 100644 --- a/web/public/locales/pl/components/dialog.json +++ b/web/public/locales/pl/components/dialog.json @@ -68,7 +68,7 @@ "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" }, @@ -77,6 +77,18 @@ "fromTimeline": { "saveExport": "Zapisz Eksport", "previewExport": "Podgląd Eksportu" + }, + "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ę" } }, "recording": { diff --git a/web/public/locales/pl/components/player.json b/web/public/locales/pl/components/player.json index 3cadb947f5..3cd8520438 100644 --- a/web/public/locales/pl/components/player.json +++ b/web/public/locales/pl/components/player.json @@ -48,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..b7430d75d6 100644 --- a/web/public/locales/pl/config/cameras.json +++ b/web/public/locales/pl/config/cameras.json @@ -221,5 +221,30 @@ "label": "Transkrypcja na żywo", "description": "Włącz transkrypcję strumieniową audio na żywo w momencie jego odbierania." } + }, + "birdseye": { + "mode": { + "label": "Tryb śledzenia" + }, + "order": { + "label": "Pozycja" + } + }, + "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" + } + }, + "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." + } } } diff --git a/web/public/locales/pl/config/global.json b/web/public/locales/pl/config/global.json index ff2108fcbd..8d16de3feb 100644 --- a/web/public/locales/pl/config/global.json +++ b/web/public/locales/pl/config/global.json @@ -68,5 +68,27 @@ "label": "Włącz uwierzytelnianie", "description": "Włącz natywne uwierzytelnianie dla interfejsu Frigate." } + }, + "birdseye": { + "mode": { + "label": "Tryb śledzenia" + }, + "order": { + "label": "Pozycja" + } + }, + "detect": { + "enabled": { + "label": "Włącz wykrywanie obiektów" + }, + "stationary": { + "label": "Konfiguracja obiektów stacjonarnych" + } + }, + "face_recognition": { + "label": "Rozpoznawanie twarzy", + "enabled": { + "label": "Włącz rozpoznawanie twarzy" + } } } diff --git a/web/public/locales/pl/views/settings.json b/web/public/locales/pl/views/settings.json index 38cb2cc4ac..6167a9d0e9 100644 --- a/web/public/locales/pl/views/settings.json +++ b/web/public/locales/pl/views/settings.json @@ -13,7 +13,62 @@ "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": { @@ -100,7 +155,8 @@ "globalConfig": "Konfiguracja globalna - Frigate", "cameraConfig": "Konfiguracja kamery - Frigate", "maintenance": "Konserwacja – Frigate", - "profiles": "Profile - Frigate" + "profiles": "Profile - Frigate", + "detectorsAndModel": "Detektory i model" }, "classification": { "title": "Ustawienia Klasyfikacji", @@ -1231,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/pt-BR/audio.json b/web/public/locales/pt-BR/audio.json index b36f099021..552a135650 100644 --- a/web/public/locales/pt-BR/audio.json +++ b/web/public/locales/pt-BR/audio.json @@ -429,5 +429,73 @@ "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" } diff --git a/web/public/locales/pt-BR/common.json b/web/public/locales/pt-BR/common.json index f02bdc03e2..b2ebe49c28 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,8 @@ "gl": "Galego (Galego)", "id": "Bahasa Indonesia (Indonésio)", "ur": "اردو (Urdu)", - "hr": "Hrvatski (Croata)" + "hr": "Hrvatski (Croata)", + "bs": "Bosanski (Bósnio)" }, "systemLogs": "Logs de sistema", "settings": "Configurações", @@ -263,7 +266,9 @@ }, "classification": "Classificação", "chat": "Chat", - "profiles": "Perfis" + "profiles": "Perfis", + "actions": "Ações", + "features": "Funcionalidades" }, "toast": { "copyUrlToClipboard": "URL copiada para a área de transferência.", @@ -272,7 +277,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": { @@ -315,5 +321,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..f82636861d 100644 --- a/web/public/locales/pt-BR/components/camera.json +++ b/web/public/locales/pt-BR/components/camera.json @@ -82,6 +82,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..788fe118d7 100644 --- a/web/public/locales/pt-BR/components/dialog.json +++ b/web/public/locales/pt-BR/components/dialog.json @@ -68,7 +68,45 @@ }, "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" + }, + "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..." } }, "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/ro/common.json b/web/public/locales/ro/common.json index 00716ac2c4..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", @@ -322,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 56dd59dcfb..56bee6587a 100644 --- a/web/public/locales/ro/components/dialog.json +++ b/web/public/locales/ro/components/dialog.json @@ -148,15 +148,15 @@ "exportButton_other": "Exportă {{count}} de camere" }, "multi": { - "title_one": "Exportă 1 recenzie", - "title_few": "Exportă {{count}} recenzii", - "title_other": "Exportă {{count}} de recenzii", - "description": "Exportă fiecare recenzie selectată. Toate exporturile vor fi grupate sub un singur caz.", - "descriptionNoCase": "Exportă fiecare recenzie selectată.", - "caseNamePlaceholder": "Export recenzie - {{date}}", - "exportButton_one": "Exportă 1 recenzie", - "exportButton_few": "Exportă {{count}} recenzii", - "exportButton_other": "Exportă {{count}} de recenzii", + "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.", diff --git a/web/public/locales/ro/components/player.json b/web/public/locales/ro/components/player.json index ebcad44a25..71cb3cbde9 100644 --- a/web/public/locales/ro/components/player.json +++ b/web/public/locales/ro/components/player.json @@ -48,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 918598b0ad..9a79c555d8 100644 --- a/web/public/locales/ro/config/cameras.json +++ b/web/public/locales/ro/config/cameras.json @@ -33,7 +33,11 @@ }, "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ă", @@ -682,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.)." @@ -862,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 f7207df758..40d2135cb8 100644 --- a/web/public/locales/ro/config/global.json +++ b/web/public/locales/ro/config/global.json @@ -19,7 +19,11 @@ }, "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ă", @@ -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": { @@ -1135,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", @@ -2298,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 index b87ef2145f..36a892cee1 100644 --- a/web/public/locales/ro/views/chat.json +++ b/web/public/locales/ro/views/chat.json @@ -42,5 +42,31 @@ "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/explore.json b/web/public/locales/ro/views/explore.json index 4cb9f3c7ff..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": { 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 59f9c34060..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", @@ -151,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 index 0f12367484..1b455df083 100644 --- a/web/public/locales/ro/views/motionSearch.json +++ b/web/public/locales/ro/views/motionSearch.json @@ -26,7 +26,9 @@ "points_few": "{{count}} puncte", "points_other": "{{count}} de puncte", "undo": "Anulează ultimul punct", - "reset": "Resetează poligonul" + "reset": "Resetează poligonul", + "moveMode": "Mută", + "drawMode": "Desenează" }, "motionHeatmapLabel": "Harta termică a mișcării", "dialog": { @@ -42,11 +44,11 @@ "settings": { "title": "Setări de căutare", "parallelMode": "Mod paralel", - "parallelModeDesc": "Scanează mai multe segmente de înregistrare în același timp (mai rapid, dar consumă semnificativ mai mult procesorul)", + "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": "Procentul minim din regiunea de interes care trebuie să se schimbe pentru a fi considerat semnificativ", + "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", @@ -72,6 +74,9 @@ "framesDecoded": "Cadre decodate", "wallTime": "Timp de căutare", "segmentErrors": "Erori segment", - "seconds": "{{seconds}}s" - } + "seconds": "{{seconds}}s", + "minutesSeconds": "{{minutes}}m {{seconds}}s", + "scanSummary": "{{segments}} segmente · {{time}}" + }, + "scanning": "Scanare {{time}}" } diff --git a/web/public/locales/ro/views/settings.json b/web/public/locales/ro/views/settings.json index 4babba1d9e..b08b2f26da 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,7 +44,7 @@ "globalFfmpeg": "FFmpeg", "globalMotion": "Detecție mișcare", "globalObjects": "Obiecte", - "globalReview": "Recenzie", + "globalReview": "Revizuire", "globalAudioEvents": "Detecție audio", "globalLivePlayback": "Redare live", "globalTimestampStyle": "Stil timestamp", @@ -73,7 +74,7 @@ "cameraSnapshots": "Snapshot-uri", "cameraMotion": "Detecție mișcare", "cameraObjects": "Obiecte", - "cameraConfigReview": "Recenzie", + "cameraConfigReview": "Revizuire", "cameraAudioEvents": "Detecție audio", "cameraAudioTranscription": "Transcriere audio", "cameraNotifications": "Notificări", @@ -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" @@ -1294,19 +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ă", @@ -1352,10 +1390,12 @@ "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ă", @@ -1364,6 +1404,95 @@ "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": { @@ -1663,7 +1792,9 @@ "options": { "embeddings": "Înglobare", "vision": "Viziune", - "tools": "Instrumente" + "tools": "Instrumente", + "descriptions": "Descrieri", + "chat": "Chat" } }, "semanticSearchModel": { @@ -1678,13 +1809,43 @@ }, "addCustomLabel": "Adaugă etichetă personalizată...", "genaiModel": { - "placeholder": "Selectează modelul…", - "search": "Caută modele…", - "noModels": "Niciun model disponibil" + "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": { @@ -1720,7 +1881,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", @@ -1745,7 +1909,15 @@ "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", @@ -1829,8 +2001,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", @@ -1840,7 +2022,14 @@ }, "onvif": { "profileAuto": "Auto", - "profileLoading": "Se încarcă profilurile..." + "profileLoading": "Se încarcă profilurile...", + "autotracking": { + "zooming": { + "disabled": "Dezactivat", + "absolute": "Absolut", + "relative": "Relativ" + } + } }, "configMessages": { "review": { @@ -1857,7 +2046,13 @@ }, "detect": { "fpsGreaterThanFive": "Setarea FPS-ului de detecție mai mare de 5 nu este recomandată. Valorile mai mari pot cauza probleme de performanță și nu vor oferi niciun beneficiu.", - "disabled": "Detecția de obiecte este dezactivată. Snapshot-urile, elementele de revizuire și îmbogățirile precum recunoașterea facială, recunoașterea plăcuțelor de înmatriculare și AI-ul generativ nu vor funcționa." + "disabled": "Detecția de obiecte este dezactivată. Snapshot-urile, elementele de revizuire și îmbogățirile precum recunoașterea facială, recunoașterea plăcuțelor de înmatriculare și AI-ul generativ nu vor funcționa.", + "resolutionShouldBeMultipleOfFour": "Pentru rezultate optime, lățimea și înălțimea de detecție ar trebui să fie multipli de 4. Alte valori pare pot produce artefacte vizuale sau o ușoară distorsionare în fluxul de detecție.", + "aspectRatioMismatch": "Lățimea și înălțimea introduse nu se potrivesc cu raportul de aspect al rezoluției actuale de detecție. Acest lucru poate produce o imagine întinsă sau distorsionată.", + "maxFramesSet": "Setarea numărului maxim de cadre suprascrie comportamentul implicit și dezactivează urmărirea obiectelor staționare. Există foarte puține situații în care acest lucru este necesar, folosește cu precauție.", + "squareResolution": "O rezoluție de detecție pătrată este neobișnuită. Lățimea și înălțimea de detecție ar trebui să se potrivească cu raportul de aspect al camerei tale (de exemplu, 16:9), nu cu dimensiunile modelului de detecție a obiectelor. Un raport de aspect nepotrivit poate întinde imaginea și reduce acuratețea detecției.", + "resolutionHigh": "Această rezoluție de detecție este mai mare decât cea recomandată și poate cauza un consum crescut de resurse fără a îmbunătăți acuratețea detecției. O rezoluție de detecție de 1080p sau mai mică este recomandată pentru majoritatea camerelor.", + "globalResolutionMultipleCameras": "O rezoluție de detecție globală este setată în timp ce sunt configurate mai multe camere. Dacă nu cumva toate camerele împart aceeași rezoluție și același raport de aspect, lățimea și înălțimea de detecție ar trebui definite per cameră pentru a se potrivi cu raportul de aspect nativ al fiecărei camere." }, "faceRecognition": { "globalDisabled": "Îmbogățirea pentru recunoaștere facială trebuie activată pentru ca funcțiile de recunoaștere facială să funcționeze pe această cameră.", @@ -1887,6 +2082,108 @@ }, "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." } + }, + "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 ef285da8b6..59f52e08c4 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", diff --git a/web/public/locales/ru/config/groups.json b/web/public/locales/ru/config/groups.json index d69c495829..a7c9152f5b 100644 --- a/web/public/locales/ru/config/groups.json +++ b/web/public/locales/ru/config/groups.json @@ -1,7 +1,8 @@ { "audio": { "global": { - "sensitivity": "Общая чувствительность" + "sensitivity": "Общая чувствительность", + "detection": "Общее обнаружение" }, "cameras": { "detection": "Обнаружение", diff --git a/web/public/locales/ru/config/validation.json b/web/public/locales/ru/config/validation.json index 1d16abf7a4..2314881ed9 100644 --- a/web/public/locales/ru/config/validation.json +++ b/web/public/locales/ru/config/validation.json @@ -27,5 +27,6 @@ "detectRequired": "Как минимум один входной поток должен быть назначен роли 'detect'.", "hwaccelDetectOnly": "Только входной поток с ролью detect может настраивать аппаратное ускорение." } - } + }, + "minimum": "Должно быть минимум {{limit}}" } diff --git a/web/public/locales/sv/common.json b/web/public/locales/sv/common.json index a60cfb2afd..d3f916b931 100644 --- a/web/public/locales/sv/common.json +++ b/web/public/locales/sv/common.json @@ -114,7 +114,12 @@ "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" }, "menu": { "language": { @@ -207,7 +212,7 @@ "help": "Hjälp", "documentation": { "title": "Dokumentation", - "label": "Frigate dokumentation" + "label": "Frigate-dokumentation" }, "uiPlayground": "UI Testmiljö", "restart": "Starta om Frigate", diff --git a/web/public/locales/sv/components/camera.json b/web/public/locales/sv/components/camera.json index 23de9471f6..75fc11f82a 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.", diff --git a/web/public/locales/sv/components/dialog.json b/web/public/locales/sv/components/dialog.json index 4b8899a2c3..d77e76a75b 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": { 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/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..701f168fe9 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", 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/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..bb41cc21a4 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" }, @@ -244,7 +244,7 @@ } }, "motionMaskLabel": "Rörelsemask {{number}}", - "objectMaskLabel": "Objektmask {{number}} ({{label}})", + "objectMaskLabel": "Objektmask {{number}}", "form": { "zoneName": { "error": { @@ -407,7 +407,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 +430,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 +567,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 +597,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 +684,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 +714,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 +728,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 +1079,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 +1225,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 index 0967ef424b..4390d3961c 100644 --- a/web/public/locales/th/views/chat.json +++ b/web/public/locales/th/views/chat.json @@ -1 +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 index 0967ef424b..852fb09cb9 100644 --- a/web/public/locales/th/views/motionSearch.json +++ b/web/public/locales/th/views/motionSearch.json @@ -1 +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 index 0967ef424b..1c8cc9b831 100644 --- a/web/public/locales/th/views/replay.json +++ b/web/public/locales/th/views/replay.json @@ -1 +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/components/dialog.json b/web/public/locales/tr/components/dialog.json index 19cf03c621..f2c8face04 100644 --- a/web/public/locales/tr/components/dialog.json +++ b/web/public/locales/tr/components/dialog.json @@ -74,7 +74,8 @@ }, "fromTimeline": { "saveExport": "Dışa Aktarımı Kaydet", - "previewExport": "Dışa Aktarımı Önizle" + "previewExport": "Dışa Aktarımı Önizle", + "useThisRange": "Use This Range" }, "name": { "placeholder": "Dışa Aktarımı Adlandırın" diff --git a/web/public/locales/uk/common.json b/web/public/locales/uk/common.json index 39dff176fc..937bd735bb 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,9 @@ "bg": "Български (Болгарська)", "gl": "Galego (Галісійська)", "id": "Bahasa Indonesia (Індонезійська)", - "ur": "اردو (Урду)" + "ur": "اردو (Урду)", + "hr": "Hrvatski (Хорватська)", + "bs": "Bosanski (Боснійська)" }, "system": "Система", "systemMetrics": "Системна метріка", @@ -223,7 +237,11 @@ }, "appearance": "Зовнішність", "withSystem": "Система", - "classification": "Класифікація" + "classification": "Класифікація", + "profiles": "Профілі", + "actions": "Дії", + "features": "Особливості", + "chat": "Чат" }, "unit": { "speed": { @@ -258,7 +276,8 @@ "error": { "title": "Не вдалося зберегти зміни конфігурації: {{errorMessage}}", "noMessage": "Не вдалося зберегти зміни налаштування" - } + }, + "success": "Зміни до налаштувань збережені вдало." }, "copyUrlToClipboard": "Скопійовано URL до буфера обміну." }, @@ -303,5 +322,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..dc12ccb8f2 100644 --- a/web/public/locales/uk/components/camera.json +++ b/web/public/locales/uk/components/camera.json @@ -82,6 +82,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..300dc5fc57 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": { 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/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..a503559ed2 100644 --- a/web/public/locales/uk/views/system.json +++ b/web/public/locales/uk/views/system.json @@ -68,7 +68,7 @@ "classification_speed": "Швидкість класифікації {{name}}", "classification_events_per_second": "{{name}} Подій класифікації за секунду" }, - "title": "Збагаченням", + "title": "Збагачення", "infPerSecond": "Висновки за секунду", "averageInf": "Середній час висновування" }, 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/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/zh-CN/common.json b/web/public/locales/zh-CN/common.json index 5771a2288e..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": { @@ -318,5 +320,8 @@ "internalID": "Frigate 在配置与数据库中使用的内部 ID" }, "no_items": "没有项目", - "validation_errors": "验证错误" + "validation_errors": "验证错误", + "credentialField": { + "savedPlaceholder": "已保存 — 留空则保留当前设置" + } } diff --git a/web/public/locales/zh-CN/components/player.json b/web/public/locales/zh-CN/components/player.json index 6cee6952be..70956ccb35 100644 --- a/web/public/locales/zh-CN/components/player.json +++ b/web/public/locales/zh-CN/components/player.json @@ -48,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 5bd976c693..9faae8a747 100644 --- a/web/public/locales/zh-CN/config/cameras.json +++ b/web/public/locales/zh-CN/config/cameras.json @@ -37,7 +37,11 @@ }, "filters": { "label": "音频过滤器", - "description": "按音频类型的过滤器设置,如用于减少误报的置信度阈值。" + "description": "按音频类型的过滤器设置,如用于减少误报的置信度阈值。", + "threshold": { + "label": "最低音频置信度", + "description": "设置音频事件所需的最低置信度阈值。" + } }, "enabled_in_config": { "label": "原始音频状态", @@ -68,7 +72,7 @@ }, "mode": { "label": "追踪模式", - "description": "在鸟瞰视图中包含摄像头的模式:'objects'(目标)、'motion'(动作)或 'continuous'(持续)。" + "description": "在鸟瞰视图中包含摄像头的模式有:“基于目标”、“基于画面变动”或“连续”。" }, "order": { "label": "排序位置", @@ -603,7 +607,7 @@ }, "image_source": { "label": "核查图像来源", - "description": "发送给生成式 AI 的画面来源('preview' 或 'recordings');'recordings' 使用更高质量的画面帧,但会消耗更多的 token。" + "description": "发送给生成式 AI 的画面来源(“预览” 或 “录制”);“录制”将使用更高质量的画面帧,但会消耗更多的 token。" }, "additional_concerns": { "label": "额外关注事项", @@ -714,7 +718,7 @@ "label": "时间戳效果", "description": "时间戳文本的视觉效果(none、solid、shadow)。" }, - "description": "应用于录像和快照的实时监控流中时间戳的样式选项。" + "description": "快照与调试视图的时间戳样式设置。" }, "semantic_search": { "label": "语义搜索", @@ -723,7 +727,7 @@ "description": "摄像头特定语义搜索触发器的操作和匹配条件。", "friendly_name": { "label": "友好名称", - "description": "在 UI 中为此触发器显示的可选友好名称。" + "description": "可选友好名称,用于在界面上为触发器显示此名称。" }, "enabled": { "label": "开启此触发器", @@ -852,8 +856,12 @@ "description": "用于在页面中排序摄像头的顺序(只会影响默认仪表板和列表);数值越大则在越后面。" }, "dashboard": { - "label": "在 UI 中显示", + "label": "在页面中显示", "description": "切换此摄像头在 Frigate 页面的所有位置是否可见。禁用此项将需要手动编辑配置才能在页面中再次查看此摄像头。" + }, + "review": { + "label": "在核查中显示", + "description": "切换该摄像头是否在核查页面可见(包含核查页、摄像头筛选栏、画面变动核查与历史视图)。" } }, "best_image_timeout": { @@ -873,7 +881,7 @@ "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 fed5425d7b..4e35be178b 100644 --- a/web/public/locales/zh-CN/config/global.json +++ b/web/public/locales/zh-CN/config/global.json @@ -48,7 +48,11 @@ }, "filters": { "label": "音频过滤器", - "description": "按音频类型的过滤器设置,如用于减少误报的置信度阈值。" + "description": "按音频类型的过滤器设置,如用于减少误报的置信度阈值。", + "threshold": { + "label": "最低音频置信度", + "description": "设置音频事件所需的最低置信度阈值。" + } }, "enabled_in_config": { "label": "原始音频状态", @@ -136,7 +140,7 @@ }, "mode": { "label": "追踪模式", - "description": "在鸟瞰视图中包含摄像头的模式:'objects'(目标)、'motion'(动作)或 'continuous'(持续)。" + "description": "在鸟瞰视图中包含摄像头的模式有:“基于目标”、“基于画面变动”或“连续”。" }, "order": { "label": "排序位置", @@ -252,7 +256,7 @@ "description": "所有摄像头的人脸检测和识别设置;可按摄像头覆盖。", "model_size": { "label": "模型大小", - "description": "用于人脸嵌入的模型大小(small/large);较大的可能需要 GPU。" + "description": "用于人脸嵌入的模型大小(小型/大型);较大的可能需要 GPU。" }, "unknown_score": { "label": "未知分数阈值", @@ -492,7 +496,7 @@ }, "default_role": { "label": "默认权限组", - "description": "当没有权限组映射适用时分配给代理认证用户的默认权限组(admin 或 viewer)。" + "description": "当没有权限组映射适用时分配给代理认证用户的默认权限组。" }, "separator": { "label": "分隔符", @@ -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": "单位系统", @@ -1652,6 +1656,41 @@ "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": { @@ -1756,7 +1795,7 @@ }, "review": { "label": "核查", - "description": "控制 UI 和存储使用的警报、检测和 GenAI 核查摘要的设置。", + "description": "控制界面与存储所使用的警报、检测和生成式 AI 核查总结的相关设置。", "alerts": { "label": "警报配置", "description": "哪些追踪目标生成警报以及如何保留警报的设置。", @@ -1822,7 +1861,7 @@ }, "image_source": { "label": "核查图像来源", - "description": "发送给生成式 AI 的画面来源('preview' 或 'recordings');'recordings' 使用更高质量的画面帧,但会消耗更多的 token。" + "description": "发送给生成式 AI 的画面来源(“预览” 或 “录制”);“录制”将使用更高质量的画面帧,但会消耗更多的 token。" }, "additional_concerns": { "label": "额外关注事项", @@ -2015,7 +2054,7 @@ }, "model_size": { "label": "模型大小", - "description": "选择模型大小;'small' 在 CPU 上运行,'large' 通常需要 GPU。" + "description": "选择模型大小;“小型”模型一般在 CPU 上运行,而“大型”模型通常需要 GPU。" }, "device": { "label": "设备", @@ -2026,7 +2065,7 @@ "description": "摄像头特定语义搜索触发器的操作和匹配条件。", "friendly_name": { "label": "友好名称", - "description": "在 UI 中为此触发器显示的可选友好名称。" + "description": "可选友好名称,用于在界面上为触发器显示此名称。" }, "enabled": { "label": "开启此触发器", @@ -2059,7 +2098,7 @@ }, "model_size": { "label": "模型大小", - "description": "用于文本检测/识别的模型大小,大多数用户应使用 'small',只有'small'模型支持中文。" + "description": "用于文本检测/识别的模型大小,大多数用户应使用“小型”模型,而且只有“小型”模型支持中文车牌。" }, "detection_threshold": { "label": "检测阈值", @@ -2172,8 +2211,12 @@ "description": "用于在页面中排序摄像头的顺序(只会影响默认仪表板和列表);数值越大则在越后面。" }, "dashboard": { - "label": "在 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 index 894b3c6d50..dee13463e5 100644 --- a/web/public/locales/zh-CN/views/chat.json +++ b/web/public/locales/zh-CN/views/chat.json @@ -42,5 +42,31 @@ "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/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 53688c6dfa..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": "屏蔽所有摄像头", @@ -151,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 index af8874daf1..11dafbeea8 100644 --- a/web/public/locales/zh-CN/views/motionSearch.json +++ b/web/public/locales/zh-CN/views/motionSearch.json @@ -22,7 +22,9 @@ "polygonControls": { "points_other": "{{count}} 个点位", "undo": "撤销上一个点位", - "reset": "重置多边形" + "reset": "重置多边形", + "drawMode": "绘制", + "moveMode": "移动" }, "motionHeatmapLabel": "画面变动热力图", "dialog": { @@ -38,11 +40,11 @@ "settings": { "title": "搜索设置", "parallelMode": "并行模式", - "parallelModeDesc": "同时扫描多个录制片段(速度更快,但 CPU 占用会显著升高)", + "parallelModeDesc": "同时扫描多个录制片段(速度更快,将使用更多解码资源)", "threshold": "灵敏度阈值", "thresholdDesc": "数值越低,可检测到越小的变化(取值范围 1-255)", "minArea": "最小变化区域", - "minAreaDesc": "最小感兴趣区域变化占比,达到该比例才会判定为有效变动", + "minAreaDesc": "单个移动区域的最小尺寸,占目标区域的百分比", "frameSkip": "帧跳过", "frameSkipDesc": "每隔 N 帧进行一次处理。将该值设置为摄像头的帧率,即可实现每秒处理一帧画面(例如:5 帧 / 秒的摄像头设为 5,30 帧 / 秒的摄像头设为 30)。数值越高处理速度越快,但有可能遗漏短时移动侦测事件。", "maxResults": "最大结果数", @@ -68,6 +70,9 @@ "framesDecoded": "画面已解码", "wallTime": "搜索时间", "segmentErrors": "片段异常", - "seconds": "{{seconds}} 秒" - } + "seconds": "{{seconds}} 秒", + "minutesSeconds": "{{minutes}}分 {{seconds}}秒", + "scanSummary": "{{segments}} 分段 · {{time}}" + }, + "scanning": "扫描中 {{time}}" } diff --git a/web/public/locales/zh-CN/views/settings.json b/web/public/locales/zh-CN/views/settings.json index 3831dfc56c..aa7904a6ca 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": "常规", @@ -52,7 +53,7 @@ "systemTls": "TLS加密链接", "systemAuthentication": "验证", "systemNetworking": "网络", - "systemProxy": "代理", + "systemProxy": "反向代理", "systemUi": "界面", "systemLogging": "日志", "systemEnvironmentVariables": "环境变量", @@ -92,7 +93,8 @@ "uiSettings": "界面设置", "profiles": "配置模板", "systemGo2rtcStreams": "go2rtc 视频流", - "maintenance": "维护" + "maintenance": "维护", + "systemDetectorsAndModel": "检测器和模型" }, "dialog": { "unsavedChanges": { @@ -724,7 +726,8 @@ "notificationUnavailable": { "title": "通知功能不可用", "desc": "网页推送通知需要安全连接(https://…)。这是浏览器的限制。请通过安全方式访问 Frigate 以使用通知功能。", - "documentation": "阅读文档" + "documentation": "阅读文档", + "descPwa": "在 iOS 设备上,只有将 Frigate 安装到主屏幕后,才能使用网页推送通知。请打开分享菜单,选择添加到主屏幕,然后从新生成的图标打开 Frigate,即可注册此设备以接收通知。" }, "email": { "title": "电子邮箱", @@ -795,12 +798,20 @@ "cameras": "摄像头", "loading": "正在加载模型信息…", "error": "加载模型信息失败", - "availableModels": "可用模型", + "availableModels": "可用 Frigate+ 模型", "loadingAvailableModels": "正在加载可用模型…", "modelSelect": "您可以在Frigate+上选择可用的模型。请注意,只能选择与当前检测器配置兼容的模型。", "plusModelType": { "baseModel": "基础模型", "userModel": "定向调优" + }, + "noModelLoaded": "当前未加载任何Frigate+模型。", + "selectModel": "选择一个模型", + "noModelsAvailable": "无可用模型", + "filter": { + "ariaLabel": "按类型筛选模型", + "baseModels": "基础模型", + "fineTunedModels": "微调过的模型" } }, "toast": { @@ -815,7 +826,8 @@ "currentModel": "当前模型", "otherModels": "其他模型", "configuration": "配置" - } + }, + "changeInDetectorsAndModel": "改变模型" }, "enrichments": { "title": "增强功能设置", @@ -846,7 +858,7 @@ "desc": "将使用 模型。该选项使用了完整的 Jina 模型,条件允许的情况下将自动使用 GPU 运行。" } }, - "title": "分类搜索", + "title": "语义搜索", "desc": "Frigate 中的语义搜索功能将能够让你通过图片、用户自定义的文本描述,或自动生成的文本描述等方式在核查项目中查找目标/物体。", "readTheDocumentation": "阅读文档" }, @@ -1122,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" @@ -1350,19 +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": "添加摄像头", @@ -1408,18 +1446,103 @@ "profiles": { "title": "配置模板的摄像头覆盖项", "selectLabel": "选择配置模板", - "description": "配置在启用某个配置模板时,哪些摄像头应被开启或关闭。设置为“继承”的摄像头会沿用它原本的启用/禁用状态。", + "description": "配置启用配置文件时开启或关闭的摄像头。设置为“继承”的摄像头会沿用它原本的默认状态。", "inherit": "继承", "enabled": "开启", - "disabled": "关闭" + "disabled": "关闭", + "on": "开", + "off": "关" }, "cameraType": { "title": "摄像头类型", "label": "摄像头类型", - "description": "为每路摄像头设置类型。专用车牌识别(LPR)摄像头为单用途设备,配备高倍光学变焦,可抓拍远处车辆的车牌。绝大多数摄像头应选用通用类型;只有专为车牌识别部署、且画面聚焦对准车牌的摄像头,才需选择专用 LPR 类型。", + "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": { @@ -1659,7 +1782,9 @@ "options": { "embeddings": "嵌入(Embedding)", "vision": "视觉(Vision)", - "tools": "工具(Tools)" + "tools": "工具(Tools)", + "descriptions": "描述生成", + "chat": "聊天对话" } }, "semanticSearchModel": { @@ -1673,13 +1798,43 @@ }, "addCustomLabel": "添加自定义标签…", "genaiModel": { - "placeholder": "选择模型…", - "search": "搜索模型…", - "noModels": "暂无模型" + "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": { @@ -1771,7 +1926,8 @@ "resetError": "重置设置失败", "saveAllSuccess_other": "所有 {{count}} 个部分保存成功。", "saveAllPartial_other": "已保存 {{successCount}} / {{totalCount}} 个部分。{{failCount}} 个失败。", - "saveAllFailure": "保存所有部分失败。" + "saveAllFailure": "保存所有部分失败。", + "saveAllSuccessRestartRequired_other": "成功保存 {{count}} 个部分。重启 Frigate 以生效。" }, "unsavedChanges": "您有未保存的更改", "confirmReset": "确认重置", @@ -1788,7 +1944,11 @@ "heading_other": "此全局设置项下有 {{count}} 个摄像头存在自定义单独配置。", "othersField_other": "其余 {{count}} 个", "profilePrefix": "{{profile}} 配置方案:{{fields}}" - } + }, + "overriddenGlobalHeading_other": "该摄像头已覆盖全局配置中的 {{count}} 项设置:", + "overriddenGlobalNoDeltas": "该摄像头已覆盖全局配置,但所有配置项数值均无差异。", + "overriddenBaseConfigHeading_other": "{{profile}} 配置模板已覆盖基础配置中的 {{count}} 项设置:", + "overriddenBaseConfigNoDeltas": "{{profile}} 配置模板已覆盖该板块,但各项参数与基础配置完全一致无差异。" }, "profiles": { "title": "配置模板", @@ -1876,12 +2036,29 @@ "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": { @@ -1903,7 +2080,9 @@ }, "detect": { "fpsGreaterThanFive": "不建议设置检测帧率高于 5,数值设置过高可能引发性能问题,且不会带来任何增益。", - "disabled": "目标检测已禁用。快照、回放条目以及人脸识别、车牌识别、生成式 AI 等增强功能都将无法使用。" + "disabled": "目标检测已禁用。快照、回放条目以及人脸识别、车牌识别、生成式 AI 等增强功能都将无法使用。", + "resolutionShouldBeMultipleOfFour": "为了获得最佳效果,检测的宽度和高度应该是4的倍数。其他偶数值可能会在检测流中产生视觉伪影或轻微失真。", + "aspectRatioMismatch": "你输入的宽度和高度与当前检测分辨率的不匹配,这可能会导致图像被拉伸或变形。" }, "faceRecognition": { "globalDisabled": "必须开启人脸识别增强功能,此摄像头的人脸识别相关功能才能正常使用。", @@ -1927,7 +2106,110 @@ "genaiNoDescriptionsProvider": "必须配置具备“描述”功能的生成式 AI 服务商,才能自动生成事件描述。" }, "semanticSearch": { - "jinav2SmallModelSize": "Jina V2 的大型模型版本内存占用与推理开销较高,建议搭配独立显卡使用大型模型。" + "jinav2SmallModelSize": "Jina V2 的大型模型版本内存占用与推理开销较高,建议搭配独立显卡使用大型模型。", + "modelSizeIgnoredForProvider": "模型大小仅适用于内置的 Jina 模型。当使用生成式 AI 作为嵌入提供者时,此值将被忽略。" + }, + "onvif": { + "autotrackingNoZones": "自动追踪至少需要一个区域。请先在“遮罩 / 区域”中为此摄像头定义一个区域,然后在下方将其设置为必需区域。" } + }, + "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-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..31ee3313e9 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..3d6f33a684 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": { @@ -57,11 +58,60 @@ "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}} 個攝影機" + }, + "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": { @@ -109,6 +159,14 @@ "markAsReviewed": "標記為已審核", "deleteNow": "立即刪除", "markAsUnreviewed": "標記為未審核" + }, + "shareTimestamp": { + "label": "分享該時間片段", + "title": "分享該時間片段", + "description": "分享帶當前錄製播放時間的網址,或選擇自訂時間。請注意這不是公開的分享連結,只有具備 Frigate 及此攝影機存取權限的使用者才能存取。", + "custom": "自訂時間", + "button": "分享時間片段網址", + "shareTitle": "Frigate 審閱時間:{{camera}}" } }, "imagePicker": { 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..1b989699af 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..258eb17608 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,1612 @@ "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 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": "模型庫", + "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": "最小置信度" + } + } + }, + "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 頁面中是否可見。停用後需要手動編輯配置才能再次在頁面中檢視此攝影機。" + } + }, + "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..2c0274b3b6 100644 --- a/web/public/locales/zh-Hant/config/validation.json +++ b/web/public/locales/zh-Hant/config/validation.json @@ -1 +1,32 @@ -{} +{ + "minimum": "必須至少為 {{limit}}", + "maximum": "最大值不能超過 {{limit}}", + "exclusiveMinimum": "必須大於 {{limit}}", + "exclusiveMaximum": "必須小於 {{limit}}", + "minLength": "長度至少為 {{limit}} 個字元", + "maxLength": "長度最多為 {{limit}} 個字元", + "minItems": "至少包含 {{limit}} 項", + "maxItems": "最多包含 {{limit}} 項", + "pattern": "格式無效", + "required": "此欄位為必填項", + "type": "值型別無效", + "enum": "必須是允許的值之一", + "const": "值與預期的常量不匹配", + "uniqueItems": "所有項必須唯一", + "format": "格式無效", + "additionalProperties": "不允許未知屬性", + "oneOf": "必須完全匹配一個允許的模式", + "anyOf": "必須至少匹配一個允許的模式", + "proxy": { + "header_map": { + "roleHeaderRequired": "設定角色對應時必須要有 role 標頭。" + } + }, + "ffmpeg": { + "inputs": { + "rolesUnique": "每個角色只能分配給一個輸入串流。", + "detectRequired": "必須至少有一個輸入串流分配為 'detect' 角色。", + "hwaccelDetectOnly": "只有分配了 detect 角色的輸入串流才能定義硬體加速引數。" + } + } +} 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 index 0967ef424b..fced43e35e 100644 --- a/web/public/locales/zh-Hant/views/chat.json +++ b/web/public/locales/zh-Hant/views/chat.json @@ -1 +1,64 @@ -{} +{ + "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/秒" + } +} diff --git a/web/public/locales/zh-Hant/views/classificationModel.json b/web/public/locales/zh-Hant/views/classificationModel.json index 796495f691..6c0a1d9651 100644 --- a/web/public/locales/zh-Hant/views/classificationModel.json +++ b/web/public/locales/zh-Hant/views/classificationModel.json @@ -8,7 +8,8 @@ "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..671e9201bf 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": "刪除此追蹤物件將移除截圖、所有已保存的嵌入,以及所有相關的追蹤詳情。歷史記錄中的錄影不會被刪除。

    你確定要刪除嗎?" + }, + "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..d1e28743fd 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 鏡頭" @@ -67,7 +70,8 @@ }, "recording": { "enable": "啟用錄影", - "disable": "停用錄影" + "disable": "停用錄影", + "disabledInConfig": "必須先在該攝影機的設定中開啟錄製功能。" }, "snapshots": { "enable": "啟用截圖", @@ -134,6 +138,9 @@ "playInBackground": { "label": "背景播放", "tips": "啟用此選項以在播放器被隱藏時繼續播放串流。" + }, + "debug": { + "picker": "除錯模式下無法切換影片流。除錯將始終使用偵測(detect)功能的影片流。" } }, "cameraSettings": { @@ -143,7 +150,8 @@ "recording": "錄影", "snapshots": "截圖", "audioDetection": "音訊偵測", - "autotracking": "自動追蹤" + "autotracking": "自動追蹤", + "transcription": "音訊轉錄" }, "history": { "label": "顯示歷史影像" @@ -172,5 +180,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 index 0967ef424b..a83835afa0 100644 --- a/web/public/locales/zh-Hant/views/motionSearch.json +++ b/web/public/locales/zh-Hant/views/motionSearch.json @@ -1 +1,73 @@ -{} +{ + "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": "重設多邊形" + }, + "motionHeatmapLabel": "畫面變動熱力圖", + "dialog": { + "title": "畫面變動搜尋", + "cameraLabel": "攝影機", + "previewAlt": "{{camera}} 攝影機即時預覽" + }, + "timeRange": { + "title": "搜尋範圍", + "start": "開始時間", + "end": "結束時間" + }, + "settings": { + "title": "搜尋設定", + "parallelMode": "並行模式", + "parallelModeDesc": "同時掃描多個錄製片段(速度更快,但 CPU 佔用會顯著升高)", + "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}} 秒" + } +} diff --git a/web/public/locales/zh-Hant/views/replay.json b/web/public/locales/zh-Hant/views/replay.json index 0967ef424b..afe2cee4c6 100644 --- a/web/public/locales/zh-Hant/views/replay.json +++ b/web/public/locales/zh-Hant/views/replay.json @@ -1 +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..5252467276 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": "將使用 模型。該模型使用的記憶體較少,在 CPU 上也能較快的執行,品質較好。" + }, + "desc": "用於語意搜尋的語言模型大小。", + "large": { + "title": "大", + "desc": "將使用 模型。該選項使用了完整的 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,1539 @@ "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": "重新命名" + } + }, + "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": "關閉" + }, + "cameraType": { + "title": "攝影機型別", + "label": "攝影機型別", + "description": "為每路攝影機設定型別。專用車牌辨識(LPR)攝影機為單用途裝置,配備高倍光學變焦,可抓拍遠處車輛的車牌。絕大多數攝影機應選用通用型別;只有專為車牌辨識部署、且畫面聚焦對準車牌的攝影機,才需選擇專用 LPR 型別。", + "normal": "通用", + "dedicatedLpr": "車牌辨識專用", + "saveSuccess": "已更新 {{cameraName}} 的攝影機型別,請重啟 Frigate 以使更改生效。" + } + }, + "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 以使用通知功能。" + }, + "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}}" + }, + "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": "暫無模型" + } + }, + "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": "儲存所有部分失敗。" + }, + "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": "自動選擇硬體加速" + } + }, + "birdseye": { + "trackingMode": { + "objects": "目標", + "motion": "動作", + "continuous": "持續" + } + }, + "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": "Warning", + "error": "Error", + "critical": "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 等增強功能都將無法使用。" + }, + "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 的大型模型版本記憶體佔用與推理開銷較高,建議搭配獨立顯示卡使用大型模型。" } } } diff --git a/web/public/locales/zh-Hant/views/system.json b/web/public/locales/zh-Hant/views/system.json index e956b9a42e..23aa19f880 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": { @@ -156,7 +191,8 @@ "cameraDetect": "{{camName}} 偵測", "cameraFramesPerSecond": "{{camName}} 幀率", "cameraDetectionsPerSecond": "{{camName}} 每秒偵測幀率", - "cameraSkippedDetectionsPerSecond": "{{camName}} 每秒跳過偵測幀率" + "cameraSkippedDetectionsPerSecond": "{{camName}} 每秒跳過偵測幀率", + "cameraGpu": "{{camName}} GPU" }, "toast": { "success": { @@ -165,6 +201,20 @@ "error": { "unableToProbeCamera": "無法檢測鏡頭:{{errorMessage}}" } + }, + "noCameras": { + "title": "沒有找到攝影機" + }, + "connectionQuality": { + "title": "連線品質", + "excellent": "優秀", + "fair": "一般", + "poor": "較差", + "unusable": "不可用", + "fps": "幀率", + "expectedFps": "預期幀率", + "reconnectsLastHour": "最近一小時重連次數", + "stallsLastHour": "最近一小時卡頓次數" } }, "lastRefreshed": "最後更新: ", @@ -176,7 +226,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 d00772f9d5..d73e5a3090 100644 --- a/web/src/api/WsProvider.tsx +++ b/web/src/api/WsProvider.tsx @@ -10,15 +10,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; @@ -31,6 +37,10 @@ export function WsProvider({ children }: { children: ReactNode }) { 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) => { @@ -64,6 +74,7 @@ export function WsProvider({ children }: { children: ReactNode }) { ws.onerror = null; ws.close(); } + queue.clear(); resetWsStore(); }; }, [wsUrl]); 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} > -
    -
    +
    +
    {children}
    diff --git a/web/src/components/card/ExportCard.tsx b/web/src/components/card/ExportCard.tsx index 893f251f8f..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: ( + + + + ), + }); + } 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 && ( <> ), 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 ( +
    + {attachedEventId && onClearAttachment && ( +
    + +
    + )} + {attachedEventId && ( + sendMessage(text)} + disabled={isLoading} + /> + )} +
    + {showPaperclip && ( + + )} + {supportsThinking && ( + + + + + + {t("thinking.toggle")} + + + )} + setInput(e.target.value)} + onKeyDown={handleKeyDown} + aria-busy={isLoading} + /> + {showStop ? ( + + ) : ( + + )} +
    +
    + ); +} diff --git a/web/src/components/chat/ChatEventThumbnailsRow.tsx b/web/src/components/chat/ChatEventThumbnailsRow.tsx index 94eca3f2d6..47b8d46656 100644 --- a/web/src/components/chat/ChatEventThumbnailsRow.tsx +++ b/web/src/components/chat/ChatEventThumbnailsRow.tsx @@ -1,4 +1,5 @@ import { useApiHost } from "@/api"; +import { baseUrl } from "@/api/baseUrl"; import { useTranslation } from "react-i18next"; import { LuExternalLink } from "react-icons/lu"; import { @@ -54,7 +55,7 @@ export function ChatEventThumbnailsRow({
    e.stopPropagation()} diff --git a/web/src/components/chat/ChatSettings.tsx b/web/src/components/chat/ChatSettings.tsx index 0f68ef22d6..ad63694591 100644 --- a/web/src/components/chat/ChatSettings.tsx +++ b/web/src/components/chat/ChatSettings.tsx @@ -48,7 +48,7 @@ export default function ChatSettings({
    -
    {t("settings.show_stats.title")}
    +
    {t("settings.show_stats.title")}
    {t("settings.show_stats.desc")}
    @@ -77,7 +77,7 @@ export default function ChatSettings({
    -
    @@ -457,7 +457,7 @@ export default function Step1NameAndDefine({
    @@ -489,7 +489,7 @@ export default function Step1NameAndDefine({ -
    +
    diff --git a/web/src/components/classification/wizard/Step2StateArea.tsx b/web/src/components/classification/wizard/Step2StateArea.tsx index efba0d358a..e768b1d970 100644 --- a/web/src/components/classification/wizard/Step2StateArea.tsx +++ b/web/src/components/classification/wizard/Step2StateArea.tsx @@ -14,6 +14,7 @@ import Konva from "konva"; import { useResizeObserver } from "@/hooks/resize-observer"; import { useApiHost } from "@/api"; import { resolveCameraName } from "@/hooks/use-camera-friendly-name"; +import { isReplayCamera } from "@/utils/cameraUtil"; import Heading from "@/components/ui/heading"; import { isMobile } from "react-device-detect"; import { cn } from "@/lib/utils"; @@ -50,6 +51,7 @@ export default function Step2StateArea({ const [imageLoaded, setImageLoaded] = useState(false); const containerRef = useRef(null); + const popoverContainerRef = useRef(null); const imageRef = useRef(null); const stageRef = useRef(null); const rectRef = useRef(null); @@ -67,6 +69,7 @@ export default function Step2StateArea({ ([name, cam]) => cam.enabled && cam.enabled_in_config && + !isReplayCamera(name) && !selectedCameraNames.includes(name), ) .map(([name]) => ({ @@ -222,7 +225,7 @@ export default function Step2StateArea({ const canContinue = cameraAreas.length > 0; return ( -
    +
    e.preventDefault()} >
    @@ -456,7 +460,7 @@ export default function Step2StateArea({
    -
    +
    diff --git a/web/src/components/classification/wizard/Step3ChooseExamples.tsx b/web/src/components/classification/wizard/Step3ChooseExamples.tsx index c6693d0296..75f4d263f0 100644 --- a/web/src/components/classification/wizard/Step3ChooseExamples.tsx +++ b/web/src/components/classification/wizard/Step3ChooseExamples.tsx @@ -1,4 +1,4 @@ -import { Button } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { useTranslation } from "react-i18next"; import { useState, useEffect, useCallback, useMemo } from "react"; import ActivityIndicator from "@/components/indicators/activity-indicator"; @@ -540,7 +540,7 @@ export default function Step3ChooseExamples({ {t("button.continue", { ns: "common" })} @@ -693,7 +693,7 @@ export default function Step3ChooseExamples({ )} {!isTraining && ( -
    +
    diff --git a/web/src/components/config-form/ConfigFieldMessage.tsx b/web/src/components/config-form/ConfigFieldMessage.tsx index 5c0a5c5056..545d50ba7c 100644 --- a/web/src/components/config-form/ConfigFieldMessage.tsx +++ b/web/src/components/config-form/ConfigFieldMessage.tsx @@ -15,13 +15,13 @@ const severityVariantMap: Record< function SeverityIcon({ severity }: { severity: string }) { switch (severity) { case "info": - return ; + return ; case "warning": - return ; + return ; case "error": - return ; + return ; default: - return ; + return ; } } diff --git a/web/src/components/config-form/ConfigMessageBanner.tsx b/web/src/components/config-form/ConfigMessageBanner.tsx index f5b8280003..8e701947a0 100644 --- a/web/src/components/config-form/ConfigMessageBanner.tsx +++ b/web/src/components/config-form/ConfigMessageBanner.tsx @@ -18,11 +18,11 @@ const severityVariantMap: Record< function SeverityIcon({ severity }: { severity: MessageSeverity }) { switch (severity) { case "info": - return ; + return ; case "warning": - return ; + return ; case "error": - return ; + return ; } } @@ -44,7 +44,7 @@ export function ConfigMessageBanner({ messages }: ConfigMessageBannerProps) { className="flex items-center [&>svg+div]:translate-y-0 [&>svg]:static [&>svg~*]:pl-2" > - {t(msg.messageKey)} + {t(msg.messageKey, msg.values)} ))}
    diff --git a/web/src/components/config-form/FieldMessagesContext.ts b/web/src/components/config-form/FieldMessagesContext.ts new file mode 100644 index 0000000000..5d45f7d79b --- /dev/null +++ b/web/src/components/config-form/FieldMessagesContext.ts @@ -0,0 +1,13 @@ +import { createContext } from "react"; +import type { FieldConditionalMessage } from "./section-configs/types"; + +// Provides currently-active field messages to FieldTemplate without going +// through RJSF's per-field uiSchema. RJSF caches state.uiSchema across renders +// in a way that can leave stale ui:messages attached to a field when the +// triggering condition flips back to false (see processPendingChange in +// @rjsf/core Form.js — formData is updated immediately, uiSchema is not). +// useContext re-runs consumers directly on provider value change, sidestepping +// that staleness. +export const FieldMessagesContext = createContext( + [], +); diff --git a/web/src/components/config-form/LiveFormDataContext.ts b/web/src/components/config-form/LiveFormDataContext.ts new file mode 100644 index 0000000000..10d9a3e82c --- /dev/null +++ b/web/src/components/config-form/LiveFormDataContext.ts @@ -0,0 +1,13 @@ +import { createContext } from "react"; +import type { ConfigSectionData } from "@/types/configForm"; + +// Mirrors the current section's in-flight form data so widgets can react +// to changes that RJSF wouldn't otherwise re-render them for. RJSF's +// Form memoizes SchemaField via deep equality and, in some transitions +// (notably reverting a field to its saved value), can skip re-rendering +// a widget even though the form data it depends on changed. useContext +// re-runs consumers directly on every provider value update, sidestepping +// that. +export const LiveFormDataContext = createContext( + null, +); diff --git a/web/src/components/config-form/section-configs/birdseye.ts b/web/src/components/config-form/section-configs/birdseye.ts index 26e3d2ec8c..f0538030dc 100644 --- a/web/src/components/config-form/section-configs/birdseye.ts +++ b/web/src/components/config-form/section-configs/birdseye.ts @@ -19,7 +19,7 @@ const birdseye: SectionConfigOverrides = { ], restartRequired: [], fieldOrder: ["enabled", "mode", "order"], - hiddenFields: [], + hiddenFields: ["order"], advancedFields: [], overrideFields: ["enabled", "mode"], uiSchema: { @@ -56,6 +56,7 @@ const birdseye: SectionConfigOverrides = { uiSchema: { mode: { "ui:size": "xs", + "ui:after": { render: "BirdseyeCameraReorder" }, }, }, }, diff --git a/web/src/components/config-form/section-configs/database.ts b/web/src/components/config-form/section-configs/database.ts index aa86798a99..a10618951e 100644 --- a/web/src/components/config-form/section-configs/database.ts +++ b/web/src/components/config-form/section-configs/database.ts @@ -2,7 +2,7 @@ import type { SectionConfigOverrides } from "./types"; const database: SectionConfigOverrides = { base: { - sectionDocs: "/configuration/advanced#database", + sectionDocs: "/configuration/advanced/system#database", restartRequired: ["path"], fieldOrder: ["path"], advancedFields: [], diff --git a/web/src/components/config-form/section-configs/detect.ts b/web/src/components/config-form/section-configs/detect.ts index 5bbd219822..9176c49a74 100644 --- a/web/src/components/config-form/section-configs/detect.ts +++ b/web/src/components/config-form/section-configs/detect.ts @@ -13,6 +13,103 @@ const detect: SectionConfigOverrides = { }, ], fieldMessages: [ + { + key: "detect-resolution-not-multiple-of-four", + field: "width", + position: "before", + messageKey: "configMessages.detect.resolutionShouldBeMultipleOfFour", + severity: "warning", + condition: (ctx) => { + const width = ctx.formData?.width as number | null | undefined; + const height = ctx.formData?.height as number | null | undefined; + const isEvenButNotFour = (v: unknown) => + typeof v === "number" && v % 2 === 0 && v % 4 !== 0; + return isEvenButNotFour(width) || isEvenButNotFour(height); + }, + }, + { + key: "detect-global-resolution-multiple-cameras", + field: "width", + position: "before", + messageKey: "configMessages.detect.globalResolutionMultipleCameras", + severity: "info", + condition: (ctx) => { + if (ctx.level !== "global") return false; + const width = ctx.formData?.width as number | null | undefined; + const height = ctx.formData?.height as number | null | undefined; + if (typeof width !== "number" && typeof height !== "number") { + return false; + } + const cameraCount = Object.keys(ctx.fullConfig?.cameras ?? {}).length; + return cameraCount > 1; + }, + }, + { + key: "detect-resolution-high", + field: "width", + position: "before", + messageKey: "configMessages.detect.resolutionHigh", + severity: "warning", + condition: (ctx) => { + const width = ctx.formData?.width as number | null | undefined; + const height = ctx.formData?.height as number | null | undefined; + if (typeof width !== "number" || typeof height !== "number") { + return false; + } + return Math.min(width, height) > 1080; + }, + }, + { + key: "detect-square-resolution", + field: "width", + position: "before", + messageKey: "configMessages.detect.squareResolution", + severity: "warning", + condition: (ctx) => { + const width = ctx.formData?.width as number | null | undefined; + const height = ctx.formData?.height as number | null | undefined; + return ( + typeof width === "number" && + typeof height === "number" && + width > 0 && + width === height + ); + }, + }, + { + key: "detect-aspect-ratio-mismatch", + field: "width", + position: "before", + messageKey: "configMessages.detect.aspectRatioMismatch", + severity: "warning", + condition: (ctx) => { + const newWidth = ctx.formData?.width as number | null | undefined; + const newHeight = ctx.formData?.height as number | null | undefined; + if (typeof newWidth !== "number" || typeof newHeight !== "number") { + return false; + } + const saved = + ctx.level === "camera" + ? ctx.fullCameraConfig?.detect + : ctx.fullConfig?.detect; + const savedWidth = saved?.width; + const savedHeight = saved?.height; + if ( + typeof savedWidth !== "number" || + typeof savedHeight !== "number" || + savedWidth <= 0 || + savedHeight <= 0 + ) { + return false; + } + if (newWidth === savedWidth && newHeight === savedHeight) { + return false; + } + const newRatio = newWidth / newHeight; + const savedRatio = savedWidth / savedHeight; + return Math.abs(newRatio - savedRatio) > 0.01; + }, + }, { key: "fps-greater-than-five", field: "fps", @@ -27,6 +124,31 @@ const detect: SectionConfigOverrides = { return detectFps != null && streamFps != null && detectFps > 5; }, }, + { + key: "max-frames-set", + field: "stationary.max_frames", + messageKey: "configMessages.detect.maxFramesSet", + severity: "warning", + position: "after", + condition: (ctx) => { + const stationary = ctx.formData?.stationary as + | { + max_frames?: { + default?: number | null; + objects?: Record; + } | null; + } + | null + | undefined; + const maxFrames = stationary?.max_frames; + if (!maxFrames) return false; + return ( + typeof maxFrames.default === "number" || + (maxFrames.objects != null && + Object.keys(maxFrames.objects).length > 0) + ); + }, + }, ], fieldOrder: [ "enabled", @@ -37,9 +159,9 @@ const detect: SectionConfigOverrides = { "max_disappeared", "annotation_offset", "stationary", - "interval", - "threshold", - "max_frames", + "stationary.interval", + "stationary.threshold", + "stationary.max_frames", ], restartRequired: [], fieldGroups: { @@ -72,6 +194,22 @@ const detect: SectionConfigOverrides = { "max_disappeared", ], }, + replay: { + restartRequired: [], + fieldOrder: ["width", "height", "fps"], + fieldGroups: { + resolution: ["width", "height", "fps"], + }, + hiddenFields: [ + "enabled", + "enabled_in_config", + "min_initialized", + "max_disappeared", + "annotation_offset", + "stationary", + ], + advancedFields: [], + }, }; export default detect; diff --git a/web/src/components/config-form/section-configs/environment_vars.ts b/web/src/components/config-form/section-configs/environment_vars.ts index 2100d3e354..969095d075 100644 --- a/web/src/components/config-form/section-configs/environment_vars.ts +++ b/web/src/components/config-form/section-configs/environment_vars.ts @@ -2,7 +2,7 @@ import type { SectionConfigOverrides } from "./types"; const environmentVars: SectionConfigOverrides = { base: { - sectionDocs: "/configuration/advanced#environment_vars", + sectionDocs: "/configuration/advanced/system#environment_vars", fieldOrder: [], advancedFields: [], uiSchema: { diff --git a/web/src/components/config-form/section-configs/genai.ts b/web/src/components/config-form/section-configs/genai.ts index a5f1cd8a32..c5645cbbad 100644 --- a/web/src/components/config-form/section-configs/genai.ts +++ b/web/src/components/config-form/section-configs/genai.ts @@ -2,7 +2,7 @@ import type { SectionConfigOverrides } from "./types"; const genai: SectionConfigOverrides = { base: { - sectionDocs: "/configuration/genai/config", + sectionDocs: "/configuration/genai/genai_config", advancedFields: ["*.base_url", "*.provider_options", "*.runtime_options"], hiddenFields: ["genai.enabled_in_config"], restartRequired: [], @@ -24,6 +24,7 @@ const genai: SectionConfigOverrides = { "ui:widget": "genaiRoles", }, "*.api_key": { + "ui:widget": "password", "ui:options": { size: "lg" }, }, "*.base_url": { diff --git a/web/src/components/config-form/section-configs/live.ts b/web/src/components/config-form/section-configs/live.ts index c0d80627c2..c0026ec8da 100644 --- a/web/src/components/config-form/section-configs/live.ts +++ b/web/src/components/config-form/section-configs/live.ts @@ -4,17 +4,26 @@ const live: SectionConfigOverrides = { base: { sectionDocs: "/configuration/live", restartRequired: [], - fieldOrder: ["stream_name", "height", "quality"], + fieldOrder: ["streams", "height", "quality"], fieldGroups: {}, hiddenFields: ["enabled_in_config"], advancedFields: ["height", "quality"], }, global: { - restartRequired: ["stream_name", "height", "quality"], + restartRequired: ["streams", "height", "quality"], hiddenFields: ["streams"], }, camera: { restartRequired: ["height", "quality"], + uiSchema: { + streams: { + "ui:field": "LiveStreamsField", + "ui:options": { + label: false, + suppressDescription: true, + }, + }, + }, }, }; diff --git a/web/src/components/config-form/section-configs/logger.ts b/web/src/components/config-form/section-configs/logger.ts index d70b48aae0..5cd7f6490b 100644 --- a/web/src/components/config-form/section-configs/logger.ts +++ b/web/src/components/config-form/section-configs/logger.ts @@ -2,7 +2,7 @@ import type { SectionConfigOverrides } from "./types"; const logger: SectionConfigOverrides = { base: { - sectionDocs: "/configuration/advanced#logger", + sectionDocs: "/configuration/advanced/system#frigate-logger", restartRequired: ["default", "logs"], fieldOrder: ["default", "logs"], advancedFields: ["logs"], diff --git a/web/src/components/config-form/section-configs/mqtt.ts b/web/src/components/config-form/section-configs/mqtt.ts index 67d863b089..52fd6b6d79 100644 --- a/web/src/components/config-form/section-configs/mqtt.ts +++ b/web/src/components/config-form/section-configs/mqtt.ts @@ -64,6 +64,7 @@ const mqtt: SectionConfigOverrides = { liveValidate: true, uiSchema: { password: { + "ui:widget": "password", "ui:options": { size: "xs" }, }, }, diff --git a/web/src/components/config-form/section-configs/networking.ts b/web/src/components/config-form/section-configs/networking.ts index a7ed95bf01..1a2f43505e 100644 --- a/web/src/components/config-form/section-configs/networking.ts +++ b/web/src/components/config-form/section-configs/networking.ts @@ -2,10 +2,12 @@ import type { SectionConfigOverrides } from "./types"; const networking: SectionConfigOverrides = { base: { - sectionDocs: "/configuration/advanced", + sectionDocs: "/configuration/advanced/system#network-configuration", fieldDocs: { - "listen.internal": "/configuration/advanced#listen-on-different-ports", - "listen.external": "/configuration/advanced#listen-on-different-ports", + "listen.internal": + "/configuration/advanced/system#listen-on-different-ports", + "listen.external": + "/configuration/advanced/system#listen-on-different-ports", }, restartRequired: ["ipv6.enabled", "listen.internal", "listen.external"], fieldOrder: [], diff --git a/web/src/components/config-form/section-configs/objects.ts b/web/src/components/config-form/section-configs/objects.ts index 3d27abb012..371a1f5140 100644 --- a/web/src/components/config-form/section-configs/objects.ts +++ b/web/src/components/config-form/section-configs/objects.ts @@ -1,12 +1,60 @@ -import type { FrigateConfig } from "@/types/frigateConfig"; +import type { HiddenFieldContext } from "@/types/configForm"; +import { getEffectiveAttributeLabels } from "@/utils/configUtil"; import type { SectionConfigOverrides } from "./types"; // Attribute labels (face, license_plate, Frigate+ couriers like DHL/Amazon, -// etc.) are populated into objects.filters by the backend even when the -// model can't actually detect them. They aren't user-settable, so hide any -// `filters.` patterns from forms and override comparisons. -const hideAttributeFilters = (config: FrigateConfig): string[] => - (config.model?.all_attributes ?? []).map((attr) => `filters.${attr}`); +// etc.) are populated into objects.filters by the backend for every +// attribute the model knows about. +// +// - Untracked attributes: hide the whole `filters.` collapsible. +// - Tracked attributes: strip the FilterConfig fields we don't expose +// (`threshold`, `min_ratio`, `max_ratio`) from the form data so RJSF +// doesn't surface them as ad-hoc additionalProperties entries under the +// restricted AttributeFilter schema (see modifySchemaForSection objects +// branch). The data is sanitized out symmetrically from the baseline +// too, so power-user YAML values for those fields are preserved on save +// (buildOverrides only emits diffs of fields the form has seen). +const ATTRIBUTE_FILTER_HIDDEN_SUBFIELDS = [ + "threshold", + "min_ratio", + "max_ratio", +]; + +const hideAttributeFilters = ({ + fullConfig, + fullCameraConfig, + level, + formData, +}: HiddenFieldContext): string[] => { + const trackFromForm = Array.isArray( + (formData as { track?: unknown } | undefined)?.track, + ) + ? (formData as { track: string[] }).track + : undefined; + + const track = + trackFromForm ?? + (level !== "global" ? fullCameraConfig?.objects?.track : undefined) ?? + fullConfig.objects?.track ?? + []; + + const attrs = getEffectiveAttributeLabels( + fullConfig, + fullCameraConfig, + level, + ); + const hidden: string[] = []; + for (const attr of attrs) { + if (!track.includes(attr)) { + hidden.push(`filters.${attr}`); + } else { + for (const field of ATTRIBUTE_FILTER_HIDDEN_SUBFIELDS) { + hidden.push(`filters.${attr}.${field}`); + } + } + } + return hidden; +}; const objects: SectionConfigOverrides = { base: { diff --git a/web/src/components/config-form/section-configs/onvif.ts b/web/src/components/config-form/section-configs/onvif.ts index 71163a0341..a031c992c0 100644 --- a/web/src/components/config-form/section-configs/onvif.ts +++ b/web/src/components/config-form/section-configs/onvif.ts @@ -25,10 +25,31 @@ const onvif: SectionConfigOverrides = { advancedFields: ["tls_insecure", "ignore_time_mismatch"], overrideFields: [], restartRequired: ["autotracking.calibrate_on_startup"], + fieldMessages: [ + { + key: "autotracking-no-zones", + field: "autotracking.required_zones", + messageKey: "configMessages.onvif.autotrackingNoZones", + severity: "error", + position: "before", + condition: (ctx) => { + if (ctx.level !== "camera") return false; + const zones = ctx.fullCameraConfig?.zones; + return ( + !zones || + typeof zones !== "object" || + Object.keys(zones).length === 0 + ); + }, + }, + ], uiSchema: { host: { "ui:options": { size: "sm" }, }, + password: { + "ui:widget": "password", + }, profile: { "ui:widget": "onvifProfile", }, @@ -36,11 +57,16 @@ const onvif: SectionConfigOverrides = { required_zones: { "ui:widget": "zoneNames", }, + return_preset: { + "ui:options": { size: "sm" }, + "ui:widget": "ptzPresets", + }, track: { "ui:widget": "objectLabels", }, zooming: { "ui:options": { + size: "xs", enumI18nPrefix: "onvif.autotracking.zooming", }, }, diff --git a/web/src/components/config-form/section-configs/proxy.ts b/web/src/components/config-form/section-configs/proxy.ts index ffdb27cf9f..897aef63e0 100644 --- a/web/src/components/config-form/section-configs/proxy.ts +++ b/web/src/components/config-form/section-configs/proxy.ts @@ -18,8 +18,13 @@ const proxy: SectionConfigOverrides = { "ui:options": { size: "lg" }, }, auth_secret: { + "ui:widget": "password", "ui:options": { size: "md" }, }, + default_role: { + "ui:widget": "defaultRole", + "ui:options": { size: "sm" }, + }, header_map: { "ui:after": { render: "ProxyRoleMap" }, }, diff --git a/web/src/components/config-form/section-configs/record.ts b/web/src/components/config-form/section-configs/record.ts index 0e0da30692..d1c2a94e64 100644 --- a/web/src/components/config-form/section-configs/record.ts +++ b/web/src/components/config-form/section-configs/record.ts @@ -46,7 +46,11 @@ const record: SectionConfigOverrides = { uiSchema: { export: { hwaccel_args: { - "ui:options": { suppressMultiSchema: true, size: "lg" }, + "ui:widget": "FfmpegArgsWidget", + "ui:options": { + suppressMultiSchema: true, + ffmpegPresetField: "hwaccel_args", + }, }, }, "alerts.retain.mode": { diff --git a/web/src/components/config-form/section-configs/semantic_search.ts b/web/src/components/config-form/section-configs/semantic_search.ts index 884401b7d2..dde2b75531 100644 --- a/web/src/components/config-form/section-configs/semantic_search.ts +++ b/web/src/components/config-form/section-configs/semantic_search.ts @@ -35,6 +35,7 @@ const semanticSearch: SectionConfigOverrides = { "ui:widget": "semanticSearchModel", }, model_size: { + "ui:widget": "semanticSearchModelSize", "ui:options": { size: "xs", enumI18nPrefix: "modelSize" }, }, }, diff --git a/web/src/components/config-form/section-configs/telemetry.ts b/web/src/components/config-form/section-configs/telemetry.ts index 20003a4977..43b3387446 100644 --- a/web/src/components/config-form/section-configs/telemetry.ts +++ b/web/src/components/config-form/section-configs/telemetry.ts @@ -2,7 +2,7 @@ import type { SectionConfigOverrides } from "./types"; const telemetry: SectionConfigOverrides = { base: { - sectionDocs: "/configuration/reference", + sectionDocs: "/configuration/advanced/reference", restartRequired: ["version_check"], fieldOrder: ["network_interfaces", "stats", "version_check"], advancedFields: [], diff --git a/web/src/components/config-form/section-configs/timestamp_style.ts b/web/src/components/config-form/section-configs/timestamp_style.ts index e43373c263..0639b8148d 100644 --- a/web/src/components/config-form/section-configs/timestamp_style.ts +++ b/web/src/components/config-form/section-configs/timestamp_style.ts @@ -2,7 +2,7 @@ import type { SectionConfigOverrides } from "./types"; const timestampStyle: SectionConfigOverrides = { base: { - sectionDocs: "/configuration/reference", + sectionDocs: "/configuration/advanced/reference", restartRequired: [], fieldOrder: ["position", "format", "thickness", "color"], hiddenFields: ["effect", "enabled_in_config"], diff --git a/web/src/components/config-form/section-configs/types.ts b/web/src/components/config-form/section-configs/types.ts index 9efeb2b32f..e3a89ba264 100644 --- a/web/src/components/config-form/section-configs/types.ts +++ b/web/src/components/config-form/section-configs/types.ts @@ -24,6 +24,8 @@ export type ConditionalMessage = { severity: MessageSeverity; /** Function returning true when the message should be shown */ condition: (ctx: MessageConditionContext) => boolean; + /** Optional interpolation values passed to t() for {{var}} substitution */ + values?: Record; }; /** Field-level conditional message, adds field targeting */ diff --git a/web/src/components/config-form/section-configs/ui.ts b/web/src/components/config-form/section-configs/ui.ts index bb1a50c248..0bf31c0c5c 100644 --- a/web/src/components/config-form/section-configs/ui.ts +++ b/web/src/components/config-form/section-configs/ui.ts @@ -2,21 +2,15 @@ import type { SectionConfigOverrides } from "./types"; const ui: SectionConfigOverrides = { base: { - sectionDocs: "/configuration/reference", + sectionDocs: "/configuration/advanced/reference", restartRequired: [], fieldOrder: ["dashboard", "order"], - hiddenFields: [], + hiddenFields: ["order"], advancedFields: [], overrideFields: [], }, global: { - fieldOrder: [ - "timezone", - "time_format", - "date_style", - "time_style", - "unit_system", - ], + fieldOrder: ["timezone", "time_format", "unit_system"], advancedFields: [], restartRequired: ["unit_system"], uiSchema: { @@ -26,12 +20,6 @@ const ui: SectionConfigOverrides = { time_format: { "ui:options": { enumI18nPrefix: "ui.timeFormat" }, }, - date_style: { - "ui:options": { enumI18nPrefix: "ui.TimeOrDateStyle" }, - }, - time_style: { - "ui:options": { enumI18nPrefix: "ui.TimeOrDateStyle" }, - }, unit_system: { "ui:options": { enumI18nPrefix: "ui.unitSystem" }, }, diff --git a/web/src/components/config-form/section-validations/detect.ts b/web/src/components/config-form/section-validations/detect.ts new file mode 100644 index 0000000000..7ecc805b72 --- /dev/null +++ b/web/src/components/config-form/section-validations/detect.ts @@ -0,0 +1,36 @@ +import type { FormValidation } from "@rjsf/utils"; +import type { TFunction } from "i18next"; +import { isJsonObject } from "@/lib/utils"; +import type { JsonObject } from "@/types/configForm"; + +export function validateDetectDimensions( + formData: unknown, + errors: FormValidation, + t: TFunction, +): FormValidation { + if (!isJsonObject(formData as JsonObject)) { + return errors; + } + + const data = formData as JsonObject; + const width = data.width; + const height = data.height; + + const widthErrors = errors.width as + | { addError?: (message: string) => void } + | undefined; + const heightErrors = errors.height as + | { addError?: (message: string) => void } + | undefined; + + const message = t("detect.dimensionMustBeEven", { ns: "config/validation" }); + + if (typeof width === "number" && width % 2 !== 0) { + widthErrors?.addError?.(message); + } + if (typeof height === "number" && height % 2 !== 0) { + heightErrors?.addError?.(message); + } + + return errors; +} diff --git a/web/src/components/config-form/section-validations/index.ts b/web/src/components/config-form/section-validations/index.ts index 31a02a1d10..33c02b1c7a 100644 --- a/web/src/components/config-form/section-validations/index.ts +++ b/web/src/components/config-form/section-validations/index.ts @@ -1,5 +1,6 @@ import type { FormValidation } from "@rjsf/utils"; import type { TFunction } from "i18next"; +import { validateDetectDimensions } from "./detect"; import { validateFfmpegInputRoles } from "./ffmpeg"; import { validateProxyRoleHeader } from "./proxy"; @@ -19,6 +20,10 @@ export function getSectionValidation({ level, t, }: SectionValidationOptions): SectionValidation | undefined { + if (sectionPath === "detect") { + return (formData, errors) => validateDetectDimensions(formData, errors, t); + } + if (sectionPath === "ffmpeg" && level === "camera") { return (formData, errors) => validateFfmpegInputRoles(formData, errors, t); } diff --git a/web/src/components/config-form/sectionExtras/BirdseyeCameraReorder.tsx b/web/src/components/config-form/sectionExtras/BirdseyeCameraReorder.tsx new file mode 100644 index 0000000000..b481e9a86f --- /dev/null +++ b/web/src/components/config-form/sectionExtras/BirdseyeCameraReorder.tsx @@ -0,0 +1,213 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import axios from "axios"; +import { toast } from "sonner"; +import useSWR from "swr"; +import { Reorder, useDragControls } from "framer-motion"; +import { LuCheck, LuGripVertical } from "react-icons/lu"; +import { SplitCardRow } from "@/components/card/SettingsGroupCard"; +import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { cn } from "@/lib/utils"; +import type { SectionRendererProps } from "./registry"; + +const SAVED_INDICATOR_MS = 1500; + +type SaveStatus = "idle" | "saving" | "saved"; + +export default function BirdseyeCameraReorder({ + formContext, +}: SectionRendererProps) { + const { t } = useTranslation(["views/settings", "common"]); + const { data: config, mutate: updateConfig } = + useSWR("config"); + + const birdseyeCameras = useMemo(() => { + if (!config) return []; + return Object.keys(config.cameras) + .filter( + (name) => + config.cameras[name].enabled_in_config && + config.cameras[name].birdseye?.enabled !== false, + ) + .sort((a, b) => { + const orderA = config.cameras[a].birdseye?.order ?? 0; + const orderB = config.cameras[b].birdseye?.order ?? 0; + if (orderA !== orderB) return orderA - orderB; + return a.localeCompare(b); + }); + }, [config]); + + const [orderedCameras, setOrderedCameras] = + useState(birdseyeCameras); + const orderedCamerasRef = useRef(orderedCameras); + useEffect(() => { + orderedCamerasRef.current = orderedCameras; + }, [orderedCameras]); + + useEffect(() => { + setOrderedCameras((prev) => { + if ( + prev.length === birdseyeCameras.length && + prev.every((cam, i) => cam === birdseyeCameras[i]) + ) { + return prev; + } + return birdseyeCameras; + }); + }, [birdseyeCameras]); + + const [saveStatus, setSaveStatus] = useState("idle"); + const savedResetTimerRef = useRef | null>(null); + + useEffect(() => { + return () => { + if (savedResetTimerRef.current) { + clearTimeout(savedResetTimerRef.current); + } + }; + }, []); + + const handleDragEnd = useCallback(async () => { + const current = orderedCamerasRef.current; + if ( + current.length === birdseyeCameras.length && + current.every((cam, i) => cam === birdseyeCameras[i]) + ) { + return; + } + + const cameraUpdates: Record = {}; + current.forEach((cam, i) => { + cameraUpdates[cam] = { birdseye: { order: i * 10 } }; + }); + + if (savedResetTimerRef.current) { + clearTimeout(savedResetTimerRef.current); + savedResetTimerRef.current = null; + } + setSaveStatus("saving"); + + try { + await axios.put("config/set", { + requires_restart: 0, + config_data: { cameras: cameraUpdates }, + }); + await updateConfig(); + setSaveStatus("saved"); + savedResetTimerRef.current = setTimeout(() => { + setSaveStatus("idle"); + savedResetTimerRef.current = null; + }, SAVED_INDICATOR_MS); + } catch (error) { + setOrderedCameras(birdseyeCameras); + setSaveStatus("idle"); + const errorMessage = + axios.isAxiosError(error) && + (error.response?.data?.message || error.response?.data?.detail) + ? error.response?.data?.message || error.response?.data?.detail + : t("toast.save.error.noMessage", { ns: "common" }); + + toast.error(t("toast.save.error.title", { errorMessage, ns: "common" }), { + position: "top-center", + }); + } + }, [birdseyeCameras, updateConfig, t]); + + if (formContext?.level && formContext.level !== "global") { + return null; + } + + if (!config || birdseyeCameras.length < 2) { + return null; + } + + return ( + + + {orderedCameras.map((camera) => ( + + ))} + + +
    + } + /> + ); +} + +type SaveStatusIndicatorProps = { + status: SaveStatus; +}; + +function SaveStatusIndicator({ status }: SaveStatusIndicatorProps) { + const { t } = useTranslation(["views/settings"]); + return ( +
    + {status === "saving" && ( + + {t("birdseye.cameraOrder.saving")} + + )} + {status === "saved" && ( + + + {t("birdseye.cameraOrder.saved")} + + )} +
    + ); +} + +type BirdseyeCameraRowProps = { + camera: string; + onDragEnd: () => void; +}; + +function BirdseyeCameraRow({ camera, onDragEnd }: BirdseyeCameraRowProps) { + const { t } = useTranslation(["views/settings"]); + const controls = useDragControls(); + + return ( + + + + + ); +} diff --git a/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx b/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx index d4404a0513..7a560af9f2 100644 --- a/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx +++ b/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx @@ -10,7 +10,6 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { Toaster } from "@/components/ui/sonner"; import { StatusBarMessagesContext } from "@/context/statusbar-provider"; import { FrigateConfig } from "@/types/frigateConfig"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -49,6 +48,8 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Trans, useTranslation } from "react-i18next"; import { useDateLocale } from "@/hooks/use-date-locale"; import { useDocDomain } from "@/hooks/use-doc-domain"; +import { isPWA } from "@/utils/isPWA"; +import { isIOS } from "react-device-detect"; import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; import CustomSuspensionDialog from "@/components/overlay/dialog/CustomSuspensionDialog"; import { useIsAdmin } from "@/hooks/use-is-admin"; @@ -58,6 +59,7 @@ import isEqual from "lodash/isEqual"; import set from "lodash/set"; import type { ConfigSectionData, JsonObject } from "@/types/configForm"; import { sanitizeSectionData } from "@/utils/configUtil"; +import { isReplayCamera } from "@/utils/cameraUtil"; import type { SectionRendererProps } from "./registry"; const NOTIFICATION_SERVICE_WORKER = "/notifications-worker.js"; @@ -95,7 +97,7 @@ export default function NotificationsSettingsExtras({ return Object.values(config.cameras) .sort((aConf, bConf) => aConf.ui.order - bConf.ui.order) - .filter((c) => c.enabled_in_config); + .filter((c) => c.enabled_in_config && !isReplayCamera(c.name)); }, [config]); const notificationCameras = useMemo(() => { @@ -107,6 +109,7 @@ export default function NotificationsSettingsExtras({ .filter( (conf) => conf.enabled_in_config && + !isReplayCamera(conf.name) && conf.notifications && conf.notifications.enabled_in_config, ) @@ -360,6 +363,7 @@ export default function NotificationsSettingsExtras({ Object.values(config.cameras).some( (c) => c.enabled_in_config && + !isReplayCamera(c.name) && c.notifications && c.notifications.enabled_in_config, ), @@ -436,6 +440,12 @@ export default function NotificationsSettingsExtras({ } if (!("Notification" in window) || !window.isSecureContext) { + // iOS only exposes web push to apps installed to the Home Screen, so a + // secure-context iOS browser tab that isn't an installed PWA has no + // Notification API. Android supports web push in a normal tab, so it never + // reaches this case and keeps the generic secure-context message. + const requiresPwaInstall = isIOS && window.isSecureContext && !isPWA; + return (
    @@ -464,12 +474,21 @@ export default function NotificationsSettingsExtras({ {t("notification.notificationUnavailable.title")} - - notification.notificationUnavailable.desc - +
    -
    {isAdmin && ( @@ -519,7 +537,7 @@ export default function NotificationsSettingsExtras({ diff --git a/web/src/components/config-form/sectionExtras/registry.ts b/web/src/components/config-form/sectionExtras/registry.ts index 08e3dd86a8..26cd48dbdb 100644 --- a/web/src/components/config-form/sectionExtras/registry.ts +++ b/web/src/components/config-form/sectionExtras/registry.ts @@ -3,6 +3,7 @@ import SemanticSearchReindex from "./SemanticSearchReindex.tsx"; import CameraReviewStatusToggles from "./CameraReviewStatusToggles"; import ProxyRoleMap from "./ProxyRoleMap"; import NotificationsSettingsExtras from "./NotificationsSettingsExtras"; +import BirdseyeCameraReorder from "./BirdseyeCameraReorder"; import type { ConfigFormContext } from "@/types/configForm"; // Props that will be injected into all section renderers @@ -52,6 +53,9 @@ export const sectionRenderers: SectionRenderers = { notifications: { NotificationsSettingsExtras, }, + birdseye: { + BirdseyeCameraReorder, + }, }; export default sectionRenderers; diff --git a/web/src/components/config-form/sections/BaseSection.tsx b/web/src/components/config-form/sections/BaseSection.tsx index 6c32ffae6a..685b06ebe3 100644 --- a/web/src/components/config-form/sections/BaseSection.tsx +++ b/web/src/components/config-form/sections/BaseSection.tsx @@ -32,7 +32,7 @@ import { ProfileOverridesBadge } from "./ProfileOverridesBadge"; import { useSectionSchema } from "@/hooks/use-config-schema"; import type { FrigateConfig } from "@/types/frigateConfig"; import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { LuChevronDown, LuChevronRight } from "react-icons/lu"; import Heading from "@/components/ui/heading"; import get from "lodash/get"; @@ -86,6 +86,8 @@ import type { } from "../section-configs/types"; import { useConfigMessages } from "@/hooks/use-config-messages"; import { ConfigMessageBanner } from "../ConfigMessageBanner"; +import { FieldMessagesContext } from "../FieldMessagesContext"; +import { LiveFormDataContext } from "../LiveFormDataContext"; export interface SectionConfig { /** Field ordering within the section */ @@ -175,6 +177,9 @@ export interface BaseSectionProps { isSavingAll?: boolean; /** Callback when this section's saving state changes */ onSavingChange?: (isSaving: boolean) => void; + /** When true, render the form fields only; suppress the internal save/undo bar. + * The parent owns the save action and reads pending data via `onPendingDataChange`. */ + embedded?: boolean; } export interface CreateSectionOptions { @@ -211,6 +216,7 @@ export function ConfigSection({ onDeleteProfileSection, isSavingAll = false, onSavingChange, + embedded = false, }: ConfigSectionProps) { // For replay level, treat as camera-level config access const effectiveLevel = level === "replay" ? "camera" : level; @@ -304,11 +310,30 @@ export function ConfigSection({ // Get section schema using cached hook const sectionSchema = useSectionSchema(sectionPath, effectiveLevel); - // Apply special case handling for sections with problematic schema defaults + // Apply special case handling for sections with problematic schema defaults. + // The HiddenFieldContext is built from `config` (saved state) only — not the + // in-flight raw section value — because the schema is computed before + // rawFormData is derived. The objects-branch fallback in + // modifySchemaForSection reads `track` from fullCameraConfig / fullConfig. const modifiedSchema = useMemo( () => - modifySchemaForSection(sectionPath, level, sectionSchema ?? undefined), - [sectionPath, level, sectionSchema], + modifySchemaForSection( + sectionPath, + level, + sectionSchema ?? undefined, + config + ? { + fullConfig: config, + fullCameraConfig: + effectiveLevel === "camera" && cameraName + ? config.cameras?.[cameraName] + : undefined, + level, + cameraName, + } + : undefined, + ), + [sectionPath, level, sectionSchema, config, effectiveLevel, cameraName], ); // Get override status (camera vs global) @@ -380,7 +405,19 @@ export function ConfigSection({ // When editing a profile, hide fields that require a restart since they // cannot take effect via profile switching alone. const effectiveHiddenFields = useMemo(() => { - const base = resolveHiddenFieldEntries(sectionConfig.hiddenFields, config); + const ctx = config + ? { + fullConfig: config, + fullCameraConfig: + effectiveLevel === "camera" && cameraName + ? config.cameras?.[cameraName] + : undefined, + level, + cameraName, + formData: rawFormData, + } + : undefined; + const base = resolveHiddenFieldEntries(sectionConfig.hiddenFields, ctx); if (!profileName || !sectionConfig.restartRequired?.length) { return base; } @@ -390,6 +427,10 @@ export function ConfigSection({ sectionConfig.hiddenFields, sectionConfig.restartRequired, config, + effectiveLevel, + cameraName, + level, + rawFormData, ]); const sanitizeSectionData = useCallback( @@ -588,44 +629,6 @@ export function ConfigSection({ messageContext, ); - // Merge field-level conditional messages into uiSchema - const effectiveUiSchema = useMemo(() => { - if (activeFieldMessages.length === 0) return sectionConfig.uiSchema; - const merged = { ...(sectionConfig.uiSchema ?? {}) }; - for (const msg of activeFieldMessages) { - const segments = msg.field.split("."); - // Navigate to the nested uiSchema node, shallow-cloning along the way - let node = merged; - for (let i = 0; i < segments.length - 1; i++) { - const seg = segments[i]; - node[seg] = { ...(node[seg] as Record) }; - node = node[seg] as Record; - } - const leafKey = segments[segments.length - 1]; - const existing = node[leafKey] as Record | undefined; - const existingMessages = ((existing?.["ui:messages"] as unknown[]) ?? - []) as Array<{ - key: string; - messageKey: string; - severity: string; - position?: string; - }>; - node[leafKey] = { - ...existing, - "ui:messages": [ - ...existingMessages, - { - key: msg.key, - messageKey: msg.messageKey, - severity: msg.severity, - position: msg.position ?? "before", - }, - ], - }; - } - return merged; - }, [sectionConfig.uiSchema, activeFieldMessages]); - const currentOverrides = useMemo(() => { if (!currentFormData || typeof currentFormData !== "object") { return undefined; @@ -739,6 +742,7 @@ export function ConfigSection({ "Settings saved successfully. Restart Frigate to apply your changes.", }), { + duration: 10000, action: ( setRestartDialogOpen(true)}> - )} - {profileName && - profileOverridesSection && - !hasChanges && - !skipSave && - onDeleteProfileSection && ( - - )} +
    {hasChanges && ( +
    + + {t("unsavedChanges", { + ns: "views/settings", + defaultValue: "You have unsaved changes", + })} + + +
    + )} +
    + {((effectiveLevel === "camera" && isOverridden) || + effectiveLevel === "global") && + !hasChanges && + !skipSave && + !profileName && ( + + )} + {profileName && + profileOverridesSection && + !hasChanges && + !skipSave && + onDeleteProfileSection && ( + + )} + {hasChanges && ( + + )} - )} - +
    -
    + )} @@ -1224,7 +1236,7 @@ export function ConfigSection({ {t("button.cancel", { ns: "common" })} { onDeleteProfileSection?.(); setIsDeleteProfileDialogOpen(false); @@ -1246,12 +1258,12 @@ export function ConfigSection({
    - {isOpen ? ( - - ) : ( - - )} - {title} + + {title} + {showOverrideIndicator && effectiveLevel === "camera" && (profileOverridesSection || isOverridden) && @@ -1281,12 +1293,17 @@ export function ConfigSection({ })} )} + {isOpen ? ( + + ) : ( + + )}
    -
    {sectionContent}
    +
    {sectionContent}
    diff --git a/web/src/components/config-form/sections/CameraOverridesBadge.tsx b/web/src/components/config-form/sections/CameraOverridesBadge.tsx index 466934a770..9d3dde29d6 100644 --- a/web/src/components/config-form/sections/CameraOverridesBadge.tsx +++ b/web/src/components/config-form/sections/CameraOverridesBadge.tsx @@ -20,6 +20,7 @@ import type { ProfilesApiResponse } from "@/types/profile"; import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name"; import { formatList } from "@/utils/stringUtil"; import { + buildHiddenFieldContext, getEffectiveHiddenFields, pathMatchesHiddenPattern, } from "@/utils/configUtil"; @@ -187,7 +188,7 @@ export function CameraOverridesBadge({ sectionPath, className }: Props) { const hiddenFields = getEffectiveHiddenFields( sectionPath, "global", - config, + buildHiddenFieldContext(config, "global"), ); if (hiddenFields.length === 0) return rawEntries; return rawEntries diff --git a/web/src/components/config-form/sections/section-special-cases.ts b/web/src/components/config-form/sections/section-special-cases.ts index 62a4bfa85a..84bdaae9a9 100644 --- a/web/src/components/config-form/sections/section-special-cases.ts +++ b/web/src/components/config-form/sections/section-special-cases.ts @@ -9,7 +9,8 @@ import { RJSFSchema } from "@rjsf/utils"; import { applySchemaDefaults } from "@/lib/config-schema"; import { isJsonObject } from "@/lib/utils"; -import { JsonObject, JsonValue } from "@/types/configForm"; +import { HiddenFieldContext, JsonObject, JsonValue } from "@/types/configForm"; +import { getEffectiveAttributeLabels } from "@/utils/configUtil"; /** * Sections that require special handling at the global level. @@ -37,13 +38,28 @@ export function isSpecialCaseSection( * * - detectors: Strip the "default" field to prevent RJSF from merging the * default {"cpu": {"type": "cpu"}} with stored detector keys. + * - genai: Inject a default provider value on the additionalProperties shape. + * - objects: Promote tracked attribute labels (face, license_plate, courier + * logos) from `filters.additionalProperties` to explicit + * `filters.properties.` entries with a restricted FilterConfig + * shape, so RJSF renders just that one field for + * attribute filters. Non-attribute tracked labels (person, car, …) + * keep flowing through the unmodified `additionalProperties` and render + * the full FilterConfig form. */ export function modifySchemaForSection( sectionPath: string, level: string, schema: RJSFSchema | undefined, + ctx?: HiddenFieldContext, ): RJSFSchema | undefined { - if (!schema || !isSpecialCaseSection(sectionPath, level)) { + if (!schema) return schema; + + if (sectionPath === "objects") { + return modifyObjectsSchema(schema, ctx); + } + + if (!isSpecialCaseSection(sectionPath, level)) { return schema; } @@ -79,6 +95,175 @@ export function modifySchemaForSection( return schema; } +/** + * Build a stripped FilterConfig schema for tracked attribute filters + * (face, license_plate, etc.). Keeps only the fields meaningful for + * attribute detections — `min_score`, `min_area`, `max_area`. `threshold` + * and the ratio fields aren't exposed: attributes don't flow through + * `_is_false_positive` (no median-of-history check), and aspect-ratio + * filtering isn't a typical attribute-tuning knob. + * + * `min_area` and `max_area` are `Union[int, float]` in Pydantic which + * emits as `anyOf` in JSON schema; we flatten to a plain `number` so RJSF + * doesn't render the int/float type-selector dropdown for each attribute + * filter. The backend still accepts either int (pixels) or float + * (percentage) since the underlying FilterConfig union is unchanged. + */ +function buildAttributeFilterSchema( + filterConfigSchema: RJSFSchema, + attributeLabel: string, +): RJSFSchema { + const props = isJsonObject( + (filterConfigSchema as { properties?: unknown }).properties, + ) + ? (filterConfigSchema as { properties: Record }) + .properties + : undefined; + + const minScoreSchema = + props && props.min_score ? props.min_score : { type: "number" }; + + const flattenToNumber = (src: RJSFSchema | undefined): RJSFSchema => { + if (!src) return { type: "number" }; + const { anyOf: _anyOf, ...rest } = src as { + anyOf?: unknown; + [k: string]: unknown; + }; + return { ...rest, type: "number" } as RJSFSchema; + }; + + return { + type: "object", + title: attributeLabel, + properties: { + min_score: minScoreSchema, + min_area: flattenToNumber(props && props.min_area), + max_area: flattenToNumber(props && props.max_area), + }, + additionalProperties: false, + } as RJSFSchema; +} + +function modifyObjectsSchema( + schema: RJSFSchema, + ctx: HiddenFieldContext | undefined, +): RJSFSchema { + if (!ctx) return schema; + + const allAttributes = getEffectiveAttributeLabels( + ctx.fullConfig, + ctx.fullCameraConfig, + ctx.level, + ); + + // Resolve effective track at this scope, falling back through camera + // config then global config (matches hideAttributeFilters in objects.ts). + const trackFromForm = Array.isArray( + (ctx.formData as { track?: unknown } | undefined)?.track, + ) + ? (ctx.formData as { track: string[] }).track + : undefined; + const track = + trackFromForm ?? + (ctx.level !== "global" + ? ctx.fullCameraConfig?.objects?.track + : undefined) ?? + ctx.fullConfig.objects?.track ?? + []; + + // Also promote any label that has a saved filter entry but isn't in + // `track` (e.g. the user toggled an object off but left a customized + // filter in YAML). Without this, RJSF falls back to the additional- + // properties Key/Value editor for those orphans. + const filtersSaved = + (ctx.level !== "global" + ? ctx.fullCameraConfig?.objects?.filters + : undefined) ?? + ctx.fullConfig.objects?.filters ?? + {}; + + if (track.length === 0 && Object.keys(filtersSaved).length === 0) { + return schema; + } + + const schemaProperties = isJsonObject( + (schema as { properties?: unknown }).properties, + ) + ? (schema as { properties: Record }).properties + : undefined; + const filtersSchema = + schemaProperties && schemaProperties.filters + ? schemaProperties.filters + : undefined; + if (!filtersSchema) return schema; + + const filterEntrySchema = isJsonObject( + (filtersSchema as { additionalProperties?: unknown }).additionalProperties, + ) + ? (filtersSchema as { additionalProperties: RJSFSchema }) + .additionalProperties + : undefined; + if (!filterEntrySchema) return schema; + + const attributeSet = new Set(allAttributes); + const existingProperties = isJsonObject( + (filtersSchema as { properties?: unknown }).properties, + ) + ? (filtersSchema as { properties: Record }).properties + : {}; + + // Promote every tracked label (and any orphaned filter entry) to an + // explicit property entry so RJSF renders it as a normal collapsible + // (no additionalProperties key/value editor UI). Attribute labels get a + // restricted shape with only `min_score`/`min_area`/`max_area`; + // non-attribute labels get the full FilterConfig. Sorted alphabetically + // so the filter collapsibles match the order of the sibling `track` + // switches. + const labelsToPromote = new Set(); + for (const label of track) { + if (typeof label === "string") labelsToPromote.add(label); + } + for (const key of Object.keys(filtersSaved)) { + // Skip attribute labels that aren't tracked — those are hidden + // entirely via hideAttributeFilters; promoting them would surface a + // collapsible we then have to hide separately. + if (attributeSet.has(key) && !labelsToPromote.has(key)) continue; + labelsToPromote.add(key); + } + const sortedTrackedLabels = [...labelsToPromote].sort((a, b) => + a.localeCompare(b), + ); + const updatedFilterProperties: Record = { + ...existingProperties, + }; + for (const label of sortedTrackedLabels) { + if (attributeSet.has(label)) { + updatedFilterProperties[label] = buildAttributeFilterSchema( + filterEntrySchema, + label, + ); + } else { + updatedFilterProperties[label] = { + ...filterEntrySchema, + title: label, + } as RJSFSchema; + } + } + + const updatedFiltersSchema: RJSFSchema = { + ...filtersSchema, + properties: updatedFilterProperties, + }; + + return { + ...schema, + properties: { + ...schemaProperties, + filters: updatedFiltersSchema, + }, + }; +} + /** * Get effective defaults for sections with special schema patterns. * diff --git a/web/src/components/config-form/theme/fields/LiveStreamsField.tsx b/web/src/components/config-form/theme/fields/LiveStreamsField.tsx new file mode 100644 index 0000000000..b74547ba1b --- /dev/null +++ b/web/src/components/config-form/theme/fields/LiveStreamsField.tsx @@ -0,0 +1,346 @@ +import type { FieldPathList, FieldProps, RJSFSchema } from "@rjsf/utils"; +import { useCallback, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Command, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { cn } from "@/lib/utils"; +import { Check, ChevronsUpDown, Plus } from "lucide-react"; +import { LuPlus, LuTrash2 } from "react-icons/lu"; +import type { ConfigFormContext } from "@/types/configForm"; +import get from "lodash/get"; +import { isSubtreeModified } from "../utils"; + +type LiveStreamsData = Record; + +type StreamValueComboboxProps = { + id: string; + value: string; + options: string[]; + disabled?: boolean; + readonly?: boolean; + onChange: (next: string) => void; +}; + +function StreamValueCombobox({ + id, + value, + options, + disabled, + readonly, + onChange, +}: StreamValueComboboxProps) { + const { t } = useTranslation(["views/settings", "common"]); + const [open, setOpen] = useState(false); + const [searchValue, setSearchValue] = useState(""); + + const trimmedSearch = searchValue.trim(); + const matchesOption = useMemo( + () => options.some((o) => o.toLowerCase() === trimmedSearch.toLowerCase()), + [options, trimmedSearch], + ); + const showCustomOption = trimmedSearch.length > 0 && !matchesOption; + + const commit = (next: string) => { + onChange(next); + setSearchValue(""); + setOpen(false); + }; + + const placeholder = t("configForm.liveStreams.go2rtcStreamPlaceholder", { + ns: "views/settings", + }); + const searchPlaceholder = t("configForm.liveStreams.go2rtcStreamSearch", { + ns: "views/settings", + }); + const noStreams = t("configForm.liveStreams.noGo2rtcStreams", { + ns: "views/settings", + }); + const availableHeading = t("configForm.liveStreams.availableStreams", { + ns: "views/settings", + }); + + return ( + { + setOpen(next); + if (!next) setSearchValue(""); + }} + > + + + + + + { + if (e.key === "Enter" && showCustomOption) { + e.preventDefault(); + commit(trimmedSearch); + } + }} + /> + + {showCustomOption && ( + + commit(trimmedSearch)} + > + + {t("configForm.liveStreams.useCustom", { + ns: "views/settings", + value: trimmedSearch, + })} + + + )} + {options.length > 0 ? ( + + {options.map((option) => ( + commit(option)} + > + + {option} + + ))} + + ) : !showCustomOption ? ( +
    + {noStreams} +
    + ) : null} +
    +
    +
    +
    + ); +} + +export function LiveStreamsField(props: FieldProps) { + const { schema, formData, onChange, idSchema, disabled, readonly } = props; + const formContext = props.registry?.formContext as + | ConfigFormContext + | undefined; + + const configNamespace = + formContext?.i18nNamespace ?? + (formContext?.level === "camera" ? "config/cameras" : "config/global"); + const { t: fallbackT } = useTranslation(["common", configNamespace]); + const t = formContext?.t ?? fallbackT; + + const data: LiveStreamsData = useMemo(() => { + if (!formData || typeof formData !== "object" || Array.isArray(formData)) { + return {}; + } + return formData as LiveStreamsData; + }, [formData]); + + const entries = useMemo(() => Object.entries(data), [data]); + + const id = idSchema?.$id ?? props.name; + const sectionPrefix = formContext?.sectionI18nPrefix; + + const title = + t(`${sectionPrefix}.${id}.label`) ?? (schema as RJSFSchema).title; + const description = + t(`${sectionPrefix}.${id}.description`) ?? + (schema as RJSFSchema).description; + + const go2rtcStreamNames = useMemo(() => { + const streams = formContext?.fullConfig?.go2rtc?.streams; + if (!streams || typeof streams !== "object") return []; + return Object.keys(streams).sort(); + }, [formContext?.fullConfig?.go2rtc?.streams]); + + const emptyPath = useMemo(() => [] as FieldPathList, []); + const fieldPath = + (props as { fieldPathId?: { path?: FieldPathList } }).fieldPathId?.path ?? + emptyPath; + + const isModified = useMemo(() => { + const baselineRoot = formContext?.baselineFormData; + const baselineValue = baselineRoot + ? get(baselineRoot, fieldPath) + : undefined; + return isSubtreeModified( + data, + baselineValue, + formContext?.overrides, + fieldPath, + formContext?.formData, + ); + }, [fieldPath, formContext, data]); + + const handleAddEntry = useCallback(() => { + const next = { ...data, "": "" }; + onChange(next, fieldPath); + }, [data, fieldPath, onChange]); + + const handleRemoveEntry = useCallback( + (key: string) => { + const next = { ...data }; + delete next[key]; + onChange(next, fieldPath); + }, + [data, fieldPath, onChange], + ); + + const handleRenameKey = useCallback( + (oldKey: string, newKey: string) => { + if (oldKey === newKey) return; + const next: LiveStreamsData = {}; + for (const [k, v] of Object.entries(data)) { + if (k === oldKey) { + next[newKey] = v; + } else { + next[k] = v; + } + } + onChange(next, fieldPath); + }, + [data, fieldPath, onChange], + ); + + const handleUpdateValue = useCallback( + (key: string, value: string) => { + const next = { ...data, [key]: value }; + onChange(next, fieldPath); + }, + [data, fieldPath, onChange], + ); + + const baseId = idSchema?.$id || "live_streams"; + const deleteLabel = t("button.delete", { + ns: "common", + defaultValue: "Delete", + }); + const streamNameLabel = t("configForm.liveStreams.streamNameLabel", { + ns: "views/settings", + }); + const streamNamePlaceholder = t( + "configForm.liveStreams.streamNamePlaceholder", + { ns: "views/settings" }, + ); + const go2rtcStreamLabel = t("configForm.liveStreams.go2rtcStreamLabel", { + ns: "views/settings", + }); + const addStreamLabel = t("configForm.liveStreams.addStream", { + ns: "views/settings", + }); + + return ( + + + + {title} + + {description && ( +

    {description}

    + )} +
    + + {entries.map(([key, value], entryIndex) => { + const entryId = `${baseId}-${entryIndex}`; + return ( +
    +
    + + handleRenameKey(key, e.target.value)} + /> +
    +
    + + handleUpdateValue(key, next)} + /> +
    +
    + +
    +
    + ); + })} + +
    + +
    +
    +
    + ); +} + +export default LiveStreamsField; diff --git a/web/src/components/config-form/theme/fields/index.ts b/web/src/components/config-form/theme/fields/index.ts index b6b7078661..4bb91ed352 100644 --- a/web/src/components/config-form/theme/fields/index.ts +++ b/web/src/components/config-form/theme/fields/index.ts @@ -2,3 +2,4 @@ export { LayoutGridField } from "./LayoutGridField"; export { DetectorHardwareField } from "./DetectorHardwareField"; export { ReplaceRulesField } from "./ReplaceRulesField"; +export { LiveStreamsField } from "./LiveStreamsField"; diff --git a/web/src/components/config-form/theme/frigateTheme.ts b/web/src/components/config-form/theme/frigateTheme.ts index ae612d9ac7..109c44eb9a 100644 --- a/web/src/components/config-form/theme/frigateTheme.ts +++ b/web/src/components/config-form/theme/frigateTheme.ts @@ -31,7 +31,10 @@ import { TimezoneSelectWidget } from "./widgets/TimezoneSelectWidget"; import { CameraPathWidget } from "./widgets/CameraPathWidget"; import { OptionalFieldWidget } from "./widgets/OptionalFieldWidget"; import { SemanticSearchModelWidget } from "./widgets/SemanticSearchModelWidget"; +import { SemanticSearchModelSizeWidget } from "./widgets/SemanticSearchModelSizeWidget"; import { OnvifProfileWidget } from "./widgets/OnvifProfileWidget"; +import { PTZPresetsWidget } from "./widgets/PTZPresetsWidget"; +import { DefaultRoleWidget } from "./widgets/DefaultRoleWidget"; import { FieldTemplate } from "./templates/FieldTemplate"; import { ObjectFieldTemplate } from "./templates/ObjectFieldTemplate"; @@ -50,6 +53,7 @@ import { ReplaceRulesField } from "./fields/ReplaceRulesField"; import { CameraInputsField } from "./fields/CameraInputsField"; import { DictAsYamlField } from "./fields/DictAsYamlField"; import { KnownPlatesField } from "./fields/KnownPlatesField"; +import { LiveStreamsField } from "./fields/LiveStreamsField"; export interface FrigateTheme { widgets: RegistryWidgetsType; @@ -86,7 +90,10 @@ export const frigateTheme: FrigateTheme = { timezoneSelect: TimezoneSelectWidget, optionalField: OptionalFieldWidget, semanticSearchModel: SemanticSearchModelWidget, + semanticSearchModelSize: SemanticSearchModelSizeWidget, onvifProfile: OnvifProfileWidget, + ptzPresets: PTZPresetsWidget, + defaultRole: DefaultRoleWidget, }, templates: { FieldTemplate: FieldTemplate as React.ComponentType, @@ -107,5 +114,6 @@ export const frigateTheme: FrigateTheme = { CameraInputsField: CameraInputsField, DictAsYamlField: DictAsYamlField, KnownPlatesField: KnownPlatesField, + LiveStreamsField: LiveStreamsField, }, }; diff --git a/web/src/components/config-form/theme/templates/FieldTemplate.tsx b/web/src/components/config-form/theme/templates/FieldTemplate.tsx index 1c5e801dc3..2ee91ae033 100644 --- a/web/src/components/config-form/theme/templates/FieldTemplate.tsx +++ b/web/src/components/config-form/theme/templates/FieldTemplate.tsx @@ -5,8 +5,9 @@ import { getUiOptions, ADDITIONAL_PROPERTY_FLAG, } from "@rjsf/utils"; -import { ComponentType, ReactNode } from "react"; +import { ComponentType, ReactNode, useContext } from "react"; import { isValidElement } from "react"; +import { FieldMessagesContext } from "../../FieldMessagesContext"; import { Label } from "@/components/ui/label"; import { cn } from "@/lib/utils"; import { useTranslation } from "react-i18next"; @@ -95,6 +96,7 @@ export function FieldTemplate(props: FieldTemplateProps) { "views/settings", ]); const { getLocaleDocUrl } = useDocDomain(); + const allFieldMessages = useContext(FieldMessagesContext); if (hidden) { return
    {children}
    ; @@ -384,21 +386,15 @@ export function FieldTemplate(props: FieldTemplateProps) { const beforeContent = renderCustom(beforeSpec); const afterContent = renderCustom(afterSpec); - // Render conditional field messages from ui:messages - const fieldMessageSpecs = uiSchema?.["ui:messages"] as - | Array<{ - key: string; - messageKey: string; - severity: string; - position?: string; - }> - | undefined; - const beforeMessages = fieldMessageSpecs?.filter( + // Read field-level conditional messages from FieldMessagesContext + const fieldPathStr = pathSegments.join("."); + const fieldMessageSpecs = allFieldMessages.filter( + (m) => m.field === fieldPathStr, + ); + const beforeMessages = fieldMessageSpecs.filter( (m) => (m.position ?? "before") === "before", ); - const afterMessages = fieldMessageSpecs?.filter( - (m) => m.position === "after", - ); + const afterMessages = fieldMessageSpecs.filter((m) => m.position === "after"); const beforeMessagesContent = beforeMessages && beforeMessages.length > 0 ? (
    diff --git a/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx b/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx index bdd61eb1ff..9d703b3c8d 100644 --- a/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx +++ b/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx @@ -371,7 +371,7 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) { key={group.groupKey} className="space-y-4 rounded-lg border border-border/70 bg-card/30 p-4" > -
    +
    {group.label}
    diff --git a/web/src/components/config-form/theme/utils/i18n.ts b/web/src/components/config-form/theme/utils/i18n.ts index 5a27020655..a5f7ea1527 100644 --- a/web/src/components/config-form/theme/utils/i18n.ts +++ b/web/src/components/config-form/theme/utils/i18n.ts @@ -6,6 +6,7 @@ */ import type { ConfigFormContext } from "@/types/configForm"; +import { getEffectiveAttributeLabels } from "@/utils/configUtil"; const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null; @@ -70,12 +71,27 @@ export function buildTranslationPath( (segment): segment is string => typeof segment === "string", ); - // Handle filters section - skip the dynamic filter object name - // Example: filters.person.threshold -> filters.threshold + // Handle filters section - skip the dynamic filter object name. Route + // to `filters_attribute.` when the dynamic key is an attribute + // label (face, license_plate, courier logos) so attribute filter fields + // pick up the attribute-worded translations emitted by + // generate_config_translations.py. + // Example: filters.person.threshold -> filters.threshold + // Example: filters.face.min_area -> filters_attribute.min_area const filtersIndex = stringSegments.indexOf("filters"); if (filtersIndex !== -1 && stringSegments.length > filtersIndex + 2) { + const filterKey = stringSegments[filtersIndex + 1]; + const allAttributes = getEffectiveAttributeLabels( + formContext?.fullConfig, + formContext?.fullCameraConfig, + formContext?.level, + ); + const sectionWord = allAttributes.includes(filterKey) + ? "filters_attribute" + : "filters"; const normalized = [ - ...stringSegments.slice(0, filtersIndex + 1), + ...stringSegments.slice(0, filtersIndex), + sectionWord, ...stringSegments.slice(filtersIndex + 2), ]; return normalized.join("."); diff --git a/web/src/components/config-form/theme/widgets/ArrayAsTextWidget.tsx b/web/src/components/config-form/theme/widgets/ArrayAsTextWidget.tsx index 21bba78cca..2f32de6453 100644 --- a/web/src/components/config-form/theme/widgets/ArrayAsTextWidget.tsx +++ b/web/src/components/config-form/theme/widgets/ArrayAsTextWidget.tsx @@ -79,7 +79,7 @@ export function ArrayAsTextWidget(props: WidgetProps) { return (