Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
5bd109b29a
Merge 05a357c71d into 2a9c028f55 2025-11-24 12:13:22 -05:00
236 changed files with 961 additions and 4541 deletions

View File

@ -81,5 +81,3 @@ librosa==0.11.*
soundfile==0.13.*
# DeGirum detector
degirum == 0.16.*
# Memory profiling
memray == 1.15.*

View File

@ -2,7 +2,7 @@ variable "AMDGPU" {
default = "gfx900"
}
variable "ROCM" {
default = "7.1.0"
default = "7.1"
}
variable "HSA_OVERRIDE_GFX_VERSION" {
default = ""

View File

@ -75,13 +75,7 @@ audio:
### Audio Transcription
Frigate supports fully local audio transcription using either `sherpa-onnx` or OpenAIs open-source Whisper models via `faster-whisper`. The goal of this feature is to support Semantic Search for `speech` audio events. Frigate is not intended to act as a continuous, fully-automatic speech transcription service — automatically transcribing all speech (or queuing many audio events for transcription) requires substantial CPU (or GPU) resources and is impractical on most systems. For this reason, transcriptions for events are initiated manually from the UI or the API rather than being run continuously in the background.
Transcription accuracy also depends heavily on the quality of your camera's microphone and recording conditions. Many cameras use inexpensive microphones, and distance to the speaker, low audio bitrate, or background noise can significantly reduce transcription quality. If you need higher accuracy, more robust long-running queues, or large-scale automatic transcription, consider using the HTTP API in combination with an automation platform and a cloud transcription service.
#### Configuration
To enable transcription, enable it in your config. Note that audio detection must also be enabled as described above in order to use audio transcription features.
Frigate supports fully local audio transcription using either `sherpa-onnx` or OpenAIs open-source Whisper models via `faster-whisper`. To enable transcription, enable it in your config. Note that audio detection must also be enabled as described above in order to use audio transcription features.
```yaml
audio_transcription:

View File

@ -232,7 +232,7 @@ When your browser runs into problems playing back your camera streams, it will l
- **mse-decode**
- What it means: The browser reported a decoding error while trying to play the stream, which usually is a result of a codec incompatibility or corrupted frames.
- What to try: Check the browser console for the supported and negotiated codecs. Ensure your camera/restream is using H.264 video and AAC audio (these are the most compatible). If your camera uses a non-standard audio codec, configure `go2rtc` to transcode the stream to AAC. Try another browser (some browsers have stricter MSE/codec support) and, for iPhone, ensure you're on iOS 17.1 or newer.
- What to try: Ensure your camera/restream is using H.264 video and AAC audio (these are the most compatible). If your camera uses a non-standard audio codec, configure `go2rtc` to transcode the stream to AAC. Try another browser (some browsers have stricter MSE/codec support) and, for iPhone, ensure you're on iOS 17.1 or newer.
- Possible console messages from the player code:

View File

@ -1,129 +0,0 @@
---
id: memory
title: Memory Troubleshooting
---
Frigate includes built-in memory profiling using [memray](https://bloomberg.github.io/memray/) to help diagnose memory issues. This feature allows you to profile specific Frigate modules to identify memory leaks, excessive allocations, or other memory-related problems.
## Enabling Memory Profiling
Memory profiling is controlled via the `FRIGATE_MEMRAY_MODULES` environment variable. Set it to a comma-separated list of module names you want to profile:
```bash
export FRIGATE_MEMRAY_MODULES="frigate.review_segment_manager,frigate.capture"
```
### Module Names
Frigate processes are named using a module-based naming scheme. Common module names include:
- `frigate.review_segment_manager` - Review segment processing
- `frigate.recording_manager` - Recording management
- `frigate.capture` - Camera capture processes (all cameras with this module name)
- `frigate.process` - Camera processing/tracking (all cameras with this module name)
- `frigate.output` - Output processing
- `frigate.audio_manager` - Audio processing
- `frigate.embeddings` - Embeddings processing
You can also specify the full process name (including camera-specific identifiers) if you want to profile a specific camera:
```bash
export FRIGATE_MEMRAY_MODULES="frigate.capture:front_door"
```
When you specify a module name (e.g., `frigate.capture`), all processes with that module prefix will be profiled. For example, `frigate.capture` will profile all camera capture processes.
## How It Works
1. **Binary File Creation**: When profiling is enabled, memray creates a binary file (`.bin`) in `/config/memray_reports/` that is updated continuously in real-time as the process runs.
2. **Automatic HTML Generation**: On normal process exit, Frigate automatically:
- Stops memray tracking
- Generates an HTML flamegraph report
- Saves it to `/config/memray_reports/<module_name>.html`
3. **Crash Recovery**: If a process crashes (SIGKILL, segfault, etc.), the binary file is preserved with all data up to the crash point. You can manually generate the HTML report from the binary file.
## Viewing Reports
### Automatic Reports
After a process exits normally, you'll find HTML reports in `/config/memray_reports/`. Open these files in a web browser to view interactive flamegraphs showing memory usage patterns.
### Manual Report Generation
If a process crashes or you want to generate a report from an existing binary file, you can manually create the HTML report:
```bash
memray flamegraph /config/memray_reports/<module_name>.bin
```
This will generate an HTML file that you can open in your browser.
## Understanding the Reports
Memray flamegraphs show:
- **Memory allocations over time**: See where memory is being allocated in your code
- **Call stacks**: Understand the full call chain leading to allocations
- **Memory hotspots**: Identify functions or code paths that allocate the most memory
- **Memory leaks**: Spot patterns where memory is allocated but not freed
The interactive HTML reports allow you to:
- Zoom into specific time ranges
- Filter by function names
- View detailed allocation information
- Export data for further analysis
## Best Practices
1. **Profile During Issues**: Enable profiling when you're experiencing memory issues, not all the time, as it adds some overhead.
2. **Profile Specific Modules**: Instead of profiling everything, focus on the modules you suspect are causing issues.
3. **Let Processes Run**: Allow processes to run for a meaningful duration to capture representative memory usage patterns.
4. **Check Binary Files**: If HTML reports aren't generated automatically (e.g., after a crash), check for `.bin` files in `/config/memray_reports/` and generate reports manually.
5. **Compare Reports**: Generate reports at different times to compare memory usage patterns and identify trends.
## Troubleshooting
### No Reports Generated
- Check that the environment variable is set correctly
- Verify the module name matches exactly (case-sensitive)
- Check logs for memray-related errors
- Ensure `/config/memray_reports/` directory exists and is writable
### Process Crashed Before Report Generation
- Look for `.bin` files in `/config/memray_reports/`
- Manually generate HTML reports using: `memray flamegraph <file>.bin`
- The binary file contains all data up to the crash point
### Reports Show No Data
- Ensure the process ran long enough to generate meaningful data
- Check that memray is properly installed (included by default in Frigate)
- Verify the process actually started and ran (check process logs)
## Example Usage
```bash
# Enable profiling for review and capture modules
export FRIGATE_MEMRAY_MODULES="frigate.review_segment_manager,frigate.capture"
# Start Frigate
# ... let it run for a while ...
# Check for reports
ls -lh /config/memray_reports/
# If a process crashed, manually generate report
memray flamegraph /config/memray_reports/frigate_capture_front_door.bin
```
For more information about memray and interpreting reports, see the [official memray documentation](https://bloomberg.github.io/memray/).

View File

@ -131,7 +131,6 @@ const sidebars: SidebarsConfig = {
"troubleshooting/recordings",
"troubleshooting/gpu",
"troubleshooting/edgetpu",
"troubleshooting/memory",
],
Development: [
"development/contributing",

View File

@ -23,7 +23,7 @@ from markupsafe import escape
from peewee import SQL, fn, operator
from pydantic import ValidationError
from frigate.api.auth import allow_any_authenticated, allow_public, require_role
from frigate.api.auth import require_role
from frigate.api.defs.query.app_query_parameters import AppTimelineHourlyQueryParameters
from frigate.api.defs.request.app_body import AppConfigSetBody
from frigate.api.defs.tags import Tags
@ -56,33 +56,29 @@ logger = logging.getLogger(__name__)
router = APIRouter(tags=[Tags.app])
@router.get(
"/", response_class=PlainTextResponse, dependencies=[Depends(allow_public())]
)
@router.get("/", response_class=PlainTextResponse)
def is_healthy():
return "Frigate is running. Alive and healthy!"
@router.get("/config/schema.json", dependencies=[Depends(allow_public())])
@router.get("/config/schema.json")
def config_schema(request: Request):
return Response(
content=request.app.frigate_config.schema_json(), media_type="application/json"
)
@router.get(
"/version", response_class=PlainTextResponse, dependencies=[Depends(allow_public())]
)
@router.get("/version", response_class=PlainTextResponse)
def version():
return VERSION
@router.get("/stats", dependencies=[Depends(allow_any_authenticated())])
@router.get("/stats")
def stats(request: Request):
return JSONResponse(content=request.app.stats_emitter.get_latest_stats())
@router.get("/stats/history", dependencies=[Depends(allow_any_authenticated())])
@router.get("/stats/history")
def stats_history(request: Request, keys: str = None):
if keys:
keys = keys.split(",")
@ -90,7 +86,7 @@ def stats_history(request: Request, keys: str = None):
return JSONResponse(content=request.app.stats_emitter.get_stats_history(keys))
@router.get("/metrics", dependencies=[Depends(allow_any_authenticated())])
@router.get("/metrics")
def metrics(request: Request):
"""Expose Prometheus metrics endpoint and update metrics with latest stats"""
# Retrieve the latest statistics and update the Prometheus metrics
@ -107,7 +103,7 @@ def metrics(request: Request):
return Response(content=content, media_type=content_type)
@router.get("/config", dependencies=[Depends(allow_any_authenticated())])
@router.get("/config")
def config(request: Request):
config_obj: FrigateConfig = request.app.frigate_config
config: dict[str, dict[str, Any]] = config_obj.model_dump(
@ -213,7 +209,7 @@ def config_raw_paths(request: Request):
return JSONResponse(content=raw_paths)
@router.get("/config/raw", dependencies=[Depends(allow_any_authenticated())])
@router.get("/config/raw")
def config_raw():
config_file = find_config_file()
@ -456,7 +452,7 @@ def config_set(request: Request, body: AppConfigSetBody):
)
@router.get("/vainfo", dependencies=[Depends(allow_any_authenticated())])
@router.get("/vainfo")
def vainfo():
vainfo = vainfo_hwaccel()
return JSONResponse(
@ -476,16 +472,12 @@ def vainfo():
)
@router.get("/nvinfo", dependencies=[Depends(allow_any_authenticated())])
@router.get("/nvinfo")
def nvinfo():
return JSONResponse(content=get_nvidia_driver_info())
@router.get(
"/logs/{service}",
tags=[Tags.logs],
dependencies=[Depends(allow_any_authenticated())],
)
@router.get("/logs/{service}", tags=[Tags.logs])
async def logs(
service: str = Path(enum=["frigate", "nginx", "go2rtc"]),
download: Optional[str] = None,
@ -593,7 +585,7 @@ def restart():
)
@router.get("/labels", dependencies=[Depends(allow_any_authenticated())])
@router.get("/labels")
def get_labels(camera: str = ""):
try:
if camera:
@ -611,7 +603,7 @@ def get_labels(camera: str = ""):
return JSONResponse(content=labels)
@router.get("/sub_labels", dependencies=[Depends(allow_any_authenticated())])
@router.get("/sub_labels")
def get_sub_labels(split_joined: Optional[int] = None):
try:
events = Event.select(Event.sub_label).distinct()
@ -642,7 +634,7 @@ def get_sub_labels(split_joined: Optional[int] = None):
return JSONResponse(content=sub_labels)
@router.get("/plus/models", dependencies=[Depends(allow_any_authenticated())])
@router.get("/plus/models")
def plusModels(request: Request, filterByCurrentModelDetector: bool = False):
if not request.app.frigate_config.plus_api.is_active():
return JSONResponse(
@ -684,9 +676,7 @@ def plusModels(request: Request, filterByCurrentModelDetector: bool = False):
return JSONResponse(content=validModels)
@router.get(
"/recognized_license_plates", dependencies=[Depends(allow_any_authenticated())]
)
@router.get("/recognized_license_plates")
def get_recognized_license_plates(split_joined: Optional[int] = None):
try:
query = (
@ -720,7 +710,7 @@ def get_recognized_license_plates(split_joined: Optional[int] = None):
return JSONResponse(content=recognized_license_plates)
@router.get("/timeline", dependencies=[Depends(allow_any_authenticated())])
@router.get("/timeline")
def timeline(camera: str = "all", limit: int = 100, source_id: Optional[str] = None):
clauses = []
@ -757,7 +747,7 @@ def timeline(camera: str = "all", limit: int = 100, source_id: Optional[str] = N
return JSONResponse(content=[t for t in timeline])
@router.get("/timeline/hourly", dependencies=[Depends(allow_any_authenticated())])
@router.get("/timeline/hourly")
def hourly_timeline(params: AppTimelineHourlyQueryParameters = Depends()):
"""Get hourly summary for timeline."""
cameras = params.cameras

View File

@ -32,154 +32,10 @@ from frigate.models import User
logger = logging.getLogger(__name__)
def require_admin_by_default():
"""
Global admin requirement dependency for all endpoints by default.
This is set as the default dependency on the FastAPI app to ensure all
endpoints require admin access unless explicitly overridden with
allow_public(), allow_any_authenticated(), or require_role().
Port 5000 (internal) always has admin role set by the /auth endpoint,
so this check passes automatically for internal requests.
Certain paths are exempted from the global admin check because they must
be accessible before authentication (login, auth) or they have their own
route-level authorization dependencies that handle access control.
"""
# Paths that have route-level auth dependencies and should bypass global admin check
# These paths still have authorization - it's handled by their route-level dependencies
EXEMPT_PATHS = {
# Public auth endpoints (allow_public)
"/auth",
"/auth/first_time_login",
"/login",
# Authenticated user endpoints (allow_any_authenticated)
"/logout",
"/profile",
# Public info endpoints (allow_public)
"/",
"/version",
"/config/schema.json",
"/metrics",
# Authenticated user endpoints (allow_any_authenticated)
"/stats",
"/stats/history",
"/config",
"/config/raw",
"/vainfo",
"/nvinfo",
"/labels",
"/sub_labels",
"/plus/models",
"/recognized_license_plates",
"/timeline",
"/timeline/hourly",
"/events/summary",
"/recordings/storage",
"/recordings/summary",
"/recordings/unavailable",
"/go2rtc/streams",
}
# Path prefixes that should be exempt (for paths with parameters)
EXEMPT_PREFIXES = (
"/logs/", # /logs/{service}
"/review", # /review, /review/{id}, /review_ids, /review/summary, etc.
"/reviews/", # /reviews/viewed, /reviews/delete
"/events/", # /events/{id}/thumbnail, etc. (camera-scoped)
"/go2rtc/streams/", # /go2rtc/streams/{camera}
"/users/", # /users/{username}/password (has own auth)
"/preview/", # /preview/{file}/thumbnail.jpg
)
async def admin_checker(request: Request):
path = request.url.path
# Check exact path matches
if path in EXEMPT_PATHS:
return
# Check prefix matches for parameterized paths
if path.startswith(EXEMPT_PREFIXES):
return
# For all other paths, require admin role
# Port 5000 (internal) requests have admin role set automatically
role = request.headers.get("remote-role")
if role == "admin":
return
raise HTTPException(
status_code=403,
detail="Admin role required for this endpoint",
)
return admin_checker
def _is_authenticated(request: Request) -> bool:
"""
Helper to determine if a request is from an authenticated user.
Returns True if the request has a valid authenticated user (not anonymous).
Port 5000 internal requests are considered anonymous despite having admin role.
"""
username = request.headers.get("remote-user")
return username is not None and username != "anonymous"
def allow_public():
"""
Override dependency to allow unauthenticated access to an endpoint.
Use this for endpoints that should be publicly accessible without
authentication, such as login page, health checks, or pre-auth info.
Example:
@router.get("/public-endpoint", dependencies=[Depends(allow_public())])
"""
async def public_checker(request: Request):
return # Always allow
return public_checker
def allow_any_authenticated():
"""
Override dependency to allow any authenticated user (bypass admin requirement).
Allows:
- Port 5000 internal requests (have admin role despite anonymous user)
- Any authenticated user with a real username (not "anonymous")
Rejects:
- Port 8971 requests with anonymous user (auth disabled, no proxy auth)
Example:
@router.get("/authenticated-endpoint", dependencies=[Depends(allow_any_authenticated())])
"""
async def auth_checker(request: Request):
# Port 5000 requests have admin role and should be allowed
role = request.headers.get("remote-role")
if role == "admin":
return
# Otherwise require a real authenticated user (not anonymous)
if not _is_authenticated(request):
raise HTTPException(status_code=401, detail="Authentication required")
return
return auth_checker
router = APIRouter(tags=[Tags.auth])
@router.get("/auth/first_time_login", dependencies=[Depends(allow_public())])
@router.get("/auth/first_time_login")
def first_time_login(request: Request):
"""Return whether the admin first-time login help flag is set in config.
@ -496,7 +352,7 @@ def resolve_role(
# Endpoints
@router.get("/auth", dependencies=[Depends(allow_public())])
@router.get("/auth")
def auth(request: Request):
auth_config: AuthConfig = request.app.frigate_config.auth
proxy_config: ProxyConfig = request.app.frigate_config.proxy
@ -622,7 +478,7 @@ def auth(request: Request):
return fail_response
@router.get("/profile", dependencies=[Depends(allow_any_authenticated())])
@router.get("/profile")
def profile(request: Request):
username = request.headers.get("remote-user", "anonymous")
role = request.headers.get("remote-role", "viewer")
@ -636,7 +492,7 @@ def profile(request: Request):
)
@router.get("/logout", dependencies=[Depends(allow_any_authenticated())])
@router.get("/logout")
def logout(request: Request):
auth_config: AuthConfig = request.app.frigate_config.auth
response = RedirectResponse("/login", status_code=303)
@ -647,7 +503,7 @@ def logout(request: Request):
limiter = Limiter(key_func=get_remote_addr)
@router.post("/login", dependencies=[Depends(allow_public())])
@router.post("/login")
@limiter.limit(limit_value=rateLimiter.get_limit)
def login(request: Request, body: AppPostLoginBody):
JWT_COOKIE_NAME = request.app.frigate_config.auth.cookie_name
@ -722,21 +578,13 @@ def create_user(
return JSONResponse(content={"username": body.username})
@router.delete("/users/{username}", dependencies=[Depends(require_role(["admin"]))])
def delete_user(request: Request, username: str):
# Prevent deletion of the built-in admin user
if username == "admin":
return JSONResponse(
content={"message": "Cannot delete admin user"}, status_code=403
)
@router.delete("/users/{username}")
def delete_user(username: str):
User.delete_by_id(username)
return JSONResponse(content={"success": True})
@router.put(
"/users/{username}/password", dependencies=[Depends(allow_any_authenticated())]
)
@router.put("/users/{username}/password")
async def update_password(
request: Request,
username: str,

View File

@ -15,11 +15,7 @@ from onvif import ONVIFCamera, ONVIFError
from zeep.exceptions import Fault, TransportError
from zeep.transports import AsyncTransport
from frigate.api.auth import (
allow_any_authenticated,
require_camera_access,
require_role,
)
from frigate.api.auth import require_role
from frigate.api.defs.tags import Tags
from frigate.config.config import FrigateConfig
from frigate.util.builtin import clean_camera_user_pass
@ -54,7 +50,7 @@ def _is_valid_host(host: str) -> bool:
return False
@router.get("/go2rtc/streams", dependencies=[Depends(allow_any_authenticated())])
@router.get("/go2rtc/streams")
def go2rtc_streams():
r = requests.get("http://127.0.0.1:1984/api/streams")
if not r.ok:
@ -70,9 +66,7 @@ def go2rtc_streams():
return JSONResponse(content=stream_data)
@router.get(
"/go2rtc/streams/{camera_name}", dependencies=[Depends(require_camera_access)]
)
@router.get("/go2rtc/streams/{camera_name}")
def go2rtc_camera_stream(request: Request, camera_name: str):
r = requests.get(
f"http://127.0.0.1:1984/api/streams?src={camera_name}&video=all&audio=all&microphone"
@ -167,7 +161,7 @@ def go2rtc_delete_stream(stream_name: str):
)
@router.get("/ffprobe", dependencies=[Depends(require_role(["admin"]))])
@router.get("/ffprobe")
def ffprobe(request: Request, paths: str = "", detailed: bool = False):
path_param = paths

View File

@ -870,46 +870,6 @@ def categorize_classification_image(request: Request, name: str, body: dict = No
)
@router.post(
"/classification/{name}/dataset/{category}/create",
response_model=GenericResponse,
dependencies=[Depends(require_role(["admin"]))],
summary="Create an empty classification category folder",
description="""Creates an empty folder for a classification category.
This is used to create folders for categories that don't have images yet.
Returns a success message or an error if the name is invalid.""",
)
def create_classification_category(request: Request, name: str, category: str):
config: FrigateConfig = request.app.frigate_config
if name not in config.classification.custom:
return JSONResponse(
content=(
{
"success": False,
"message": f"{name} is not a known classification model.",
}
),
status_code=404,
)
category_folder = os.path.join(
CLIPS_DIR, sanitize_filename(name), "dataset", sanitize_filename(category)
)
os.makedirs(category_folder, exist_ok=True)
return JSONResponse(
content=(
{
"success": True,
"message": f"Successfully created category folder: {category}",
}
),
status_code=200,
)
@router.post(
"/classification/{name}/train/delete",
response_model=GenericResponse,

View File

@ -22,7 +22,6 @@ from peewee import JOIN, DoesNotExist, fn, operator
from playhouse.shortcuts import model_to_dict
from frigate.api.auth import (
allow_any_authenticated,
get_allowed_cameras_for_filter,
require_camera_access,
require_role,
@ -809,7 +808,7 @@ def events_search(
return JSONResponse(content=processed_events)
@router.get("/events/summary", dependencies=[Depends(allow_any_authenticated())])
@router.get("/events/summary")
def events_summary(
params: EventsSummaryQueryParams = Depends(),
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
@ -1754,7 +1753,7 @@ def create_trigger_embedding(
body.data, (base64.b64encode(thumbnail).decode("ASCII"))
)
if not embedding:
if embedding is None:
return JSONResponse(
content={
"success": False,
@ -1889,7 +1888,7 @@ def update_trigger_embedding(
body.data, (base64.b64encode(thumbnail).decode("ASCII"))
)
if not embedding:
if embedding is None:
return JSONResponse(
content={
"success": False,

View File

@ -2,7 +2,7 @@ import logging
import re
from typing import Optional
from fastapi import Depends, FastAPI, Request
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from joserfc.jwk import OctKey
from playhouse.sqliteq import SqliteQueueDatabase
@ -24,7 +24,7 @@ from frigate.api import (
preview,
review,
)
from frigate.api.auth import get_jwt_secret, limiter, require_admin_by_default
from frigate.api.auth import get_jwt_secret, limiter
from frigate.comms.event_metadata_updater import (
EventMetadataPublisher,
)
@ -62,15 +62,11 @@ def create_fastapi_app(
stats_emitter: StatsEmitter,
event_metadata_updater: EventMetadataPublisher,
config_publisher: CameraConfigUpdatePublisher,
enforce_default_admin: bool = True,
):
logger.info("Starting FastAPI app")
app = FastAPI(
debug=False,
swagger_ui_parameters={"apisSorter": "alpha", "operationsSorter": "alpha"},
dependencies=[Depends(require_admin_by_default())]
if enforce_default_admin
else [],
)
# update the request_address with the x-forwarded-for header from nginx

View File

@ -22,11 +22,7 @@ from pathvalidate import sanitize_filename
from peewee import DoesNotExist, fn, operator
from tzlocal import get_localzone_name
from frigate.api.auth import (
allow_any_authenticated,
get_allowed_cameras_for_filter,
require_camera_access,
)
from frigate.api.auth import get_allowed_cameras_for_filter, require_camera_access
from frigate.api.defs.query.media_query_parameters import (
Extension,
MediaEventsSnapshotQueryParams,
@ -397,7 +393,7 @@ async def submit_recording_snapshot_to_plus(
)
@router.get("/recordings/storage", dependencies=[Depends(allow_any_authenticated())])
@router.get("/recordings/storage")
def get_recordings_storage_usage(request: Request):
recording_stats = request.app.stats_emitter.get_latest_stats()["service"][
"storage"
@ -421,7 +417,7 @@ def get_recordings_storage_usage(request: Request):
return JSONResponse(content=camera_usages)
@router.get("/recordings/summary", dependencies=[Depends(allow_any_authenticated())])
@router.get("/recordings/summary")
def all_recordings_summary(
request: Request,
params: MediaRecordingsSummaryQueryParams = Depends(),
@ -639,11 +635,7 @@ async def recordings(
return JSONResponse(content=list(recordings))
@router.get(
"/recordings/unavailable",
response_model=list[dict],
dependencies=[Depends(allow_any_authenticated())],
)
@router.get("/recordings/unavailable", response_model=list[dict])
async def no_recordings(
request: Request,
params: MediaRecordingsAvailabilityQueryParams = Depends(),
@ -1061,10 +1053,7 @@ async def event_snapshot(
)
@router.get(
"/events/{event_id}/thumbnail.{extension}",
dependencies=[Depends(require_camera_access)],
)
@router.get("/events/{event_id}/thumbnail.{extension}")
async def event_thumbnail(
request: Request,
event_id: str,
@ -1262,10 +1251,7 @@ def grid_snapshot(
)
@router.get(
"/events/{event_id}/snapshot-clean.webp",
dependencies=[Depends(require_camera_access)],
)
@router.get("/events/{event_id}/snapshot-clean.webp")
def event_snapshot_clean(request: Request, event_id: str, download: bool = False):
webp_bytes = None
try:
@ -1389,9 +1375,7 @@ def event_snapshot_clean(request: Request, event_id: str, download: bool = False
)
@router.get(
"/events/{event_id}/clip.mp4", dependencies=[Depends(require_camera_access)]
)
@router.get("/events/{event_id}/clip.mp4")
async def event_clip(
request: Request,
event_id: str,
@ -1419,9 +1403,7 @@ async def event_clip(
)
@router.get(
"/events/{event_id}/preview.gif", dependencies=[Depends(require_camera_access)]
)
@router.get("/events/{event_id}/preview.gif")
def event_preview(request: Request, event_id: str):
try:
event: Event = Event.get(Event.id == event_id)
@ -1774,7 +1756,7 @@ def preview_mp4(
)
@router.get("/review/{event_id}/preview", dependencies=[Depends(require_camera_access)])
@router.get("/review/{event_id}/preview")
def review_preview(
request: Request,
event_id: str,
@ -1800,12 +1782,8 @@ def review_preview(
return preview_mp4(request, review.camera, start_ts, end_ts)
@router.get(
"/preview/{file_name}/thumbnail.jpg", dependencies=[Depends(require_camera_access)]
)
@router.get(
"/preview/{file_name}/thumbnail.webp", dependencies=[Depends(require_camera_access)]
)
@router.get("/preview/{file_name}/thumbnail.jpg")
@router.get("/preview/{file_name}/thumbnail.webp")
def preview_thumbnail(file_name: str):
"""Get a thumbnail from the cached preview frames."""
if len(file_name) > 1000:

View File

@ -14,7 +14,6 @@ from peewee import Case, DoesNotExist, IntegrityError, fn, operator
from playhouse.shortcuts import model_to_dict
from frigate.api.auth import (
allow_any_authenticated,
get_allowed_cameras_for_filter,
get_current_user,
require_camera_access,
@ -44,11 +43,7 @@ logger = logging.getLogger(__name__)
router = APIRouter(tags=[Tags.review])
@router.get(
"/review",
response_model=list[ReviewSegmentResponse],
dependencies=[Depends(allow_any_authenticated())],
)
@router.get("/review", response_model=list[ReviewSegmentResponse])
async def review(
params: ReviewQueryParams = Depends(),
current_user: dict = Depends(get_current_user),
@ -157,11 +152,7 @@ async def review(
return JSONResponse(content=[r for r in review_query])
@router.get(
"/review_ids",
response_model=list[ReviewSegmentResponse],
dependencies=[Depends(allow_any_authenticated())],
)
@router.get("/review_ids", response_model=list[ReviewSegmentResponse])
async def review_ids(request: Request, ids: str):
ids = ids.split(",")
@ -195,11 +186,7 @@ async def review_ids(request: Request, ids: str):
)
@router.get(
"/review/summary",
response_model=ReviewSummaryResponse,
dependencies=[Depends(allow_any_authenticated())],
)
@router.get("/review/summary", response_model=ReviewSummaryResponse)
async def review_summary(
params: ReviewSummaryQueryParams = Depends(),
current_user: dict = Depends(get_current_user),
@ -474,11 +461,7 @@ async def review_summary(
return JSONResponse(content=data)
@router.post(
"/reviews/viewed",
response_model=GenericResponse,
dependencies=[Depends(allow_any_authenticated())],
)
@router.post("/reviews/viewed", response_model=GenericResponse)
async def set_multiple_reviewed(
request: Request,
body: ReviewModifyMultipleBody,
@ -661,11 +644,7 @@ def motion_activity(
return JSONResponse(content=normalized)
@router.get(
"/review/event/{event_id}",
response_model=ReviewSegmentResponse,
dependencies=[Depends(allow_any_authenticated())],
)
@router.get("/review/event/{event_id}", response_model=ReviewSegmentResponse)
async def get_review_from_event(request: Request, event_id: str):
try:
review = ReviewSegment.get(
@ -680,11 +659,7 @@ async def get_review_from_event(request: Request, event_id: str):
)
@router.get(
"/review/{review_id}",
response_model=ReviewSegmentResponse,
dependencies=[Depends(allow_any_authenticated())],
)
@router.get("/review/{review_id}", response_model=ReviewSegmentResponse)
async def get_review(request: Request, review_id: str):
try:
review = ReviewSegment.get(ReviewSegment.id == review_id)
@ -697,11 +672,7 @@ async def get_review(request: Request, review_id: str):
)
@router.delete(
"/review/{review_id}/viewed",
response_model=GenericResponse,
dependencies=[Depends(allow_any_authenticated())],
)
@router.delete("/review/{review_id}/viewed", response_model=GenericResponse)
async def set_not_reviewed(
review_id: str,
current_user: dict = Depends(get_current_user),

View File

@ -375,19 +375,7 @@ class WebPushClient(Communicator):
ended = state == "end" or state == "genai"
if state == "genai" and payload["after"]["data"]["metadata"]:
base_title = payload["after"]["data"]["metadata"]["title"]
threat_level = payload["after"]["data"]["metadata"].get(
"potential_threat_level", 0
)
# Add prefix for threat levels 1 and 2
if threat_level == 1:
title = f"Needs Review: {base_title}"
elif threat_level == 2:
title = f"Security Concern: {base_title}"
else:
title = base_title
title = payload["after"]["data"]["metadata"]["title"]
message = payload["after"]["data"]["metadata"]["scene"]
else:
title = f"{titlecase(', '.join(sorted_objects).replace('_', ' '))}{' was' if state == 'end' else ''} detected in {titlecase(', '.join(payload['after']['data']['zones']).replace('_', ' '))}"

View File

@ -12,7 +12,6 @@ from typing import Any
import cv2
from peewee import DoesNotExist
from titlecase import titlecase
from frigate.comms.embeddings_updater import EmbeddingsRequestEnum
from frigate.comms.inter_process import InterProcessRequestor
@ -456,14 +455,14 @@ def run_analysis(
for i, verified_label in enumerate(final_data["data"]["verified_objects"]):
object_type = verified_label.replace("-verified", "").replace("_", " ")
name = titlecase(sub_labels_list[i].replace("_", " "))
name = sub_labels_list[i].replace("_", " ").title()
unified_objects.append(f"{name} ({object_type})")
for label in objects_list:
if "-verified" in label:
continue
elif label in labelmap_objects:
object_type = titlecase(label.replace("_", " "))
object_type = label.replace("_", " ").title()
if label in attribute_labels:
unified_objects.append(f"{object_type} (delivery/service)")

View File

@ -405,6 +405,9 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi):
if obj_data.get("end_time") is not None:
return
if obj_data.get("stationary"):
return
object_id = obj_data["id"]
if (

View File

@ -2,6 +2,7 @@ import glob
import logging
import os
import shutil
import time
import urllib.request
import zipfile
from queue import Queue
@ -54,9 +55,6 @@ class MemryXDetector(DetectionApi):
)
return
# Initialize stop_event as None, will be set later by set_stop_event()
self.stop_event = None
model_cfg = getattr(detector_config, "model", None)
# Check if model_type was explicitly set by the user
@ -365,43 +363,26 @@ class MemryXDetector(DetectionApi):
def process_input(self):
"""Input callback function: wait for frames in the input queue, preprocess, and send to MX3 (return)"""
while True:
# Check if shutdown is requested
if self.stop_event and self.stop_event.is_set():
logger.debug("[process_input] Stop event detected, returning None")
return None
try:
# Wait for a frame from the queue with timeout to check stop_event periodically
frame = self.capture_queue.get(block=True, timeout=0.5)
# Wait for a frame from the queue (blocking call)
frame = self.capture_queue.get(
block=True
) # Blocks until data is available
return frame
except Exception as e:
# Silently handle queue.Empty timeouts (expected during normal operation)
# Log any other unexpected exceptions
if "Empty" not in str(type(e).__name__):
logger.warning(f"[process_input] Unexpected error: {e}")
# Loop continues and will check stop_event at the top
logger.info(f"[process_input] Error processing input: {e}")
time.sleep(0.1) # Prevent busy waiting in case of error
def receive_output(self):
"""Retrieve processed results from MemryX output queue + a copy of the original frame"""
try:
# Get connection ID with timeout
connection_id = self.capture_id_queue.get(
block=True, timeout=1.0
) # Get the corresponding connection ID
detections = self.output_queue.get() # Get detections from MemryX
connection_id = (
self.capture_id_queue.get()
) # Get the corresponding connection ID
detections = self.output_queue.get() # Get detections from MemryX
return connection_id, detections
except Exception as e:
# On timeout or stop event, return None
if self.stop_event and self.stop_event.is_set():
logger.debug("[receive_output] Stop event detected, exiting")
# Silently handle queue.Empty timeouts, they're expected during normal operation
elif "Empty" not in str(type(e).__name__):
logger.warning(f"[receive_output] Error receiving output: {e}")
return None, None
return connection_id, detections
def post_process_yolonas(self, output):
predictions = output[0]
@ -850,19 +831,6 @@ class MemryXDetector(DetectionApi):
f"{self.memx_model_type} is currently not supported for memryx. See the docs for more info on supported models."
)
def set_stop_event(self, stop_event):
"""Set the stop event for graceful shutdown."""
self.stop_event = stop_event
def shutdown(self):
"""Gracefully shutdown the MemryX accelerator"""
try:
if hasattr(self, "accl") and self.accl is not None:
self.accl.shutdown()
logger.info("MemryX accelerator shutdown complete")
except Exception as e:
logger.error(f"Error during MemryX shutdown: {e}")
def detect_raw(self, tensor_input: np.ndarray):
"""Removed synchronous detect_raw() function so that we only use async"""
return 0

View File

@ -205,20 +205,14 @@ Rules for the report:
- Group bullets under subheadings when multiple events fall into the same category (e.g., Vehicle Activity, Porch Activity, Unusual Behavior).
- Threat levels
- Always show the threat level for each event using these labels:
- Threat level 0: "Normal"
- Threat level 1: "Needs review"
- Threat level 2: "Security concern"
- Format as (threat level: Normal), (threat level: Needs review), or (threat level: Security concern).
- Always show (threat level: X) for each event.
- If multiple events at the same time share the same threat level, only state it once.
- Final assessment
- End with a Final Assessment section.
- If all events are threat level 0:
- If all events are threat level 1 with no escalation:
Final assessment: Only normal residential activity observed during this period.
- If threat level 1 events are present:
Final assessment: Some activity requires review but no security concerns identified.
- If threat level 2 events are present, clearly summarize them as Security concerns requiring immediate attention.
- If threat level 2+ events are present, clearly summarize them as Potential concerns requiring review.
- Conciseness
- Do not repeat benign clothing/appearance details unless they distinguish individuals.

View File

@ -43,7 +43,6 @@ class BaseLocalDetector(ObjectDetector):
self,
detector_config: BaseDetectorConfig = None,
labels: str = None,
stop_event: MpEvent = None,
):
self.fps = EventsPerSecond()
if labels is None:
@ -61,10 +60,6 @@ class BaseLocalDetector(ObjectDetector):
self.detect_api = create_detector(detector_config)
# If the detector supports stop_event, pass it
if hasattr(self.detect_api, "set_stop_event") and stop_event:
self.detect_api.set_stop_event(stop_event)
def _transform_input(self, tensor_input: np.ndarray) -> np.ndarray:
if self.input_transform:
tensor_input = np.transpose(tensor_input, self.input_transform)
@ -245,10 +240,6 @@ class AsyncDetectorRunner(FrigateProcess):
while not self.stop_event.is_set():
connection_id, detections = self._detector.async_receive_output()
# Handle timeout case (queue.Empty) - just continue
if connection_id is None:
continue
if not self.send_times:
# guard; shouldn't happen if send/recv are balanced
continue
@ -275,38 +266,21 @@ class AsyncDetectorRunner(FrigateProcess):
self._frame_manager = SharedMemoryFrameManager()
self._publisher = ObjectDetectorPublisher()
self._detector = AsyncLocalObjectDetector(
detector_config=self.detector_config, stop_event=self.stop_event
)
self._detector = AsyncLocalObjectDetector(detector_config=self.detector_config)
for name in self.cameras:
self.create_output_shm(name)
t_detect = threading.Thread(target=self._detect_worker, daemon=False)
t_result = threading.Thread(target=self._result_worker, daemon=False)
t_detect = threading.Thread(target=self._detect_worker, daemon=True)
t_result = threading.Thread(target=self._result_worker, daemon=True)
t_detect.start()
t_result.start()
try:
while not self.stop_event.is_set():
time.sleep(0.5)
while not self.stop_event.is_set():
time.sleep(0.5)
logger.info(
"Stop event detected, waiting for detector threads to finish..."
)
# Wait for threads to finish processing
t_detect.join(timeout=5)
t_result.join(timeout=5)
# Shutdown the AsyncDetector
self._detector.detect_api.shutdown()
self._publisher.stop()
except Exception as e:
logger.error(f"Error during async detector shutdown: {e}")
finally:
logger.info("Exited Async detection process...")
self._publisher.stop()
logger.info("Exited async detection process...")
class ObjectDetectProcess:
@ -334,7 +308,7 @@ class ObjectDetectProcess:
# if the process has already exited on its own, just return
if self.detect_process and self.detect_process.exitcode:
return
self.detect_process.terminate()
logging.info("Waiting for detection process to exit gracefully...")
self.detect_process.join(timeout=30)
if self.detect_process.exitcode is None:

View File

@ -3,8 +3,6 @@ import logging
import os
import unittest
from fastapi import Request
from fastapi.testclient import TestClient
from peewee_migrate import Router
from playhouse.sqlite_ext import SqliteExtDatabase
from playhouse.sqliteq import SqliteQueueDatabase
@ -18,20 +16,6 @@ from frigate.review.types import SeverityEnum
from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS
class AuthTestClient(TestClient):
"""TestClient that automatically adds auth headers to all requests."""
def request(self, *args, **kwargs):
# Add default auth headers if not already present
headers = kwargs.get("headers") or {}
if "remote-user" not in headers:
headers["remote-user"] = "admin"
if "remote-role" not in headers:
headers["remote-role"] = "admin"
kwargs["headers"] = headers
return super().request(*args, **kwargs)
class BaseTestHttp(unittest.TestCase):
def setUp(self, models):
# setup clean database for each test run
@ -129,9 +113,7 @@ class BaseTestHttp(unittest.TestCase):
pass
def create_app(self, stats=None, event_metadata_publisher=None):
from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user
app = create_fastapi_app(
return create_fastapi_app(
FrigateConfig(**self.minimal_config),
self.db,
None,
@ -141,33 +123,8 @@ class BaseTestHttp(unittest.TestCase):
stats,
event_metadata_publisher,
None,
enforce_default_admin=False,
)
# Default test mocks for authentication
# Tests can override these in their setUp if needed
# This mock uses headers set by AuthTestClient
async def mock_get_current_user(request: Request):
username = request.headers.get("remote-user")
role = request.headers.get("remote-role")
if not username or not role:
from fastapi.responses import JSONResponse
return JSONResponse(
content={"message": "No authorization headers."}, status_code=401
)
return {"username": username, "role": role}
async def mock_get_allowed_cameras_for_filter(request: Request):
return list(self.minimal_config.get("cameras", {}).keys())
app.dependency_overrides[get_current_user] = mock_get_current_user
app.dependency_overrides[get_allowed_cameras_for_filter] = (
mock_get_allowed_cameras_for_filter
)
return app
def insert_mock_event(
self,
id: str,

View File

@ -1,8 +1,10 @@
from unittest.mock import Mock
from fastapi.testclient import TestClient
from frigate.models import Event, Recordings, ReviewSegment
from frigate.stats.emitter import StatsEmitter
from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp
from frigate.test.http_api.base_http_test import BaseTestHttp
class TestHttpApp(BaseTestHttp):
@ -18,7 +20,7 @@ class TestHttpApp(BaseTestHttp):
stats.get_latest_stats.return_value = self.test_stats
app = super().create_app(stats)
with AuthTestClient(app) as client:
with TestClient(app) as client:
response = client.get("/stats")
response_json = response.json()
assert response_json == self.test_stats

View File

@ -1,13 +1,14 @@
from unittest.mock import patch
from fastapi import HTTPException, Request
from fastapi.testclient import TestClient
from frigate.api.auth import (
get_allowed_cameras_for_filter,
get_current_user,
)
from frigate.models import Event, Recordings, ReviewSegment
from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp
from frigate.test.http_api.base_http_test import BaseTestHttp
class TestCameraAccessEventReview(BaseTestHttp):
@ -15,17 +16,9 @@ class TestCameraAccessEventReview(BaseTestHttp):
super().setUp([Event, ReviewSegment, Recordings])
self.app = super().create_app()
# Mock get_current_user for all tests
async def mock_get_current_user(request: Request):
username = request.headers.get("remote-user")
role = request.headers.get("remote-role")
if not username or not role:
from fastapi.responses import JSONResponse
return JSONResponse(
content={"message": "No authorization headers."}, status_code=401
)
return {"username": username, "role": role}
# Mock get_current_user to return valid user for all tests
async def mock_get_current_user():
return {"username": "test_user", "role": "user"}
self.app.dependency_overrides[get_current_user] = mock_get_current_user
@ -37,25 +30,21 @@ class TestCameraAccessEventReview(BaseTestHttp):
super().insert_mock_event("event1", camera="front_door")
super().insert_mock_event("event2", camera="back_door")
async def mock_cameras(request: Request):
return ["front_door"]
self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras
with AuthTestClient(self.app) as client:
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
"front_door"
]
with TestClient(self.app) as client:
resp = client.get("/events")
assert resp.status_code == 200
ids = [e["id"] for e in resp.json()]
assert "event1" in ids
assert "event2" not in ids
async def mock_cameras(request: Request):
return [
"front_door",
"back_door",
]
self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras
with AuthTestClient(self.app) as client:
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
"front_door",
"back_door",
]
with TestClient(self.app) as client:
resp = client.get("/events")
assert resp.status_code == 200
ids = [e["id"] for e in resp.json()]
@ -65,25 +54,21 @@ class TestCameraAccessEventReview(BaseTestHttp):
super().insert_mock_review_segment("rev1", camera="front_door")
super().insert_mock_review_segment("rev2", camera="back_door")
async def mock_cameras(request: Request):
return ["front_door"]
self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras
with AuthTestClient(self.app) as client:
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
"front_door"
]
with TestClient(self.app) as client:
resp = client.get("/review")
assert resp.status_code == 200
ids = [r["id"] for r in resp.json()]
assert "rev1" in ids
assert "rev2" not in ids
async def mock_cameras(request: Request):
return [
"front_door",
"back_door",
]
self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras
with AuthTestClient(self.app) as client:
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
"front_door",
"back_door",
]
with TestClient(self.app) as client:
resp = client.get("/review")
assert resp.status_code == 200
ids = [r["id"] for r in resp.json()]
@ -99,7 +84,7 @@ class TestCameraAccessEventReview(BaseTestHttp):
raise HTTPException(status_code=403, detail="Access denied")
with patch("frigate.api.event.require_camera_access", mock_require_allowed):
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
resp = client.get("/events/event1")
assert resp.status_code == 200
assert resp.json()["id"] == "event1"
@ -109,7 +94,7 @@ class TestCameraAccessEventReview(BaseTestHttp):
raise HTTPException(status_code=403, detail="Access denied")
with patch("frigate.api.event.require_camera_access", mock_require_disallowed):
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
resp = client.get("/events/event1")
assert resp.status_code == 403
@ -123,7 +108,7 @@ class TestCameraAccessEventReview(BaseTestHttp):
raise HTTPException(status_code=403, detail="Access denied")
with patch("frigate.api.review.require_camera_access", mock_require_allowed):
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
resp = client.get("/review/rev1")
assert resp.status_code == 200
assert resp.json()["id"] == "rev1"
@ -133,7 +118,7 @@ class TestCameraAccessEventReview(BaseTestHttp):
raise HTTPException(status_code=403, detail="Access denied")
with patch("frigate.api.review.require_camera_access", mock_require_disallowed):
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
resp = client.get("/review/rev1")
assert resp.status_code == 403
@ -141,25 +126,21 @@ class TestCameraAccessEventReview(BaseTestHttp):
super().insert_mock_event("event1", camera="front_door")
super().insert_mock_event("event2", camera="back_door")
async def mock_cameras(request: Request):
return ["front_door"]
self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras
with AuthTestClient(self.app) as client:
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
"front_door"
]
with TestClient(self.app) as client:
resp = client.get("/events", params={"cameras": "all"})
assert resp.status_code == 200
ids = [e["id"] for e in resp.json()]
assert "event1" in ids
assert "event2" not in ids
async def mock_cameras(request: Request):
return [
"front_door",
"back_door",
]
self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras
with AuthTestClient(self.app) as client:
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
"front_door",
"back_door",
]
with TestClient(self.app) as client:
resp = client.get("/events", params={"cameras": "all"})
assert resp.status_code == 200
ids = [e["id"] for e in resp.json()]
@ -169,24 +150,20 @@ class TestCameraAccessEventReview(BaseTestHttp):
super().insert_mock_event("event1", camera="front_door")
super().insert_mock_event("event2", camera="back_door")
async def mock_cameras(request: Request):
return ["front_door"]
self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras
with AuthTestClient(self.app) as client:
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
"front_door"
]
with TestClient(self.app) as client:
resp = client.get("/events/summary")
assert resp.status_code == 200
summary_list = resp.json()
assert len(summary_list) == 1
async def mock_cameras(request: Request):
return [
"front_door",
"back_door",
]
self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras
with AuthTestClient(self.app) as client:
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
"front_door",
"back_door",
]
with TestClient(self.app) as client:
resp = client.get("/events/summary")
summary_list = resp.json()
assert len(summary_list) == 2

View File

@ -2,13 +2,14 @@ from datetime import datetime
from typing import Any
from unittest.mock import Mock
from fastapi.testclient import TestClient
from playhouse.shortcuts import model_to_dict
from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user
from frigate.comms.event_metadata_updater import EventMetadataPublisher
from frigate.models import Event, Recordings, ReviewSegment, Timeline
from frigate.stats.emitter import StatsEmitter
from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp, Request
from frigate.test.http_api.base_http_test import BaseTestHttp
from frigate.test.test_storage import _insert_mock_event
@ -17,26 +18,14 @@ class TestHttpApp(BaseTestHttp):
super().setUp([Event, Recordings, ReviewSegment, Timeline])
self.app = super().create_app()
# Mock get_current_user for all tests
async def mock_get_current_user(request: Request):
username = request.headers.get("remote-user")
role = request.headers.get("remote-role")
if not username or not role:
from fastapi.responses import JSONResponse
return JSONResponse(
content={"message": "No authorization headers."}, status_code=401
)
return {"username": username, "role": role}
# Mock auth to bypass camera access for tests
async def mock_get_current_user(request: Any):
return {"username": "test_user", "role": "admin"}
self.app.dependency_overrides[get_current_user] = mock_get_current_user
async def mock_get_allowed_cameras_for_filter(request: Request):
return ["front_door"]
self.app.dependency_overrides[get_allowed_cameras_for_filter] = (
mock_get_allowed_cameras_for_filter
)
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
"front_door"
]
def tearDown(self):
self.app.dependency_overrides.clear()
@ -46,20 +35,20 @@ class TestHttpApp(BaseTestHttp):
################################### GET /events Endpoint #########################################################
####################################################################################################################
def test_get_event_list_no_events(self):
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
events = client.get("/events").json()
assert len(events) == 0
def test_get_event_list_no_match_event_id(self):
id = "123456.random"
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
super().insert_mock_event(id)
events = client.get("/events", params={"event_id": "abc"}).json()
assert len(events) == 0
def test_get_event_list_match_event_id(self):
id = "123456.random"
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
super().insert_mock_event(id)
events = client.get("/events", params={"event_id": id}).json()
assert len(events) == 1
@ -69,7 +58,7 @@ class TestHttpApp(BaseTestHttp):
now = int(datetime.now().timestamp())
id = "123456.random"
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
super().insert_mock_event(id, now, now + 1)
events = client.get(
"/events", params={"max_length": 1, "min_length": 1}
@ -80,7 +69,7 @@ class TestHttpApp(BaseTestHttp):
def test_get_event_list_no_match_max_length(self):
now = int(datetime.now().timestamp())
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
id = "123456.random"
super().insert_mock_event(id, now, now + 2)
events = client.get("/events", params={"max_length": 1}).json()
@ -89,7 +78,7 @@ class TestHttpApp(BaseTestHttp):
def test_get_event_list_no_match_min_length(self):
now = int(datetime.now().timestamp())
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
id = "123456.random"
super().insert_mock_event(id, now, now + 2)
events = client.get("/events", params={"min_length": 3}).json()
@ -99,7 +88,7 @@ class TestHttpApp(BaseTestHttp):
id = "123456.random"
id2 = "54321.random"
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
super().insert_mock_event(id)
events = client.get("/events").json()
assert len(events) == 1
@ -119,14 +108,14 @@ class TestHttpApp(BaseTestHttp):
def test_get_event_list_no_match_has_clip(self):
now = int(datetime.now().timestamp())
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
id = "123456.random"
super().insert_mock_event(id, now, now + 2)
events = client.get("/events", params={"has_clip": 0}).json()
assert len(events) == 0
def test_get_event_list_has_clip(self):
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
id = "123456.random"
super().insert_mock_event(id, has_clip=True)
events = client.get("/events", params={"has_clip": 1}).json()
@ -134,7 +123,7 @@ class TestHttpApp(BaseTestHttp):
assert events[0]["id"] == id
def test_get_event_list_sort_score(self):
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
id = "123456.random"
id2 = "54321.random"
super().insert_mock_event(id, top_score=37, score=37, data={"score": 50})
@ -152,7 +141,7 @@ class TestHttpApp(BaseTestHttp):
def test_get_event_list_sort_start_time(self):
now = int(datetime.now().timestamp())
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
id = "123456.random"
id2 = "54321.random"
super().insert_mock_event(id, start_time=now + 3)
@ -170,7 +159,7 @@ class TestHttpApp(BaseTestHttp):
def test_get_good_event(self):
id = "123456.random"
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
super().insert_mock_event(id)
event = client.get(f"/events/{id}").json()
@ -182,7 +171,7 @@ class TestHttpApp(BaseTestHttp):
id = "123456.random"
bad_id = "654321.other"
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
super().insert_mock_event(id)
event_response = client.get(f"/events/{bad_id}")
assert event_response.status_code == 404
@ -191,7 +180,7 @@ class TestHttpApp(BaseTestHttp):
def test_delete_event(self):
id = "123456.random"
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
super().insert_mock_event(id)
event = client.get(f"/events/{id}").json()
assert event
@ -204,7 +193,7 @@ class TestHttpApp(BaseTestHttp):
def test_event_retention(self):
id = "123456.random"
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
super().insert_mock_event(id)
client.post(f"/events/{id}/retain", headers={"remote-role": "admin"})
event = client.get(f"/events/{id}").json()
@ -223,11 +212,12 @@ class TestHttpApp(BaseTestHttp):
morning = 1656590400 # 06/30/2022 6 am (GMT)
evening = 1656633600 # 06/30/2022 6 pm (GMT)
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
super().insert_mock_event(morning_id, morning)
super().insert_mock_event(evening_id, evening)
# both events come back
events = client.get("/events").json()
print("events!!!", events)
assert events
assert len(events) == 2
# morning event is excluded
@ -258,7 +248,7 @@ class TestHttpApp(BaseTestHttp):
mock_event_updater.publish.side_effect = update_event
with AuthTestClient(app) as client:
with TestClient(app) as client:
super().insert_mock_event(id)
new_sub_label_response = client.post(
f"/events/{id}/sub_label",
@ -295,7 +285,7 @@ class TestHttpApp(BaseTestHttp):
mock_event_updater.publish.side_effect = update_event
with AuthTestClient(app) as client:
with TestClient(app) as client:
super().insert_mock_event(id)
client.post(
f"/events/{id}/sub_label",
@ -311,7 +301,7 @@ class TestHttpApp(BaseTestHttp):
####################################################################################################################
def test_get_metrics(self):
"""ensure correct prometheus metrics api response"""
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
ts_start = datetime.now().timestamp()
ts_end = ts_start + 30
_insert_mock_event(

View File

@ -1,13 +1,14 @@
"""Unit tests for recordings/media API endpoints."""
from datetime import datetime, timezone
from typing import Any
import pytz
from fastapi import Request
from fastapi.testclient import TestClient
from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user
from frigate.models import Recordings
from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp
from frigate.test.http_api.base_http_test import BaseTestHttp
class TestHttpMedia(BaseTestHttp):
@ -18,26 +19,15 @@ class TestHttpMedia(BaseTestHttp):
super().setUp([Recordings])
self.app = super().create_app()
# Mock get_current_user for all tests
async def mock_get_current_user(request: Request):
username = request.headers.get("remote-user")
role = request.headers.get("remote-role")
if not username or not role:
from fastapi.responses import JSONResponse
return JSONResponse(
content={"message": "No authorization headers."}, status_code=401
)
return {"username": username, "role": role}
# Mock auth to bypass camera access for tests
async def mock_get_current_user(request: Any):
return {"username": "test_user", "role": "admin"}
self.app.dependency_overrides[get_current_user] = mock_get_current_user
async def mock_get_allowed_cameras_for_filter(request: Request):
return ["front_door"]
self.app.dependency_overrides[get_allowed_cameras_for_filter] = (
mock_get_allowed_cameras_for_filter
)
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
"front_door",
"back_door",
]
def tearDown(self):
"""Clean up after tests."""
@ -62,7 +52,7 @@ class TestHttpMedia(BaseTestHttp):
# March 11, 2024 at 12:00 PM EDT (after DST)
march_11_noon = tz.localize(datetime(2024, 3, 11, 12, 0, 0)).timestamp()
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
# Insert recordings for each day
Recordings.insert(
id="recording_march_9",
@ -138,7 +128,7 @@ class TestHttpMedia(BaseTestHttp):
# November 4, 2024 at 12:00 PM EST (after DST)
nov_4_noon = tz.localize(datetime(2024, 11, 4, 12, 0, 0)).timestamp()
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
# Insert recordings for each day
Recordings.insert(
id="recording_nov_2",
@ -205,15 +195,7 @@ class TestHttpMedia(BaseTestHttp):
# March 10, 2024 at 3:00 PM EDT (after DST transition)
march_10_afternoon = tz.localize(datetime(2024, 3, 10, 15, 0, 0)).timestamp()
with AuthTestClient(self.app) as client:
# Override allowed cameras for this test to include both
async def mock_get_allowed_cameras_for_filter(_request: Request):
return ["front_door", "back_door"]
self.app.dependency_overrides[get_allowed_cameras_for_filter] = (
mock_get_allowed_cameras_for_filter
)
with TestClient(self.app) as client:
# Insert recordings for front_door on March 9
Recordings.insert(
id="front_march_9",
@ -254,14 +236,6 @@ class TestHttpMedia(BaseTestHttp):
assert summary["2024-03-09"] is True
assert summary["2024-03-10"] is True
# Reset dependency override back to default single camera for other tests
async def reset_allowed_cameras(_request: Request):
return ["front_door"]
self.app.dependency_overrides[get_allowed_cameras_for_filter] = (
reset_allowed_cameras
)
def test_recordings_summary_at_dst_transition_time(self):
"""
Test recordings that span the exact DST transition time.
@ -276,7 +250,7 @@ class TestHttpMedia(BaseTestHttp):
# This is 1.5 hours of actual time but spans the "missing" hour
after_transition = tz.localize(datetime(2024, 3, 10, 3, 30, 0)).timestamp()
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
Recordings.insert(
id="recording_during_transition",
path="/media/recordings/transition.mp4",
@ -309,7 +283,7 @@ class TestHttpMedia(BaseTestHttp):
march_9_utc = datetime(2024, 3, 9, 17, 0, 0, tzinfo=timezone.utc).timestamp()
march_10_utc = datetime(2024, 3, 10, 17, 0, 0, tzinfo=timezone.utc).timestamp()
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
Recordings.insert(
id="recording_march_9_utc",
path="/media/recordings/march_9_utc.mp4",
@ -351,7 +325,7 @@ class TestHttpMedia(BaseTestHttp):
"""
Test recordings summary when no recordings exist.
"""
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
response = client.get(
"/recordings/summary",
params={"timezone": "America/New_York", "cameras": "all"},
@ -368,7 +342,7 @@ class TestHttpMedia(BaseTestHttp):
tz = pytz.timezone("America/New_York")
march_10_noon = tz.localize(datetime(2024, 3, 10, 12, 0, 0)).timestamp()
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
# Insert recordings for both cameras
Recordings.insert(
id="front_recording",

View File

@ -1,12 +1,12 @@
from datetime import datetime, timedelta
from fastapi import Request
from fastapi.testclient import TestClient
from peewee import DoesNotExist
from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user
from frigate.models import Event, Recordings, ReviewSegment, UserReviewStatus
from frigate.review.types import SeverityEnum
from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp
from frigate.test.http_api.base_http_test import BaseTestHttp
class TestHttpReview(BaseTestHttp):
@ -16,26 +16,14 @@ class TestHttpReview(BaseTestHttp):
self.user_id = "admin"
# Mock get_current_user for all tests
# This mock uses headers set by AuthTestClient
async def mock_get_current_user(request: Request):
username = request.headers.get("remote-user")
role = request.headers.get("remote-role")
if not username or not role:
from fastapi.responses import JSONResponse
return JSONResponse(
content={"message": "No authorization headers."}, status_code=401
)
return {"username": username, "role": role}
async def mock_get_current_user():
return {"username": self.user_id, "role": "admin"}
self.app.dependency_overrides[get_current_user] = mock_get_current_user
async def mock_get_allowed_cameras_for_filter(request: Request):
return ["front_door"]
self.app.dependency_overrides[get_allowed_cameras_for_filter] = (
mock_get_allowed_cameras_for_filter
)
self.app.dependency_overrides[get_allowed_cameras_for_filter] = lambda: [
"front_door"
]
def tearDown(self):
self.app.dependency_overrides.clear()
@ -69,7 +57,7 @@ class TestHttpReview(BaseTestHttp):
but ends after is included in the results."""
now = datetime.now().timestamp()
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
super().insert_mock_review_segment("123456.random", now, now + 2)
response = client.get("/review")
assert response.status_code == 200
@ -79,7 +67,7 @@ class TestHttpReview(BaseTestHttp):
def test_get_review_no_filters(self):
now = datetime.now().timestamp()
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
id = "123456.random"
super().insert_mock_review_segment(id, now - 2, now - 1)
response = client.get("/review")
@ -93,7 +81,7 @@ class TestHttpReview(BaseTestHttp):
"""Test that review items outside the range are not returned."""
now = datetime.now().timestamp()
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
id = "123456.random"
super().insert_mock_review_segment(id, now - 2, now - 1)
super().insert_mock_review_segment(f"{id}2", now + 4, now + 5)
@ -109,7 +97,7 @@ class TestHttpReview(BaseTestHttp):
def test_get_review_with_time_filter(self):
now = datetime.now().timestamp()
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
id = "123456.random"
super().insert_mock_review_segment(id, now, now + 2)
params = {
@ -125,7 +113,7 @@ class TestHttpReview(BaseTestHttp):
def test_get_review_with_limit_filter(self):
now = datetime.now().timestamp()
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
id = "123456.random"
id2 = "654321.random"
super().insert_mock_review_segment(id, now, now + 2)
@ -144,7 +132,7 @@ class TestHttpReview(BaseTestHttp):
def test_get_review_with_severity_filters_no_matches(self):
now = datetime.now().timestamp()
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
id = "123456.random"
super().insert_mock_review_segment(id, now, now + 2, SeverityEnum.detection)
params = {
@ -161,7 +149,7 @@ class TestHttpReview(BaseTestHttp):
def test_get_review_with_severity_filters(self):
now = datetime.now().timestamp()
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
id = "123456.random"
super().insert_mock_review_segment(id, now, now + 2, SeverityEnum.detection)
params = {
@ -177,7 +165,7 @@ class TestHttpReview(BaseTestHttp):
def test_get_review_with_all_filters(self):
now = datetime.now().timestamp()
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
id = "123456.random"
super().insert_mock_review_segment(id, now, now + 2)
params = {
@ -200,7 +188,7 @@ class TestHttpReview(BaseTestHttp):
################################### GET /review/summary Endpoint #################################################
####################################################################################################################
def test_get_review_summary_all_filters(self):
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
super().insert_mock_review_segment("123456.random")
params = {
"cameras": "front_door",
@ -231,7 +219,7 @@ class TestHttpReview(BaseTestHttp):
self.assertEqual(response_json, expected_response)
def test_get_review_summary_no_filters(self):
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
super().insert_mock_review_segment("123456.random")
response = client.get("/review/summary")
assert response.status_code == 200
@ -259,7 +247,7 @@ class TestHttpReview(BaseTestHttp):
now = datetime.now()
five_days_ago = datetime.today() - timedelta(days=5)
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
super().insert_mock_review_segment(
"123456.random", now.timestamp() - 2, now.timestamp() - 1
)
@ -303,7 +291,7 @@ class TestHttpReview(BaseTestHttp):
now = datetime.now()
five_days_ago = datetime.today() - timedelta(days=5)
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
super().insert_mock_review_segment("123456.random", now.timestamp())
five_days_ago_ts = five_days_ago.timestamp()
for i in range(20):
@ -354,7 +342,7 @@ class TestHttpReview(BaseTestHttp):
def test_get_review_summary_multiple_in_same_day_with_reviewed(self):
five_days_ago = datetime.today() - timedelta(days=5)
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
five_days_ago_ts = five_days_ago.timestamp()
for i in range(10):
id = f"123456_{i}.random_alert_not_reviewed"
@ -405,14 +393,14 @@ class TestHttpReview(BaseTestHttp):
####################################################################################################################
def test_post_reviews_viewed_no_body(self):
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
super().insert_mock_review_segment("123456.random")
response = client.post("/reviews/viewed")
# Missing ids
assert response.status_code == 422
def test_post_reviews_viewed_no_body_ids(self):
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
super().insert_mock_review_segment("123456.random")
body = {"ids": [""]}
response = client.post("/reviews/viewed", json=body)
@ -420,7 +408,7 @@ class TestHttpReview(BaseTestHttp):
assert response.status_code == 422
def test_post_reviews_viewed_non_existent_id(self):
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
id = "123456.random"
super().insert_mock_review_segment(id)
body = {"ids": ["1"]}
@ -437,7 +425,7 @@ class TestHttpReview(BaseTestHttp):
)
def test_post_reviews_viewed(self):
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
id = "123456.random"
super().insert_mock_review_segment(id)
body = {"ids": [id]}
@ -457,14 +445,14 @@ class TestHttpReview(BaseTestHttp):
################################### POST reviews/delete Endpoint ################################################
####################################################################################################################
def test_post_reviews_delete_no_body(self):
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
super().insert_mock_review_segment("123456.random")
response = client.post("/reviews/delete", headers={"remote-role": "admin"})
# Missing ids
assert response.status_code == 422
def test_post_reviews_delete_no_body_ids(self):
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
super().insert_mock_review_segment("123456.random")
body = {"ids": [""]}
response = client.post(
@ -474,7 +462,7 @@ class TestHttpReview(BaseTestHttp):
assert response.status_code == 422
def test_post_reviews_delete_non_existent_id(self):
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
id = "123456.random"
super().insert_mock_review_segment(id)
body = {"ids": ["1"]}
@ -491,7 +479,7 @@ class TestHttpReview(BaseTestHttp):
assert review_ids_in_db_after[0].id == id
def test_post_reviews_delete(self):
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
id = "123456.random"
super().insert_mock_review_segment(id)
body = {"ids": [id]}
@ -507,7 +495,7 @@ class TestHttpReview(BaseTestHttp):
assert len(review_ids_in_db_after) == 0
def test_post_reviews_delete_many(self):
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
ids = ["123456.random", "654321.random"]
for id in ids:
super().insert_mock_review_segment(id)
@ -539,7 +527,7 @@ class TestHttpReview(BaseTestHttp):
def test_review_activity_motion_no_data_for_time_range(self):
now = datetime.now().timestamp()
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
params = {
"after": now,
"before": now + 3,
@ -552,7 +540,7 @@ class TestHttpReview(BaseTestHttp):
def test_review_activity_motion(self):
now = int(datetime.now().timestamp())
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
one_m = int((datetime.now() + timedelta(minutes=1)).timestamp())
id = "123456.random"
id2 = "123451.random"
@ -585,7 +573,7 @@ class TestHttpReview(BaseTestHttp):
################################### GET /review/event/{event_id} Endpoint #######################################
####################################################################################################################
def test_review_event_not_found(self):
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
response = client.get("/review/event/123456.random")
assert response.status_code == 404
response_json = response.json()
@ -597,7 +585,7 @@ class TestHttpReview(BaseTestHttp):
def test_review_event_not_found_in_data(self):
now = datetime.now().timestamp()
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
id = "123456.random"
super().insert_mock_review_segment(id, now + 1, now + 2)
response = client.get(f"/review/event/{id}")
@ -611,7 +599,7 @@ class TestHttpReview(BaseTestHttp):
def test_review_get_specific_event(self):
now = datetime.now().timestamp()
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
event_id = "123456.event.random"
super().insert_mock_event(event_id)
review_id = "123456.review.random"
@ -638,7 +626,7 @@ class TestHttpReview(BaseTestHttp):
################################### GET /review/{review_id} Endpoint #######################################
####################################################################################################################
def test_review_not_found(self):
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
response = client.get("/review/123456.random")
assert response.status_code == 404
response_json = response.json()
@ -650,7 +638,7 @@ class TestHttpReview(BaseTestHttp):
def test_get_review(self):
now = datetime.now().timestamp()
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
review_id = "123456.review.random"
super().insert_mock_review_segment(review_id, now + 1, now + 2)
response = client.get(f"/review/{review_id}")
@ -674,7 +662,7 @@ class TestHttpReview(BaseTestHttp):
####################################################################################################################
def test_delete_review_viewed_review_not_found(self):
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
review_id = "123456.random"
response = client.delete(f"/review/{review_id}/viewed")
assert response.status_code == 404
@ -687,7 +675,7 @@ class TestHttpReview(BaseTestHttp):
def test_delete_review_viewed(self):
now = datetime.now().timestamp()
with AuthTestClient(self.app) as client:
with TestClient(self.app) as client:
review_id = "123456.review.random"
super().insert_mock_review_segment(review_id, now + 1, now + 2)
self._insert_user_review_status(review_id, reviewed=True)

View File

@ -348,7 +348,7 @@ def migrate_016_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]
def migrate_017_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]:
"""Handle migrating frigate config to 0.17-0"""
"""Handle migrating frigate config to 0.16-0"""
new_config = config.copy()
# migrate global to new recording configuration
@ -380,7 +380,7 @@ def migrate_017_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]
if global_genai:
new_genai_config = {}
new_object_config = new_config.get("objects", {})
new_object_config = config.get("objects", {})
new_object_config["genai"] = {}
for key in global_genai.keys():
@ -389,8 +389,7 @@ def migrate_017_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]
else:
new_object_config["genai"][key] = global_genai[key]
new_config["genai"] = new_genai_config
new_config["objects"] = new_object_config
config["genai"] = new_genai_config
for name, camera in config.get("cameras", {}).items():
camera_config: dict[str, dict[str, Any]] = camera.copy()
@ -416,9 +415,8 @@ def migrate_017_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]
camera_genai = camera_config.get("genai", {})
if camera_genai:
camera_object_config = camera_config.get("objects", {})
camera_object_config["genai"] = camera_genai
camera_config["objects"] = camera_object_config
new_object_config = config.get("objects", {})
new_object_config["genai"] = camera_genai
del camera_config["genai"]
new_config["cameras"][name] = camera_config

View File

@ -1,10 +1,7 @@
import atexit
import faulthandler
import logging
import multiprocessing as mp
import os
import pathlib
import subprocess
import threading
from logging.handlers import QueueHandler
from multiprocessing.synchronize import Event as MpEvent
@ -51,7 +48,6 @@ class FrigateProcess(BaseProcess):
def before_start(self) -> None:
self.__log_queue = frigate.log.log_listener.queue
self.__memray_tracker = None
def pre_run_setup(self, logConfig: LoggerConfig | None = None) -> None:
os.nice(self.priority)
@ -68,86 +64,3 @@ class FrigateProcess(BaseProcess):
frigate.log.apply_log_levels(
logConfig.default.value.upper(), logConfig.logs
)
self._setup_memray()
def _setup_memray(self) -> None:
"""Setup memray profiling if enabled via environment variable."""
memray_modules = os.environ.get("FRIGATE_MEMRAY_MODULES", "")
if not memray_modules:
return
# Extract module name from process name (e.g., "frigate.capture:camera" -> "frigate.capture")
process_name = self.name
module_name = (
process_name.split(":")[0] if ":" in process_name else process_name
)
enabled_modules = [m.strip() for m in memray_modules.split(",")]
if module_name not in enabled_modules and process_name not in enabled_modules:
return
try:
import memray
reports_dir = pathlib.Path("/config/memray_reports")
reports_dir.mkdir(parents=True, exist_ok=True)
safe_name = (
process_name.replace(":", "_").replace("/", "_").replace("\\", "_")
)
binary_file = reports_dir / f"{safe_name}.bin"
self.__memray_tracker = memray.Tracker(str(binary_file))
self.__memray_tracker.__enter__()
# Register cleanup handler to stop tracking and generate HTML report
# atexit runs on normal exits and most signal-based terminations (SIGTERM, SIGINT)
# For hard kills (SIGKILL) or segfaults, the binary file is preserved for manual generation
atexit.register(self._cleanup_memray, safe_name, binary_file)
self.logger.info(
f"Memray profiling enabled for module {module_name} (process: {self.name}). "
f"Binary file (updated continuously): {binary_file}. "
f"HTML report will be generated on exit: {reports_dir}/{safe_name}.html. "
f"If process crashes, manually generate with: memray flamegraph {binary_file}"
)
except Exception as e:
self.logger.error(f"Failed to setup memray profiling: {e}", exc_info=True)
def _cleanup_memray(self, safe_name: str, binary_file: pathlib.Path) -> None:
"""Stop memray tracking and generate HTML report."""
if self.__memray_tracker is None:
return
try:
self.__memray_tracker.__exit__(None, None, None)
self.__memray_tracker = None
reports_dir = pathlib.Path("/config/memray_reports")
html_file = reports_dir / f"{safe_name}.html"
result = subprocess.run(
["memray", "flamegraph", "--output", str(html_file), str(binary_file)],
capture_output=True,
text=True,
timeout=10,
)
if result.returncode == 0:
self.logger.info(f"Memray report generated: {html_file}")
else:
self.logger.error(
f"Failed to generate memray report: {result.stderr}. "
f"Binary file preserved at {binary_file} for manual generation."
)
# Keep the binary file for manual report generation if needed
# Users can run: memray flamegraph {binary_file}
except subprocess.TimeoutExpired:
self.logger.error("Memray report generation timed out")
except Exception as e:
self.logger.error(f"Failed to cleanup memray profiling: {e}", exc_info=True)

6
web/package-lock.json generated
View File

@ -4702,9 +4702,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001757",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz",
"integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==",
"version": "1.0.30001651",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
"integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
"dev": true,
"funding": [
{

View File

@ -1,5 +1 @@
{
"form": {
"user": "Потребителско име"
}
}
{}

View File

@ -8,8 +8,5 @@
"lastHour_other": "Последните {{count}} часа"
},
"select": "Избери"
},
"restart": {
"title": "Сигурен ли сте, че искате да рестартирате Frigate?"
}
}

View File

@ -1,5 +1 @@
{
"iconPicker": {
"selectIcon": "Изберете иконка"
}
}
{}

View File

@ -1,7 +1 @@
{
"button": {
"downloadVideo": {
"label": "Свали видео"
}
}
}
{}

View File

@ -18,6 +18,5 @@
"bicycle": "Велосипед",
"skateboard": "Скейтборд",
"door": "Врата",
"blender": "Блендер",
"person": "Човек"
"blender": "Блендер"
}

View File

@ -1,3 +1 @@
{
"documentTitle": "Експорт - Frigate"
}
{}

View File

@ -63,6 +63,5 @@
},
"cameraSettings": {
"cameraEnabled": "Камерата е включена"
},
"documentTitle": "Наживо - Frigate"
}
}

View File

@ -104,8 +104,7 @@
"anonymous": "Anònim",
"logout": "Tanca la sessió",
"current": "Usuari actual: {{user}}"
},
"classification": "Classificació"
}
},
"pagination": {
"previous": {
@ -198,10 +197,7 @@
"formattedTimestampMonthDayYearHourMinute": {
"12hour": "MMM d yyyy, h:mm aaa",
"24hour": "MMM d yyyy, HH:mm"
},
"inProgress": "En curs",
"invalidStartTime": "Hora d'inici no vàlida",
"invalidEndTime": "Hora de finalització no vàlida"
}
},
"unit": {
"speed": {
@ -225,9 +221,7 @@
"back": "Torna enrere",
"hide": "Oculta {{item}}",
"show": "Mostra {{item}}",
"ID": "ID",
"none": "Cap",
"all": "Tots"
"ID": "ID"
},
"button": {
"apply": "Aplicar",
@ -264,8 +258,7 @@
"off": "APAGAT",
"unselect": "Desseleccionar",
"enable": "Habilitar",
"enabled": "Habilitat",
"continue": "Continua"
"enabled": "Habilitat"
},
"toast": {
"copyUrlToClipboard": "URL copiada al porta-retalls.",

View File

@ -17,40 +17,31 @@
"categorizedImage": "Imatge classificada amb èxit",
"trainedModel": "Model entrenat amb èxit.",
"trainingModel": "S'ha iniciat amb èxit la formació de models.",
"deletedModel_one": "S'ha suprimit correctament {{count}} model",
"deletedModel_many": "S'han suprimit correctament els {{count}} models",
"deletedModel_other": "S'han suprimit correctament els {{count}} models",
"updatedModel": "S'ha actualitzat correctament la configuració del model",
"renamedCategory": "S'ha canviat el nom de la classe a {{name}}"
"deletedModel_one": "S'ha suprimit correctament el model {{count}}",
"deletedModel_many": "S'han suprimit correctament {{count}} models",
"deletedModel_other": "",
"updatedModel": "S'ha actualitzat correctament la configuració del model"
},
"error": {
"deleteImageFailed": "No s'ha pogut suprimir: {{errorMessage}}",
"deleteCategoryFailed": "No s'ha pogut suprimir la classe: {{errorMessage}}",
"categorizeFailed": "No s'ha pogut categoritzar la imatge: {{errorMessage}}",
"trainingFailed": "Ha fallat l'entrenament del model. Comproveu els registres de fragata per a més detalls.",
"trainingFailed": "No s'ha pogut iniciar l'entrenament del model: {{errorMessage}}",
"deleteModelFailed": "No s'ha pogut suprimir el model: {{errorMessage}}",
"updateModelFailed": "No s'ha pogut actualitzar el model: {{errorMessage}}",
"renameCategoryFailed": "No s'ha pogut canviar el nom de la classe: {{errorMessage}}",
"trainingFailedToStart": "Errar en arrencar l'entrenament del model: {{errorMessage}}"
"updateModelFailed": "No s'ha pogut actualitzar el model: {{errorMessage}}"
}
},
"deleteCategory": {
"title": "Suprimeix la classe",
"desc": "Esteu segur que voleu suprimir la classe {{name}}? Això suprimirà permanentment totes les imatges associades i requerirà tornar a entrenar el model.",
"minClassesTitle": "No es pot suprimir la classe",
"minClassesDesc": "Un model de classificació ha de tenir almenys 2 classes. Afegeix una altra classe abans d'eliminar aquesta."
"desc": "Esteu segur que voleu suprimir la classe {{name}}? Això suprimirà permanentment totes les imatges associades i requerirà tornar a entrenar el model."
},
"deleteDatasetImages": {
"title": "Suprimeix les imatges del conjunt de dades",
"desc_one": "Esteu segur que voleu suprimir {{count}} imatge de {{dataset}}? Aquesta acció no es pot desfer i requerirà tornar a entrenar el model.",
"desc_many": "Esteu segur que voleu suprimir {{count}} imatges de {{dataset}}? Aquesta acció no es pot desfer i requerirà tornar a entrenar el model.",
"desc_other": "Esteu segur que voleu suprimir {{count}} imatges de {{dataset}}? Aquesta acció no es pot desfer i requerirà tornar a entrenar el model."
"desc": "Esteu segur que voleu suprimir {{count}} imatges de {{dataset}}? Aquesta acció no es pot desfer i requerirà tornar a entrenar el model."
},
"deleteTrainImages": {
"title": "Suprimeix les imatges del tren",
"desc_one": "Esteu segur que voleu suprimir {{count}} imatge? Aquesta acció no es pot desfer.",
"desc_many": "Esteu segur que voleu suprimir {{count}} imatges? Aquesta acció no es pot desfer.",
"desc_other": "Esteu segur que voleu suprimir {{count}} imatges? Aquesta acció no es pot desfer."
"desc": "Esteu segur que voleu suprimir {{count}} imatges? Aquesta acció no es pot desfer."
},
"renameCategory": {
"title": "Reanomena la classe",
@ -149,18 +140,13 @@
"generationFailed": "Ha fallat la generació. Torneu-ho a provar.",
"classifyFailed": "No s'han pogut classificar les imatges: {{error}}"
},
"generateSuccess": "Imatges de mostra generades amb èxit",
"allImagesRequired_one": "Classifiqueu totes les imatges. Queda {{count}} imatge.",
"allImagesRequired_many": "Classifiqueu totes les imatges. Queden {{count}} imatges.",
"allImagesRequired_other": "Classifiqueu totes les imatges. Queden {{count}} imatges."
"generateSuccess": "Imatges de mostra generades amb èxit"
}
},
"deleteModel": {
"title": "Suprimeix el model de classificació",
"single": "Esteu segur que voleu suprimir {{name}}? Això suprimirà permanentment totes les dades associades, incloses les imatges i les dades d'entrenament. Aquesta acció no es pot desfer.",
"desc_one": "Esteu segur que voleu suprimir el model {{count}}? Això suprimirà permanentment totes les dades associades, incloses les imatges i les dades d'entrenament. Aquesta acció no es pot desfer.",
"desc_many": "Esteu segur que voleu suprimir {{count}} models? Això suprimirà permanentment totes les dades associades, incloses les imatges i les dades d'entrenament. Aquesta acció no es pot desfer.",
"desc_other": "Esteu segur que voleu suprimir {{count}} models? Això suprimirà permanentment totes les dades associades, incloses les imatges i les dades d'entrenament. Aquesta acció no es pot desfer."
"desc": "Esteu segur que voleu suprimir {{count}} model(s)? Això suprimirà permanentment totes les dades associades, incloses les imatges i les dades d'entrenament. Aquesta acció no es pot desfer."
},
"menu": {
"objects": "Objectes",
@ -174,11 +160,5 @@
"descriptionState": "Edita les classes per a aquest model de classificació d'estats. Els canvis requeriran tornar a entrenar el model.",
"descriptionObject": "Edita el tipus d'objecte i el tipus de classificació per a aquest model de classificació d'objectes.",
"stateClassesInfo": "Nota: Canviar les classes d'estat requereix tornar a entrenar el model amb les classes actualitzades."
},
"tooltip": {
"trainingInProgress": "El model s'està entrenant actualment",
"noNewImages": "Sense noves imatges per entrenar. Classifica més imatges primer.",
"modelNotReady": "El model no está preparat per entrenar",
"noChanges": "No hi ha canvis al conjunt de dades des de l'última formació."
}
}

View File

@ -39,9 +39,9 @@
"threateningActivity": "Activitat amenaçadora",
"detail": {
"noDataFound": "No hi ha dades detallades a revisar",
"trackedObject_one": "{{count}} objecte",
"trackedObject_one": "objecte",
"aria": "Canvia la vista de detall",
"trackedObject_other": "{{count}} objectes",
"trackedObject_other": "objectes",
"noObjectDetailData": "No hi ha dades de detall d'objecte disponibles.",
"label": "Detall",
"settings": "Configuració de la vista detallada",

View File

@ -85,8 +85,7 @@
"snapshot": "instantània",
"video": "vídeo",
"object_lifecycle": "cicle de vida de l'objecte",
"thumbnail": "miniatura",
"tracking_details": "detalls del seguiment"
"thumbnail": "miniatura"
},
"details": {
"timestamp": "Marca temporal",
@ -100,7 +99,7 @@
"updatedSublabel": "Subetiqueta actualitzada amb èxit.",
"updatedLPR": "Matrícula actualitzada amb èxit.",
"regenerate": "El {{provider}} ha sol·licitat una nova descripció. En funció de la velocitat del vostre proveïdor, la nova descripció pot trigar un temps a regenerar-se.",
"audioTranscription": "S'ha sol·licitat correctament la transcripció d'àudio. Depenent de la velocitat del vostre servidor Frigate, la transcripció pot trigar una estona a completar-se."
"audioTranscription": "Transcripció d'àudio sol·licitada amb èxit."
},
"error": {
"regenerate": "No s'ha pogut contactar amb {{provider}} per obtenir una nova descripció: {{errorMessage}}",
@ -171,9 +170,7 @@
"success": "L'objectes amb seguiment s'ha suprimit correctament.",
"error": "No s'ha pogut suprimir l'objecte rastrejat: {{errorMessage}}"
}
},
"nextTrackedObject": "Següent objecte rastrejat",
"previousTrackedObject": "Objecte rastrejat anterior"
}
},
"itemMenu": {
"downloadVideo": {
@ -276,7 +273,7 @@
"label": "Òfset d'Anotació",
"desc": "Aquestes dades provenen del flux de detecció de la càmera, però se superposen a les imatges del flux de gravació. És poc probable que els dos fluxos estiguin perfectament sincronitzats. Com a resultat, el quadre delimitador i les imatges no s'alinearan perfectament. Tanmateix, es pot utilitzar el camp <code>annotation_offset</code> per ajustar-ho.",
"millisecondsToOffset": "Millisegons per l'òfset de detecció d'anotacions per. <em>Per defecte: 0</em>",
"tips": "Reduïu el valor si la reproducció del vídeo es troba per davant dels quadres i els punts de ruta, i augmenteu-lo si es troba per darrere. Aquest valor pot ser negatiu.",
"tips": "CONSELL: Imagineu-vos que hi ha un clip d'esdeveniment amb una persona caminant d'esquerra a dreta. Si el quadre delimitador de la cronologia de l'esdeveniment està constantment a l'esquerra de la persona, aleshores s'hauria de disminuir el valor. De la mateixa manera, si una persona camina d'esquerra a dreta i el quadre delimitador està constantment per davant de la persona, aleshores s'hauria d'augmentar el valor.",
"toast": {
"success": "L'Òfset d'anotació per a {{camera}} s'ha desat al fitxer de configuració. Reinicieu Frigate per aplicar els canvis."
}

View File

@ -67,7 +67,7 @@
"toast": {
"success": {
"trainedFace": "Rostre entrenat amb èxit.",
"updatedFaceScore": "S'ha actualitzat correctament la puntuació de la cara a {{name}} ({{score}}).",
"updatedFaceScore": "Puntació de rostre actualitzada amb èxit.",
"uploadedImage": "Imatge pujada amb èxit.",
"addFaceLibrary": "{{name}} s'ha afegit amb èxit a la biblioteca de rostres!",
"deletedName_one": "{{count}} rostre s'ha suprimit amb èxit.",

View File

@ -180,10 +180,6 @@
"noCameras": {
"title": "No s'ha configurat cap càmera",
"description": "Comenceu connectant una càmera a Frigate.",
"buttonText": "Afegeix una càmera",
"restricted": {
"title": "No hi ha càmeres disponibles",
"description": "No teniu permís per veure cap càmera en aquest grup."
}
"buttonText": "Afegeix una càmera"
}
}

View File

@ -7,7 +7,7 @@
"authentication": "Configuració d'autenticació - Frigate",
"camera": "Paràmetres de càmera - Frigate",
"masksAndZones": "Editor de màscares i zones - Frigate",
"general": "Configuració de la interfície d'usuari - Fragata",
"general": "Paràmetres Generals - Frigate",
"frigatePlus": "Paràmetres de Frigate+ - Frigate",
"notifications": "Paràmetres de notificació - Frigate",
"cameraManagement": "Gestionar càmeres - Frigate",
@ -39,7 +39,7 @@
"noCamera": "Cap càmera"
},
"general": {
"title": "Paràmetres de la interfície d'usuari",
"title": "Paràmetres generals",
"liveDashboard": {
"title": "Panell en directe",
"automaticLiveView": {
@ -53,10 +53,6 @@
"displayCameraNames": {
"label": "Mostra sempre els noms de la càmera",
"desc": "Mostra sempre els noms de les càmeres en un xip al tauler de visualització en directe multicàmera."
},
"liveFallbackTimeout": {
"label": "Temps d'espera per a la reserva del jugador en directe",
"desc": "Quan el flux en viu d'alta qualitat d'una càmera no està disponible, torneu al mode d'amplada de banda baixa després d'aquests molts segons. Per defecte: 3."
}
},
"storedLayouts": {
@ -862,9 +858,9 @@
"createRole": "Rol {{role}} creat exitosament",
"updateCameras": "Càmeres actualitzades per al rol {{role}}",
"deleteRole": "Rol {{role}} eliminat exitosament",
"userRolesUpdated_one": "{{count}} l'usuari assignat a aquest rol s'ha actualitzat a 'visor', que té accés a totes les càmeres.",
"userRolesUpdated_many": "{{count}} usuaris assignats a aquest rol s'han actualitzat a 'visor', que té accés a totes les càmeres.",
"userRolesUpdated_other": "{{count}} usuaris assignats a aquest rol s'han actualitzat a 'visor', que té accés a totes les càmeres."
"userRolesUpdated_one": "{{count}} usuari(s) asignats a aquest rol s'han actualitzat a 'visor', i tenen accés a totes les càmeres.",
"userRolesUpdated_many": "",
"userRolesUpdated_other": ""
},
"error": {
"createRoleFailed": "Error al crear el rol: {{errorMessage}}",
@ -875,17 +871,16 @@
}
},
"cameraWizard": {
"title": "Afegir Càmera",
"title": "Afegir C àmera",
"description": "Seguiu els passos de sota per afegir una nova càmera a la instal·lació.",
"steps": {
"nameAndConnection": "Nom i connexió",
"streamConfiguration": "Configuració de stream",
"validationAndTesting": "Validació i proves",
"probeOrSnapshot": "Prova o instantània"
"validationAndTesting": "Validació i proves"
},
"step1": {
"cameraBrand": "Marca de la càmera",
"description": "Introduïu els detalls de la càmera i trieu provar la càmera o seleccionar manualment la marca.",
"description": "Introduïu els detalls de la càmera i proveu la connexió.",
"cameraName": "Nom de la càmera",
"cameraNamePlaceholder": "p. ex., vista general de la porta davantera o de la barra posterior",
"host": "Adreça de l'amfitrió/IP",
@ -924,16 +919,7 @@
"testing": {
"probingMetadata": "S'estan provant les metadades de la càmera...",
"fetchingSnapshot": "S'està recuperant la instantània de la càmera..."
},
"connectionSettings": "Configuració de la connexió",
"detectionMethod": "Mètode de detecció de flux",
"onvifPort": "ONVIF Port",
"probeMode": "Càmera de prova",
"manualMode": "Selecció manual",
"detectionMethodDescription": "Proveu la càmera amb ONVIF (si és compatible) per trobar URL de flux de càmera, o seleccioneu manualment la marca de càmera per utilitzar URL predefinits. Per a introduir un URL RTSP personalitzat, trieu el mètode manual i seleccioneu \"Altres\".",
"onvifPortDescription": "Per a les càmeres que suporten ONVIF, això sol ser 80 o 8080.",
"useDigestAuth": "Utilitza l'autenticació digest",
"useDigestAuthDescription": "Usa l'autenticació de resum HTTP per a ONVIF. Algunes càmeres poden requerir un nom d'usuari/contrasenya ONVIF dedicat en lloc de l'usuari administrador estàndard."
}
},
"save": {
"failure": "SS'ha produït un error en desar {{cameraName}}.",
@ -950,7 +936,7 @@
"testFailed": "Ha fallat la prova de flux: {{error}}"
},
"step2": {
"description": "Proveu la càmera per als fluxos disponibles o configureu la configuració manual basada en el mètode de detecció seleccionat.",
"description": "Configura els rols de flux i afegeix fluxos addicionals per a la càmera.",
"streamsTitle": "Fluxos de la càmera",
"addStream": "Afegeix un flux",
"addAnotherStream": "Afegeix un altre flux",
@ -967,8 +953,8 @@
"audio": "Àudio"
},
"testStream": "Prova la connexió",
"testSuccess": "Prova de connexió correcta!",
"testFailed": "Ha fallat la prova de connexió. Si us plau, comproveu la vostra entrada i torneu-ho a provar.",
"testSuccess": "Prova de flux amb èxit!",
"testFailed": "Ha fallat la prova del flux",
"testFailedTitle": "Ha fallat la prova",
"connected": "Connectat",
"notConnected": "No connectat",
@ -986,39 +972,7 @@
"description": "Utilitzeu el restreaming go2rtc per reduir les connexions a la càmera."
},
"roles": "Rols",
"streamUrlPlaceholder": "rtsp://usuari:contrasenya@host:port/ruta",
"streamDetails": "Detalls del flux",
"probing": "Provant càmera...",
"retry": "Intentar de nou",
"testing": {
"probingMetadata": "S'estan provant les metadades de la càmera...",
"fetchingSnapshot": "S'està recuperant la instantània de la càmera..."
},
"probeFailed": "No s'ha pogut provar la càmera: {{error}}",
"probingDevice": "Provant dispositiu...",
"probeSuccessful": "Prova exitosa",
"probeError": "Error de prova",
"probeNoSuccess": "La prova no ha tingut èxit",
"deviceInfo": "Informació del dispositiu",
"manufacturer": "Fabricant",
"model": "Model",
"firmware": "Firmware",
"profiles": "Perfils",
"ptzSupport": "Suport PTZ",
"autotrackingSupport": "Implementació de seguiment automàtic",
"presets": "Predefinits",
"rtspCandidates": "Candidats RTSP",
"rtspCandidatesDescription": "S'han trobat els següents URL RTSP de la sonda de la càmera. Proveu la connexió per a veure les metadades del flux.",
"noRtspCandidates": "No s'ha trobat cap URL RTSP a la càmera. Les vostres credencials poden ser incorrectes, o la càmera pot no admetre ONVIF o el mètode utilitzat per recuperar els URL RTSP. Torneu enrere i introduïu l'URL RTSP manualment.",
"candidateStreamTitle": "Candidat {{number}}",
"useCandidate": "Utilitza",
"uriCopy": "Copia",
"uriCopied": "URI copiat al porta-retalls",
"testConnection": "Prova la connexió",
"toggleUriView": "Feu clic per a commutar la vista completa de l'URI",
"errors": {
"hostRequired": "Es requereix l'adreça de l'amfitrió/IP"
}
"streamUrlPlaceholder": "rtsp://usuari:contrasenya@host:port/ruta"
},
"step3": {
"none": "Cap",
@ -1042,7 +996,7 @@
"resolutionHigh": "Una resolució de {{resolution}} pot causar un ús més gran dels recursos.",
"resolutionLow": "Una resolució de {{resolution}} pot ser massa baixa per a la detecció fiable d'objectes petits."
},
"description": "Configura els rols de flux i afegeix fluxos addicionals per a la càmera.",
"description": "Validació i anàlisi final abans de desar la nova càmera. Connecta cada flux abans de desar-lo.",
"validationTitle": "Validació del flux",
"connectAllStreams": "Connecta tots els fluxos",
"reconnectionSuccess": "S'ha reconnectat correctament.",
@ -1062,91 +1016,7 @@
"streamValidated": "El flux {{number}} s'ha validat correctament",
"streamValidationFailed": "Ha fallat la validació del flux {{number}}",
"ffmpegModule": "Usa el mode de compatibilitat del flux",
"ffmpegModuleDescription": "Si el flux no es carrega després de diversos intents, proveu d'activar-ho. Quan està activat, Frigate utilitzarà el mòdul ffmpeg amb go2rtc. Això pot proporcionar una millor compatibilitat amb alguns fluxos de càmera.",
"streamsTitle": "Fluxos de la càmera",
"addStream": "Afegeix un flux",
"addAnotherStream": "Afegeix un altre flux",
"streamUrl": "URL del flux",
"streamUrlPlaceholder": "rtsp://usuari:contrasenya@host:port/ruta",
"selectStream": "Selecciona un flux",
"searchCandidates": "Cerca candidats...",
"noStreamFound": "No s'ha trobat cap flux",
"url": "URL",
"resolution": "Resolució",
"selectResolution": "Selecciona la resolució",
"quality": "Qualitat",
"selectQuality": "Selecciona la qualitat",
"roleLabels": {
"detect": "Detecció d'objectes",
"record": "Enregistrament",
"audio": "Àudio"
},
"testStream": "Prova la connexió",
"testSuccess": "Prova de flux amb èxit!",
"testFailed": "Ha fallat la prova del flux",
"testFailedTitle": "Ha fallat la prova",
"connected": "Connectat",
"notConnected": "No connectat",
"featuresTitle": "Característiques",
"go2rtc": "Redueix les connexions a la càmera",
"detectRoleWarning": "Almenys un flux ha de tenir el rol de \"detecte\" per continuar.",
"rolesPopover": {
"title": "Roles de flux",
"detect": "Canal principal per a la detecció d'objectes.",
"record": "Desa els segments del canal de vídeo basats en la configuració.",
"audio": "Canal per a la detecció basada en àudio."
},
"featuresPopover": {
"title": "Característiques del flux",
"description": "Utilitzeu el restreaming go2rtc per reduir les connexions a la càmera."
}
},
"step4": {
"description": "Validació i anàlisi final abans de desar la nova càmera. Connecta cada flux abans de desar-lo.",
"validationTitle": "Validació del flux",
"connectAllStreams": "Connecta tots els fluxos",
"reconnectionSuccess": "S'ha reconnectat correctament.",
"reconnectionPartial": "Alguns fluxos no s'han pogut tornar a connecta.",
"streamUnavailable": "La vista prèvia del flux no està disponible",
"reload": "Torna a carregar",
"connecting": "S'està connectant...",
"streamTitle": "Flux {{number}}",
"valid": "Vàlid",
"failed": "Ha fallat",
"notTested": "No provat",
"connectStream": "Connecta",
"connectingStream": "Connectant",
"disconnectStream": "Desconnecta",
"estimatedBandwidth": "Amplada de banda estimada",
"roles": "Roles",
"ffmpegModule": "Usa el mode de compatibilitat del flux",
"ffmpegModuleDescription": "Si el flux no es carrega després de diversos intents, proveu d'activar-ho. Quan està activat, Frigate utilitzarà el mòdul ffmpeg amb go2rtc. Això pot proporcionar una millor compatibilitat amb alguns fluxos de càmera.",
"none": "Cap",
"error": "Error",
"streamValidated": "El flux {{number}} s'ha validat correctament",
"streamValidationFailed": "Ha fallat la validació del flux {{number}}",
"saveAndApply": "Desa una càmera nova",
"saveError": "Configuració no vàlida. Si us plau, comproveu la configuració.",
"issues": {
"title": "Validació del flux",
"videoCodecGood": "El còdec de vídeo és {{codec}}.",
"audioCodecGood": "El còdec d'àudio és {{codec}}.",
"resolutionHigh": "Una resolució de {{resolution}} pot causar un ús més gran dels recursos.",
"resolutionLow": "Una resolució de {{resolution}} pot ser massa baixa per a la detecció fiable d'objectes petits.",
"noAudioWarning": "No s'ha detectat cap àudio per a aquest flux, els enregistraments no tindran àudio.",
"audioCodecRecordError": "El còdec d'àudio AAC és necessari per a suportar l'àudio en els enregistraments.",
"audioCodecRequired": "Es requereix un flux d'àudio per admetre la detecció d'àudio.",
"restreamingWarning": "Reduir les connexions a la càmera per al flux de registre pot augmentar lleugerament l'ús de la CPU.",
"brands": {
"reolink-rtsp": "No és racomana utilitzar Reolink RSTP. Activeu HTTP a la configuració del microprogramari de la càmera i reinicieu l'assistent."
},
"dahua": {
"substreamWarning": "El substream 1 està bloquejat a una resolució baixa. Moltes càmeres Dahua / Amcrest / EmpireTech suporten subfluxos addicionals que han d'estar habilitats a la configuració de la càmera. Es recomana comprovar i utilitzar aquests corrents si estan disponibles."
},
"hikvision": {
"substreamWarning": "El substream 1 està bloquejat a una resolució baixa. Moltes càmeres Hikvision suporten subfluxos addicionals que han d'estar habilitats a la configuració de la càmera. Es recomana comprovar i utilitzar aquests corrents si estan disponibles."
}
}
"ffmpegModuleDescription": "Si el flux no es carrega després de diversos intents, proveu d'activar-ho. Quan està activat, Frigate utilitzarà el mòdul ffmpeg amb go2rtc. Això pot proporcionar una millor compatibilitat amb alguns fluxos de càmera."
}
},
"cameraManagement": {

View File

@ -76,12 +76,7 @@
}
},
"npuUsage": "Ús de NPU",
"npuMemory": "Memòria de NPU",
"intelGpuWarning": {
"title": "Avís d'estadístiques de la GPU d'Intel",
"message": "Estadístiques de GPU no disponibles",
"description": "Aquest és un error conegut en les eines d'informació de les estadístiques de GPU d'Intel (intel.gpu.top) on es trencarà i retornarà repetidament un ús de GPU del 0% fins i tot en els casos en què l'acceleració del maquinari i la detecció d'objectes s'executen correctament a la (i)GPU. Això no és un error de fragata. Podeu reiniciar l'amfitrió per a corregir temporalment el problema i confirmar que la GPU funciona correctament. Això no afecta el rendiment."
}
"npuMemory": "Memòria de NPU"
},
"otherProcesses": {
"title": "Altres processos",
@ -184,15 +179,8 @@
"plate_recognition_speed": "Velocitat de reconeixement de matrícules",
"text_embedding_speed": "Velocitat d'incrustació de text",
"yolov9_plate_detection": "Detecció de matrícules YOLOv9",
"yolov9_plate_detection_speed": "Velocitat de detecció de matrícules YOLOv9",
"review_description": "Descripció de la revisió",
"review_description_speed": "Velocitat de la descripció de la revisió",
"review_description_events_per_second": "Descripció de la revisió",
"object_description": "Descripció de l'objecte",
"object_description_speed": "Velocitat de la descripció de l'objecte",
"object_description_events_per_second": "Descripció de l'objecte"
"yolov9_plate_detection_speed": "Velocitat de detecció de matrícules YOLOv9"
},
"infPerSecond": "Inferències per segon",
"averageInf": "Temps mitjà d'inferència"
"infPerSecond": "Inferències per segon"
}
}

View File

@ -1,31 +1,7 @@
{
"documentTitle": "Klasifikační modely",
"button": {
"deleteClassificationAttempts": "Odstrániť Klasifikačné obrazy",
"renameCategory": "Premenovať triedu",
"deleteCategory": "Zmazať triedu",
"deleteImages": "Zmazať obrázok",
"trainModel": "Trenovací model",
"addClassification": "Pridať klasifikáciu",
"deleteModels": "Zmazať modeli",
"editModel": "Upraviť model"
},
"details": {
"scoreInfo": "Skóre predstavuje priemernú istotu klasifikácie naprieč detekciami tohoto objektu."
},
"tooltip": {
"trainingInProgress": "Model se práve trénuje",
"noNewImages": "Žiadne nové obrázky na trénovanie. Najskôr klasifikujte viac obrazkov v datasete.",
"noChanges": "Od posledného treningu nedošlo k žiadnym zmenám v datasete.",
"modelNotReady": "Model nieje pripravený na trénovanie."
},
"toast": {
"success": {
"deletedImage": "Zmazať obrazky",
"deletedModel_one": "Úspešne odstranený {{count}} model",
"deletedModel_few": "Úspešne odstranené {{count}} modely",
"deletedModel_other": "Úspěšne ostranených {{count}} modelov",
"deletedCategory": "Zmazať triedu"
}
"deleteClassificationAttempts": "Odstranit Klasifikační obrazy",
"renameCategory": "Přejmenovat třídu"
}
}

View File

@ -36,11 +36,5 @@
"selected_one": "{{count}} vybráno",
"selected_other": "{{count}} vybráno",
"suspiciousActivity": "Podezřelá aktivita",
"threateningActivity": "Ohrožující činnost",
"zoomIn": "Přiblížit",
"zoomOut": "Oddálit",
"detail": {
"label": "Detail",
"noDataFound": "Žádná detailní data k prohlédnutí"
}
"threateningActivity": "Ohrožující činnost"
}

View File

@ -13,11 +13,5 @@
"error": {
"renameExportFailed": "Nepodařilo se přejmenovat export: {{errorMessage}}"
}
},
"tooltip": {
"shareExport": "Sdílet export",
"downloadVideo": "Stáhnout video",
"deleteExport": "Smazat export",
"editName": "Upravit jméno"
}
}

View File

@ -306,8 +306,7 @@
"notifications": "Notifikace",
"frigateplus": "Frigate+",
"enrichments": "Obohacení",
"triggers": "Spouštěče",
"cameraManagement": "Správa"
"triggers": "Spouštěče"
},
"dialog": {
"unsavedChanges": {

View File

@ -80,9 +80,5 @@
"hammer": "Hammer",
"drill": "Bore",
"explosion": "Eksplosion",
"fireworks": "Nytårskrudt",
"babbling": "Pludren",
"yell": "Råb",
"whoop": "Jubel",
"snicker": "Smålatter"
"fireworks": "Nytårskrudt"
}

View File

@ -7,9 +7,7 @@
"usernameRequired": "Brugernavn kræves",
"passwordRequired": "Kodeord kræves",
"loginFailed": "Login fejlede",
"unknownError": "Ukendt fejl. Tjek logs.",
"rateLimit": "Grænsen for forespørgsler er overskredet. Prøv igen senere."
},
"firstTimeLogin": "Forsøger du at logge ind for første gang? Loginoplysningerne står i Frigate-loggene."
"unknownError": "Ukendt fejl. Tjek logs."
}
}
}

View File

@ -11,11 +11,7 @@
}
},
"name": {
"label": "Navn",
"placeholder": "Indtast et navn…",
"errorMessage": {
"mustLeastCharacters": "Kameragruppens navn skal være mindst 2 tegn."
}
"label": "Navn"
}
}
}

View File

@ -3,23 +3,7 @@
"title": "Er du sikker på at du vil genstarte Frigate?",
"button": "Genstart",
"restarting": {
"title": "Frigate genstarter",
"button": "Gennemtving genindlæsning nu",
"content": "Denne side genindlæses om {{countdown}} sekunder."
}
},
"explore": {
"plus": {
"submitToPlus": {
"label": "Indsend til Frigate+",
"desc": "Objekter på steder, du ønsker at undgå, er ikke falske positiver. Hvis du indsender dem som falske positiver, vil det forvirre modellen."
},
"review": {
"question": {
"label": "Bekræft denne etiket til Frigate Plus",
"ask_a": "Er dette objekt et <code>{{label}}</code>?"
}
}
"title": "Frigate genstarter"
}
}
}

View File

@ -10,10 +10,8 @@
},
"labels": {
"all": {
"short": "Labels",
"title": "Alle etiketter"
"short": "Labels"
},
"count_one": "{{count}} Label",
"label": "Etiketter"
"count_one": "{{count}} Label"
}
}

View File

@ -1,10 +1,7 @@
{
"button": {
"downloadVideo": {
"label": "Download Video",
"toast": {
"success": "Din video til gennemgang er begyndt at blive downloadet."
}
"label": "Download Video"
}
}
}

View File

@ -1,15 +1,5 @@
{
"noRecordingsFoundForThisTime": "Ingen optagelser fundet i det angivet tidsrum",
"noPreviewFound": "Ingen forhåndsvisning fundet",
"cameraDisabled": "Kamera er deaktiveret",
"noPreviewFoundFor": "Ingen forhåndsvisning fundet for {{cameraName}}",
"submitFrigatePlus": {
"title": "Indsend denne frame til Frigate+?",
"submit": "Indsend"
},
"livePlayerRequiredIOSVersion": "iOS 17.1 eller nyere kræves for denne type livestream.",
"streamOffline": {
"title": "Stream offline",
"desc": "Der er ikke modtaget nogen frames på {{cameraName}}-<code>detect</code>-streamen, tjek fejlloggene."
}
"cameraDisabled": "Kamera er deaktiveret"
}

View File

@ -1,17 +1 @@
{
"documentTitle": "Klassifikationsmodeller",
"details": {
"scoreInfo": "Scoren repræsenterer den gennemsnitlige klassifikationssikkerhed på tværs af alle registreringer af dette objekt."
},
"description": {
"invalidName": "Ugyldigt navn. Navne må kun indeholde bogstaver, tal, mellemrum, apostroffer, understregninger og bindestreger."
},
"button": {
"deleteClassificationAttempts": "Slet klassifikationsbilleder",
"renameCategory": "Omdøb klasse",
"deleteCategory": "Slet klasse",
"deleteImages": "Slet billeder",
"trainModel": "Træn model",
"addClassification": "Tilføj klassifikation"
}
}
{}

View File

@ -2,9 +2,5 @@
"documentTitle": "Konfigurationsstyring - Frigate",
"copyConfig": "Kopiér konfiguration",
"saveAndRestart": "Gem & Genstart",
"saveOnly": "Kun gem",
"configEditor": "Konfigurationseditor",
"safeConfigEditor": "Konfigurationseditor (Sikker tilstand)",
"safeModeDescription": "Frigate er i sikker tilstand på grund af en fejl ved validering af konfigurationen.",
"confirm": "Afslut uden at gemme?"
"saveOnly": "Kun gem"
}

View File

@ -7,10 +7,5 @@
},
"allCameras": "Alle kameraer",
"timeline": "Tidslinje",
"camera": "Kamera",
"empty": {
"alert": "Der er ingen advarsler at gennemgå",
"detection": "Der er ingen registreringer at gennemgå",
"motion": "Ingen bevægelsesdata fundet"
}
"camera": "Kamera"
}

View File

@ -13,17 +13,7 @@
"exploreIsUnavailable": {
"embeddingsReindexing": {
"startingUp": "Starter…",
"estimatedTime": "Estimeret tid tilbage:",
"context": "Udforsk kan bruges, når genindekseringen af de sporede objektindlejringer er fuldført.",
"finishingShortly": "Afsluttes om lidt",
"step": {
"thumbnailsEmbedded": "Miniaturer indlejret: "
}
},
"title": "Udforsk er ikke tilgængelig"
},
"exploreMore": "Udforsk flere {{label}}-objekter",
"details": {
"timestamp": "Tidsstempel"
"estimatedTime": "Estimeret tid tilbage:"
}
}
}

View File

@ -4,9 +4,6 @@
"deleteExport.desc": "Er du sikker på at du vil slette {{exportName}}?",
"editExport": {
"title": "Omdøb Eksport",
"saveExport": "Gem Eksport",
"desc": "Indtast et nyt navn for denne eksport."
},
"noExports": "Ingen eksporter fundet",
"deleteExport": "Slet eksport"
"saveExport": "Gem Eksport"
}
}

View File

@ -1,19 +1,10 @@
{
"selectItem": "Vælg {{item}}",
"description": {
"addFace": "Tilføj en ny samling til ansigtsbiblioteket ved at uploade dit første billede.",
"placeholder": "Angiv et navn for bibliotek",
"invalidName": "Ugyldigt navn. Navne må kun indeholde bogstaver, tal, mellemrum, apostroffer, understregninger og bindestreger."
"addFace": "Gennemgang af tilføjelse til ansigts bibliotek",
"placeholder": "Angiv et navn for bibliotek"
},
"details": {
"person": "Person",
"timestamp": "Tidsstempel",
"unknown": "Ukendt",
"scoreInfo": "Scoren er et vægtet gennemsnit af alle ansigtsscorer, vægtet efter ansigtets størrelse på hvert billede."
},
"documentTitle": "Ansigtsbibliotek - Frigate",
"uploadFaceImage": {
"title": "Upload ansigtsbillede",
"desc": "Upload et billede for at scanne efter ansigter og inkludere det for {{pageToggle}}"
"person": "Person"
}
}

View File

@ -8,14 +8,5 @@
"cameraAudio": {
"enable": "Aktivér kameralyd",
"disable": "Deaktivér kamera lyd"
},
"lowBandwidthMode": "Lavbåndbredde-tilstand",
"ptz": {
"move": {
"clickMove": {
"label": "Klik i billedrammen for at centrere kameraet",
"enable": "Aktivér klik for at flytte"
}
}
}
}

View File

@ -5,8 +5,7 @@
"filters": "Filtere",
"toast": {
"error": {
"endTimeMustAfterStartTime": "Sluttidspunkt skal være efter starttidspunkt",
"noValidTimeSelected": "Intet gyldigt tidsinterval valgt"
"endTimeMustAfterStartTime": "Sluttidspunkt skal være efter starttidspunkt"
}
}
}

View File

@ -6,7 +6,6 @@
"save": "Gem søgning",
"delete": "Slet gemt søgning",
"filterInformation": "Filter information",
"filterActive": "Filtre aktiv",
"clear": "Ryd søgning"
"filterActive": "Filtre aktiv"
}
}

View File

@ -3,12 +3,6 @@
"default": "Indstillinger - Frigate",
"authentication": "Bruger Indstillinger - Frigate",
"camera": "Kamera indstillinger - Frigate",
"object": "Debug - Frigate",
"cameraManagement": "Administrér kameraer - Frigate",
"cameraReview": "Indstillinger for kameragennemgang - Frigate",
"enrichments": "Indstillinger for berigelser - Frigate",
"masksAndZones": "Maske- og zoneeditor - Frigate",
"motionTuner": "Bevægelsesjustering - Frigate",
"general": "Brugergrænsefladeindstillinger - Frigate"
"object": "Debug - Frigate"
}
}

View File

@ -6,98 +6,7 @@
"frigate": "Frigate Logs - Frigate",
"go2rtc": "Go2RTC Logs - Frigate",
"nginx": "Nginx Logs - Frigate"
},
"general": "Generelle statistikker - Frigate",
"enrichments": "Beredningsstatistikker - Frigate"
},
"title": "System",
"logs": {
"copy": {
"label": "Kopier til udklipsholder",
"success": "Logs er kopieret til udklipsholder",
"error": "Kunne ikke kopiere logs til udklipsholder"
},
"type": {
"label": "Type",
"timestamp": "Tidsstempel",
"message": "Besked",
"tag": "Tag"
},
"tips": "Logs bliver streamet fra serveren",
"toast": {
"error": {
"fetchingLogsFailed": "Fejl ved indhentning af logs: {{errorMessage}}",
"whileStreamingLogs": "Fejl ved streaming af logs: {{errorMessage}}"
}
},
"download": {
"label": "Download logs"
}
},
"general": {
"title": "Generelt",
"hardwareInfo": {
"gpuUsage": "GPU forbrug",
"gpuMemory": "GPU hukommelse",
"gpuEncoder": "GPU indkoder",
"gpuDecoder": "GPU afkoder",
"title": "Hardware information",
"gpuInfo": {
"closeInfo": {
"label": "Luk GPU information"
},
"copyInfo": {
"label": "Kopier GPU information"
},
"toast": {
"success": "Kopierede GPU information til udklipsholder"
}
},
"npuUsage": "NPU forbrug",
"npuMemory": "NPU hukommelse"
},
"detector": {
"title": "Detektorer",
"inferenceSpeed": "Detektorinferenshastighed",
"temperature": "Detektor temperatur",
"cpuUsage": "Detektor CPU forbrug",
"cpuUsageInformation": "CPU brugt til at forberede input- og outputdata til/fra detektionsmodeller. Denne værdi måler ikke inferensforbrug, selvom der bruges en GPU eller accelerator.",
"memoryUsage": "Detektorhummelsesforbrug"
},
"otherProcesses": {
"title": "Andre processer",
"processCpuUsage": "Proces CPU forbrug",
"processMemoryUsage": "Proceshukommelsesforbrug"
}
},
"metrics": "System metrikker",
"storage": {
"title": "Lagring",
"overview": "Overblik",
"recordings": {
"title": "Optagelser",
"tips": "Denne værdi repræsenterer den samlede lagerplads, der bruges af optagelserne i Frigates database. Frigate sporer ikke lagerpladsforbruget for alle filer på din disk.",
"earliestRecording": "Tidligste optagelse til rådighed:"
},
"shm": {
"title": "SHM (delt hukommelse) tildeling",
"warning": "Den nuværende SHM størrelse af {{total}}MB er for lille. Øg den til minimum {{min_shm}}MB."
},
"cameraStorage": {
"title": "Kamera lagring",
"camera": "Kamera",
"unusedStorageInformation": "Ubrugt lagringsinformation",
"storageUsed": "Lagring",
"percentageOfTotalUsed": "Procentandel af total",
"bandwidth": "Båndbredde",
"unused": {
"title": "Ubrugt",
"tips": "Denne værdi repræsenterer muligvis ikke nøjagtigt den ledige plads, der er tilgængelig for Frigate, hvis du har andre filer gemt på dit drev ud over Frigates optagelser. Frigate sporer ikke lagerforbrug ud over sine optagelser."
}
}
},
"cameras": {
"title": "Kameraer",
"overview": "Overblik"
}
"title": "System"
}

View File

@ -425,21 +425,5 @@
"sanding": "Schleifen",
"machine_gun": "Maschinengewehr",
"boom": "Dröhnen",
"field_recording": "Außenaufnahme",
"liquid": "Flüssigkeit",
"splash": "Spritzer",
"slosh": "Schwenken",
"squish": "Quetschen",
"drip": "Tropfen",
"pour": "Gießen",
"trickle": "Tröpfeln",
"fill": "Füllen",
"spray": "Sprühen",
"pump": "Pumpen",
"stir": "Umrühren",
"boiling": "Köchelnd",
"arrow": "Pfeil",
"electronic_tuner": "Elektronischer Tuner",
"effects_unit": "Effekteinheit",
"chorus_effect": "Chorus-Effekt"
"field_recording": "Außenaufnahme"
}

View File

@ -121,12 +121,7 @@
"unsuspended": "fortsetzen"
},
"label": {
"back": "Zurück",
"hide": "Verstecke {{item}}",
"show": "Zeige {{item}}",
"ID": "ID",
"none": "Nichts",
"all": "Alle"
"back": "Zurück"
},
"menu": {
"configurationEditor": "Konfigurationseditor",
@ -289,14 +284,5 @@
},
"information": {
"pixels": "{{area}}px"
},
"field": {
"optional": "Optional",
"internalID": "Die interne ID, die Frigate in der Konfiguration und Datenbank verwendet"
},
"list": {
"two": "{{0}} und {{1}}",
"many": "{{items}}, und {{last}}",
"separatorWithSpace": ", "
}
}

View File

@ -10,7 +10,6 @@
"unknownError": "Unbekannter Fehler. Prüfe Logs."
},
"user": "Benutzername",
"password": "Kennwort",
"firstTimeLogin": "Versuchen Sie sich zum ersten Mal anzumelden? Die Anmeldedaten sind in den Frigate-Logs aufgeführt."
"password": "Kennwort"
}
}

View File

@ -66,7 +66,7 @@
"failed": "Fehler beim Starten des Exports: {{error}}",
"noVaildTimeSelected": "Kein gültiger Zeitraum ausgewählt"
},
"success": "Export erfolgreich gestartet. Die Datei befindet sich auf der Exportseite."
"success": "Export erfolgreich gestartet. Die Datei befindet sich im Ordner /exports."
},
"fromTimeline": {
"saveExport": "Export speichern",
@ -126,7 +126,6 @@
"search": {
"placeholder": "Nach Label oder Unterlabel suchen..."
},
"noImages": "Kein Vorschaubild für diese Kamera gefunden",
"unknownLabel": "Gespeichertes Triggerbild"
"noImages": "Kein Vorschaubild für diese Kamera gefunden"
}
}

View File

@ -1,40 +1 @@
{
"documentTitle": "Klassifizierungsmodelle",
"details": {
"scoreInfo": "Die Punktzahl gibt die durchschnittliche Klassifizierungssicherheit aller Erkennungen dieses Objekts wieder."
},
"button": {
"deleteClassificationAttempts": "Lösche Klassifizierungs-Bilder",
"renameCategory": "Klasse umbenennen",
"deleteCategory": "Klasse löschen",
"deleteImages": "Bilder löschen",
"trainModel": "Modell trainieren",
"addClassification": "Klassifizierung hinzufügen",
"deleteModels": "Modell löschen",
"editModel": "Modell bearbeiten"
},
"tooltip": {
"trainingInProgress": "Modell wird gerade trainiert",
"noNewImages": "Keine weiteren Bilder zum trainieren. Bitte klassifiziere weitere Bilder im Datensatz.",
"noChanges": "Keine Veränderungen des Datensatzes seit dem letzten Training.",
"modelNotReady": "Modell ist nicht bereit trainiert zu werden."
},
"toast": {
"success": {
"deletedCategory": "Klasse gelöscht",
"deletedImage": "Gelöschte Bilder",
"deletedModel_one": "{{count}} Model erfolgreich gelöscht",
"deletedModel_other": "{{count}} Modelle erfolgreich gelöscht",
"categorizedImage": "Bild erfolgreich klassifiziert",
"trainedModel": "Modell erfolgreich trainiert.",
"trainingModel": "Modelltraining erfolgreich gestartet.",
"updatedModel": "Modellkonfiguration erfolgreich aktualisiert",
"renamedCategory": "Klasse erfolgreich in {{name}} umbenannt"
},
"error": {
"deleteImageFailed": "Löschen fehlgeschlagen: {{errorMessage}}",
"deleteCategoryFailed": "Klasse konnte nicht gelöscht werden: {{errorMessage}}",
"deleteModelFailed": "Model konnte nicht gelöscht werden: {{errorMessage}}"
}
}
}
{}

View File

@ -36,22 +36,5 @@
"selected_other": "{{count}} ausgewählt",
"detected": "erkannt",
"suspiciousActivity": "Verdächtige Aktivität",
"threateningActivity": "Bedrohliche Aktivität",
"zoomIn": "Hereinzoomen",
"zoomOut": "Herauszoomen",
"detail": {
"label": "Detail",
"aria": "Detailansicht umschalten",
"trackedObject_one": "{{count}} Objekt",
"trackedObject_other": "{{count}} Objekte",
"noObjectDetailData": "Keine detaillierten Daten des Objekt verfügbar.",
"noDataFound": "Keine Detaildaten zur Überprüfung",
"settings": "Detailansicht Einstellungen",
"alwaysExpandActive": {
"desc": "Immer die Objektdetails des aktiven Überprüfungselements erweitern, sofern verfügbar."
}
},
"objectTrack": {
"trackedPoint": "Verfolgter Punkt"
}
"threateningActivity": "Bedrohliche Aktivität"
}

View File

@ -158,8 +158,7 @@
"details": "Details",
"video": "Video",
"object_lifecycle": "Objekt-Lebenszyklus",
"snapshot": "Snapshot",
"thumbnail": "Vorschaubild"
"snapshot": "Snapshot"
},
"itemMenu": {
"downloadSnapshot": {
@ -196,22 +195,12 @@
"addTrigger": {
"aria": "Einen Trigger für dieses verfolgte Objekt hinzufügen",
"label": "Trigger hinzufügen"
},
"viewTrackingDetails": {
"label": "Details zum Verfolgen anzeigen",
"aria": "Details zum Verfolgen anzeigen"
},
"showObjectDetails": {
"label": "Objektpfad anzeigen"
},
"hideObjectDetails": {
"label": "Objektpfad verbergen"
}
},
"dialog": {
"confirmDelete": {
"title": "Löschen bestätigen",
"desc": "Beim Löschen dieses verfolgten Objekts werden der Schnappschuss, alle gespeicherten Einbettungen und alle zugehörigen Verfolgungsdetails entfernt. Aufgezeichnetes Filmmaterial dieses verfolgten Objekts in der Verlaufsansicht wird <em>NICHT</em> gelöscht. <br /><br />Sind Sie sicher, dass Sie fortfahren möchten?"
"desc": "Beim Löschen dieses verfolgten Objekts werden der Schnappschuss, alle gespeicherten Einbettungen und alle zugehörigen Objektlebenszykluseinträge entfernt. Aufgezeichnetes Filmmaterial dieses verfolgten Objekts in der Verlaufsansicht wird <em>NICHT</em> gelöscht. <br /><br />Sind Sie sicher, dass Sie fortfahren möchten?"
}
},
"searchResult": {
@ -221,9 +210,7 @@
"error": "Das verfolgte Objekt konnte nicht gelöscht werden: {{errorMessage}}"
}
},
"tooltip": "Entspricht {{type}} bei {{confidence}}%",
"previousTrackedObject": "Vorheriges verfolgtes Objekt",
"nextTrackedObject": "Nächstes verfolgtes Objekt"
"tooltip": "Entspricht {{type}} bei {{confidence}}%"
},
"noTrackedObjects": "Keine verfolgten Objekte gefunden",
"fetchingTrackedObjectsFailed": "Fehler beim Abrufen von verfolgten Objekten: {{errorMessage}}",
@ -235,49 +222,5 @@
},
"concerns": {
"label": "Bedenken"
},
"trackingDetails": {
"noImageFound": "Kein Bild mit diesem Zeitstempel gefunden.",
"createObjectMask": "Objekt-Maske erstellen",
"scrollViewTips": "Klicke, um die relevanten Momente aus dem Lebenszyklus dieses Objektes zu sehen.",
"lifecycleItemDesc": {
"visible": "{{label}} erkannt",
"entered_zone": "{{label}} betrat {{zones}}",
"active": "{{label}} wurde aktiv",
"stationary": "{{label}} wurde stationär",
"attribute": {
"faceOrLicense_plate": "{{attribute}} erkannt für {{label}}",
"other": "{{label}} erkannt als {{attribute}}"
},
"gone": "{{label}} verließ",
"heard": "{{label}} wurde gehört",
"external": "{{label}} erkannt",
"header": {
"zones": "Zonen",
"ratio": "Verhältnis",
"area": "Bereich"
}
},
"annotationSettings": {
"title": "Anmerkungseinstellungen",
"showAllZones": {
"title": "Zeige alle Zonen",
"desc": "Immer Zonen auf Rahmen anzeigen, in die Objekte eingetreten sind."
},
"offset": {
"label": "Anmerkungen Versatz",
"desc": "Diese Daten stammen aus dem Erkennungsfeed der Kamera, werden jedoch über Bilder aus dem Aufzeichnungsfeed gelegt. Es ist unwahrscheinlich, dass beide Streams perfekt synchron sind. Daher stimmen der Begrenzungsrahmen und das Filmmaterial nicht vollständig überein. Mit dieser Einstellung lassen sich die Anmerkungen zeitlich nach vorne oder hinten verschieben, um sie besser an das aufgezeichnete Filmmaterial anzupassen.",
"millisecondsToOffset": "Millisekunden, um Erkennungs-Anmerkungen zu verschieben. <em>Standard: 0</em>",
"tips": "Verringere den Wert, wenn die Videowiedergabe den Boxen und Wegpunkten voraus ist, und erhöhe den Wert, wenn die Videowiedergabe hinter ihnen zurückbleibt. Dieser Wert kann negativ sein.",
"toast": {
"success": "Der Anmerkungs-Offset für {{camera}} wurde in der Konfigurationsdatei gespeichert. Starte Frigate neu, um Ihre Änderungen zu übernehmen."
}
}
},
"carousel": {
"previous": "Vorherige Anzeige",
"next": "Nächste Anzeige"
},
"title": "Verfolgungsdetails"
}
}

View File

@ -13,11 +13,5 @@
"error": {
"renameExportFailed": "Umbenennen des Exports fehlgeschlagen: {{errorMessage}}"
}
},
"tooltip": {
"shareExport": "Export teilen",
"downloadVideo": "Video herunterladen",
"editName": "Name ändern",
"deleteExport": "Export löschen"
}
}

View File

@ -1,7 +1,7 @@
{
"description": {
"placeholder": "Gib einen Name für diese Kollektion ein",
"addFace": "Füge der Gesichtsbibliothek eine neue Sammlung hinzu, indem ein erstes Bild hochgeladen wird.",
"addFace": "Anleitung für das Hinzufügen einer neuen Kollektion zur Gesichtsbibliothek.",
"invalidName": "Ungültiger Name. Namen dürfen nur Buchstaben, Zahlen, Leerzeichen, Apostrophe, Unterstriche und Bindestriche enthalten."
},
"details": {
@ -22,7 +22,7 @@
"title": "Kollektion erstellen",
"new": "Lege ein neues Gesicht an",
"desc": "Erstelle eine neue Kollektion",
"nextSteps": "Um eine solide Grundlage zu bilden: <li> Benutze den \"Aktuelle Erkennungen\" Tab, um Bilder für jede erkannte Person auszuwählen und zu trainieren.</li> <li>Konzentriere dich für gute Ergebnisse auf Frontalfotos; vermeide Bilder zu Trainingszwecken, bei denen Gesichter aus einem Winkel erfasst wurden.</li></ul>"
"nextSteps": "Um eine solide Grundlage zu bilden: <li> Benutze den Trainieren Tab, um Bilder für jede erkannte Person auszuwählen und zu trainieren.</li> <li>Konzentriere dich für gute Ergebnisse auf Frontalfotos; vermeide Bilder zu Trainingszwecken, bei denen Gesichter aus einem Winkel erfasst wurden.</li></ul>"
},
"documentTitle": "Gesichtsbibliothek - Frigate",
"selectItem": "Wähle {{item}}",
@ -44,8 +44,8 @@
"deleteFace": "Lösche Gesicht"
},
"train": {
"title": "Aktuelle Erkennungen",
"aria": "Wähle aktuelle Erkennungen",
"title": "Trainiere",
"aria": "Wähle Training",
"empty": "Es gibt keine aktuellen Versuche zur Gesichtserkennung"
},
"deleteFaceLibrary": {

View File

@ -5,7 +5,7 @@
"camera": "Kameraeinstellungen - Frigate",
"masksAndZones": "Masken- und Zonen-Editor Frigate",
"object": "Debug - Frigate",
"general": "UI Einstellungen Frigate",
"general": "Allgemeine Einstellungen Frigate",
"frigatePlus": "Frigate+ Einstellungen Frigate",
"classification": "Klassifizierungseinstellungen Frigate",
"motionTuner": "Bewegungserkennungs-Optimierer Frigate",

View File

@ -10,7 +10,6 @@
"loginFailed": "Αποτυχία σύνδεσης",
"unknownError": "Άγνωστο σφάλμα. Ελέγξτε το αρχείο καταγραφής.",
"webUnknownError": "Άγνωστο σφάλμα. Εξετάστε το αρχείο καταγραφής κονσόλας."
},
"firstTimeLogin": "Προσπαθείτε να συνδεθείτε για πρώτη φορά; Τα διαπιστευτήρια είναι τυπωμένα στα logs του Frigate."
}
}
}

View File

@ -53,7 +53,6 @@
"selectOrExport": "Select or Export",
"toast": {
"success": "Successfully started export. View the file in the exports page.",
"view": "View",
"error": {
"failed": "Failed to start export: {{error}}",
"endTimeMustAfterStartTime": "End time must be after start time",

View File

@ -166,7 +166,6 @@
"noImages": "No sample images generated",
"classifying": "Classifying & Training...",
"trainingStarted": "Training started successfully",
"modelCreated": "Model created successfully. Use the Recent Classifications view to add images for missing states, then train the model.",
"errors": {
"noCameras": "No cameras configured",
"noObjectLabel": "No object label selected",
@ -174,11 +173,7 @@
"generationFailed": "Generation failed. Please try again.",
"classifyFailed": "Failed to classify images: {{error}}"
},
"generateSuccess": "Successfully generated sample images",
"missingStatesWarning": {
"title": "Missing State Examples",
"description": "You haven't selected examples for all states. The model will not be trained until all states have images. After continuing, use the Recent Classifications view to classify images for the missing states, then train the model."
}
"generateSuccess": "Successfully generated sample images"
}
}
}

View File

@ -54,7 +54,6 @@
"selected_other": "{{count}} selected",
"camera": "Camera",
"detected": "detected",
"normalActivity": "Normal",
"needsReview": "Needs review",
"securityConcern": "Security concern"
"suspiciousActivity": "Suspicious Activity",
"threateningActivity": "Threatening Activity"
}

View File

@ -61,8 +61,7 @@
"header": {
"zones": "Zones",
"ratio": "Ratio",
"area": "Area",
"score": "Score"
"area": "Area"
}
},
"annotationSettings": {

View File

@ -18,19 +18,15 @@
"deletedModel_many": "Borrados con éxito {{count}} modelos",
"deletedModel_other": "Borrados con éxito {{count}} modelos",
"categorizedImage": "Imagen Clasificada Correctamente",
"trainedModel": "Modelo entrenado correctamente.",
"trainingModel": "Entrenamiento del modelo iniciado correctamente.",
"updatedModel": "Configuración del modelo actualizada correctamente",
"renamedCategory": "Clase renombrada correctamente a {{name}}"
"trainedModel": "Modelo entrenado correctamente."
},
"error": {
"deleteImageFailed": "Fallo al borrar: {{errorMessage}}",
"deleteCategoryFailed": "Fallo al borrar clase: {{errorMessage}}",
"deleteModelFailed": "Fallo al borrar modelo: {{errorMessage}}",
"categorizeFailed": "Fallo al categorizar imagen: {{errorMessage}}",
"trainingFailed": "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}}"
"trainingFailed": "Fallo al iniciar el entrenamiento del modelo: {{errorMessage}}",
"updateModelFailed": "Fallo al actualizar modelo: {{errorMessage}}"
}
},
"deleteCategory": {
@ -39,21 +35,6 @@
},
"deleteModel": {
"title": "Borrar Modelo de Clasificación",
"single": "¿Está seguro de que quiere eliminar {{name}}? Esto borrar permanentemente todos los datos asociados incluidas las imágenes y los datos de entrenamiento. Esta acción no se puede deshacer.",
"desc_one": "¿Estas seguro de que quiere borrar {{count}} modelo? Esto borrara permanentemente todos los datos asociados, incluyendo imágenes y datos de entrenamiento. Esta acción no puede ser desehecha.",
"desc_many": "¿Estas seguro de que quiere borrar {{count}} modelos? Esto borrara permanentemente todos los datos asociados, incluyendo imágenes y datos de entrenamiento. Esta acción no puede ser desehecha.",
"desc_other": "¿Estas seguro de que quiere borrar {{count}} modelos? Esto borrara permanentemente todos los datos asociados, incluyendo imágenes y datos de entrenamiento. Esta acción no puede ser desehecha."
},
"edit": {
"title": "Editar modelo de clasificación"
},
"tooltip": {
"noChanges": "No se han realizado cambios en el conjunto de datos desde el último entrenamiento.",
"modelNotReady": "El modelo no está listo para el entrenamiento",
"trainingInProgress": "El modelo está entrenándose actualmente.",
"noNewImages": "No hay imágenes nuevas para entrenar. Clasifica antes más imágenes del conjunto de datos."
},
"details": {
"scoreInfo": "La puntuación representa la confianza media de clasificación en todas las detecciones de este objeto."
"single": "¿Está seguro de que quiere eliminar {{name}}? Esto borrar permanentemente todos los datos asociados incluidas las imágenes y los datos de entrenamiento. Esta acción no se puede deshacer."
}
}

View File

@ -42,19 +42,12 @@
"zoomOut": "Alejar",
"detail": {
"label": "Detalle",
"trackedObject_one": "{{count}} objeto",
"trackedObject_other": "{{count}} objetos",
"trackedObject_one": "objeto",
"trackedObject_other": "objetos",
"noObjectDetailData": "No hay datos detallados del objeto.",
"settings": "Configuración de la Vista Detalle",
"noDataFound": "No hay datos detallados para revisar",
"aria": "Alternar vista de detalles",
"alwaysExpandActive": {
"title": "Expandir siempre los activos",
"desc": "Expandir siempre los detalles del objeto activo cuando esten disponibles."
}
"settings": "Configuración de la Vista Detalle"
},
"objectTrack": {
"clickToSeek": "Clic para ir a este momento",
"trackedPoint": "Puntro trazado"
"clickToSeek": "Clic para ir a este momento"
}
}

View File

@ -111,8 +111,7 @@
"video": "vídeo",
"object_lifecycle": "ciclo de vida del objeto",
"details": "detalles",
"thumbnail": "miniatura",
"tracking_details": "detalles de seguimiento"
"thumbnail": "miniatura"
},
"objectLifecycle": {
"title": "Ciclo de vida del objeto",
@ -228,14 +227,9 @@
"label": "Preocupaciones"
},
"trackingDetails": {
"title": "Detalles del seguimiento",
"title": "Detalles del Seguimiento",
"noImageFound": "No se ha encontrado imagen en este momento.",
"createObjectMask": "Crear máscara de objeto",
"adjustAnnotationSettings": "Ajustar configuración de anotaciones",
"scrollViewTips": "Haz clic para ver los momentos relevantes del ciclo de vida de este objeto.",
"count": "{{first}} de {{second}}",
"lifecycleItemDesc": {
"visible": "{{label}} detectado"
}
"createObjectMask": "Crear Máscara de Objeto",
"adjustAnnotationSettings": "Ajustar configuración de anotaciones"
}
}

View File

@ -85,7 +85,7 @@
"disable": "Ocultar estadísticas de transmisión"
},
"manualRecording": {
"title": "Bajo demanda",
"title": "Grabación bajo demanda",
"tips": "Iniciar un evento manual basado en la configuración de retención de grabaciones de esta cámara.",
"playInBackground": {
"label": "Reproducir en segundo plano",

View File

@ -7,7 +7,7 @@
"camera": "Configuración de cámara - Frigate",
"motionTuner": "Ajuste de movimiento - Frigate",
"classification": "Configuración de clasificación - Frigate",
"general": "Configuración de Interfaz de Usuario - Frigate",
"general": "Configuración General - Frigate",
"frigatePlus": "Configuración de Frigate+ - Frigate",
"notifications": "Configuración de Notificaciones - Frigate",
"enrichments": "Configuración de Análisis Avanzado - Frigate",
@ -78,7 +78,7 @@
"title": "Diseños guardados",
"clearAll": "Borrar todos los diseños"
},
"title": "Ajustes de Interfaz de Usuario",
"title": "Configuración general",
"toast": {
"success": {
"clearStoredLayout": "Diseño almacenado eliminado para {{cameraName}}",

View File

@ -87,10 +87,7 @@
"formattedTimestampMonthDayYear": {
"12hour": "d MMM, yyyy",
"24hour": "d MMM,yyyy"
},
"inProgress": "En cours",
"invalidStartTime": "Heure de début invalide",
"invalidEndTime": "Heure de fin invalide"
}
},
"button": {
"apply": "Appliquer",
@ -127,8 +124,7 @@
"export": "Exporter",
"deleteNow": "Supprimer maintenant",
"download": "Télécharger",
"done": "Terminé",
"continue": "Continuer"
"done": "Terminé"
},
"menu": {
"configuration": "Configuration",
@ -230,8 +226,7 @@
},
"uiPlayground": "Bac à sable de l'interface",
"faceLibrary": "Bibliothèque de visages",
"languages": "Langues",
"classification": "Classification"
"languages": "Langues"
},
"toast": {
"save": {
@ -277,9 +272,7 @@
"back": "Retour",
"hide": "Masquer {{item}}",
"show": "Afficher {{item}}",
"ID": "ID",
"none": "Aucun",
"all": "Tous"
"ID": "ID"
},
"unit": {
"speed": {

View File

@ -20,37 +20,28 @@
"deletedModel_one": "{{count}} modèle supprimé avec succès",
"deletedModel_many": "{{count}} modèles supprimés avec succès",
"deletedModel_other": "{{count}} modèles supprimés avec succès",
"updatedModel": "Configuration du modèle mise à jour avec succès",
"renamedCategory": "Classe renommée en {{name}} avec succès"
"updatedModel": "Configuration du modèle mise à jour avec succès"
},
"error": {
"deleteImageFailed": "Échec de la suppression : {{errorMessage}}",
"deleteCategoryFailed": "Échec de la suppression de la classe : {{errorMessage}}",
"categorizeFailed": "Échec de la catégorisation de l'image : {{errorMessage}}",
"trainingFailed": "L'entraînement du modèle a échoué. Consultez les journaux de Frigate pour plus de détails.",
"trainingFailed": "Échec du démarrage de l'entraînement du modèle : {{errorMessage}}",
"deleteModelFailed": "Impossible de supprimer le modèle : {{errorMessage}}",
"updateModelFailed": "Impossible de mettre à jour le modèle : {{errorMessage}}",
"renameCategoryFailed": "Impossible de renommer la classe : {{errorMessage}}",
"trainingFailedToStart": "Impossible de démarrer l'entraînement du modèle : {{errorMessage}}"
"updateModelFailed": "Impossible de mettre à jour le modèle : {{errorMessage}}"
}
},
"deleteCategory": {
"title": "Supprimer la classe",
"desc": "Êtes-vous sûr de vouloir supprimer la classe {{name}}? Cette action supprimera définitivement toutes les images associées et nécessitera un réentraînement du modèle.",
"minClassesTitle": "Impossible de supprimer la classe",
"minClassesDesc": "Un modèle de classification doit avoir au moins 2 classes. Ajoutez une autre classe avant de supprimer celle-ci."
"desc": "Êtes-vous sûr de vouloir supprimer la classe {{name}}? Cette action supprimera définitivement toutes les images associées et nécessitera un réentraînement du modèle."
},
"deleteDatasetImages": {
"title": "Supprimer les images du jeu de données",
"desc_one": "Êtes-vous sûr de vouloir supprimer {{count}} image du jeu de données {{dataset}}? Cette action est irréversible et nécessitera un réentraînement du modèle.",
"desc_many": "Êtes-vous sûr de vouloir supprimer {{count}} images du jeu de données {{dataset}}? Cette action est irréversible et nécessitera un réentraînement du modèle.",
"desc_other": "Êtes-vous sûr de vouloir supprimer {{count}} images du jeu de données {{dataset}}? Cette action est irréversible et nécessitera un réentraînement du modèle."
"desc": "Êtes-vous sûr de vouloir supprimer {{count}} images du jeu de données {{dataset}}? Cette action est irréversible et nécessitera un réentraînement du modèle."
},
"deleteTrainImages": {
"title": "Supprimer les images d'entraînement",
"desc_one": "Êtes-vous sûr de vouloir supprimer {{count}} image? Cette action est irréversible.",
"desc_many": "Êtes-vous sûr de vouloir supprimer {{count}} images? Cette action est irréversible.",
"desc_other": "Êtes-vous sûr de vouloir supprimer {{count}} images? Cette action est irréversible."
"desc": "Êtes-vous sûr de vouloir supprimer {{count}} images? Cette action est irréversible."
},
"renameCategory": {
"title": "Renommer la classe",
@ -149,18 +140,13 @@
"generationFailed": "Échec de la génération. Veuillez réessayer.",
"classifyFailed": "Échec de la classification des images : {{error}}"
},
"generateSuccess": "Génération des images d'exemple réussie",
"allImagesRequired_one": "Veuillez classifier toutes les images. {{count}} image restante.",
"allImagesRequired_many": "Veuillez classifier toutes les images. {{count}} images restantes.",
"allImagesRequired_other": "Veuillez classifier toutes les images. {{count}} images restantes."
"generateSuccess": "Génération des images d'exemple réussie"
}
},
"deleteModel": {
"title": "Supprimer le modèle de classification",
"single": "Voulez-vous vraiment supprimer {{name}}? Cela supprimera définitivement toutes les données associées, y compris les images et les données d'entraînement. Cette action est irréversible.",
"desc_one": "Voulez-vous vraiment supprimer {{count}} modèle? Cela supprimera définitivement toutes les données associées, y compris les images et les données d'entraînement. Cette action est irréversible.",
"desc_many": "Voulez-vous vraiment supprimer {{count}} modèles? Cela supprimera définitivement toutes les données associées, y compris les images et les données d'entraînement. Cette action est irréversible.",
"desc_other": "Voulez-vous vraiment supprimer {{count}} modèles? Cela supprimera définitivement toutes les données associées, y compris les images et les données d'entraînement. Cette action est irréversible."
"desc": "Voulez-vous vraiment supprimer {{count}} modèle(s)? Cela supprimera définitivement toutes les données associées, y compris les images et les données d'entraînement. Cette action est irréversible."
},
"menu": {
"objects": "Objets",
@ -174,11 +160,5 @@
"descriptionState": "Modifier les classes pour ce modèle de classification d'état. Les modifications nécessiteront un réentraînement du modèle.",
"descriptionObject": "Modifier le type d'objet et le type de classification pour ce modèle de classification d'objet",
"stateClassesInfo": "Note : La modification des classes d'état nécessite un réentraînement du modèle avec les classes mises à jour."
},
"tooltip": {
"trainingInProgress": "Modèle en cours d'entraînement",
"noNewImages": "Aucune nouvelle image pour l'entraînement. Veuillez d'abord classifier plus d'images dans le jeu de données.",
"modelNotReady": "Le modèle n'est pas prêt pour l'entraînement.",
"noChanges": "Aucune modification du jeu de données depuis le dernier entraînement"
}
}

View File

@ -41,8 +41,8 @@
"detail": {
"noDataFound": "Aucun détail à examiner",
"aria": "Activer/désactiver la vue détaillée",
"trackedObject_one": "{{count}} objet",
"trackedObject_other": "{{count}} objets",
"trackedObject_one": "objet",
"trackedObject_other": "objets",
"noObjectDetailData": "Aucun détail d'objet disponible",
"label": "Détail",
"settings": "Paramètres de la vue Détail",

View File

@ -42,7 +42,7 @@
"regenerate": "Une nouvelle description a été demandée à {{provider}}. Selon la vitesse de votre fournisseur, la régénération de la nouvelle description peut prendre un certain temps.",
"updatedSublabel": "Sous-étiquette mise à jour avec succès",
"updatedLPR": "Plaque d'immatriculation mise à jour avec succès",
"audioTranscription": "Transcription audio demandée avec succès. Selon la vitesse de votre serveur Frigate, la transcription peut prendre un certain temps."
"audioTranscription": "Transcription audio demandée avec succès"
},
"error": {
"regenerate": "Échec de l'appel de {{provider}} pour une nouvelle description : {{errorMessage}}",
@ -110,8 +110,7 @@
"video": "vidéo",
"object_lifecycle": "cycle de vie de l'objet",
"snapshot": "instantané",
"thumbnail": "Miniature",
"tracking_details": "Détails du suivi"
"thumbnail": "Miniature"
},
"objectLifecycle": {
"title": "Cycle de vie de l'objet",
@ -228,9 +227,7 @@
"error": "Échec de la suppression de l'objet suivi : {{errorMessage}}"
}
},
"tooltip": "Correspondance : {{type}} à {{confidence}}%",
"previousTrackedObject": "Objet suivi précédent",
"nextTrackedObject": "Objet suivi suivant"
"tooltip": "Correspondance : {{type}} à {{confidence}}%"
},
"exploreMore": "Explorer plus d'objets {{label}}",
"aiAnalysis": {
@ -270,7 +267,7 @@
"offset": {
"desc": "Ces données proviennent du flux de détection de votre caméra, mais elles sont superposées aux images du flux d'enregistrement. Il est peu probable que les deux flux soient parfaitement synchronisés. Par conséquent, le cadre de délimitation et la vidéo ne s'aligneront pas parfaitement. Vous pouvez utiliser ce paramètre pour décaler les annotations vers l'avant ou vers l'arrière dans le temps afin de mieux les aligner avec la vidéo enregistrée.",
"millisecondsToOffset": "Millisecondes de décalage pour les annotations de détection. <em>Par défaut : 0</em>",
"tips": "Diminuez la valeur si la lecture vidéo est en avance sur les cadres de détection et les points de tracé, et augmentez-la si la lecture vidéo est en retard sur ceux-ci. Cette valeur peut être négative.",
"tips": "ASTUCE : Imaginez une séquence d'événement avec une personne marchant de gauche à droite. Si le cadre de détection sur la chronologie de l'événement est constamment à gauche de la personne, la valeur doit être diminuée. De même, si une personne marche de gauche à droite et que le cadre de détection est constamment devant la personne, la valeur doit être augmentée.",
"toast": {
"success": "Le décalage des annotations pour {{camera}} a été sauvegardé dans le fichier de configuration. Redémarrez Frigate pour appliquer vos modifications."
},

View File

@ -60,7 +60,7 @@
"deletedName_other": "{{count}} visages ont été supprimés avec succès.",
"uploadedImage": "Image téléversée avec succès",
"addFaceLibrary": "{{name}} a été ajouté avec succès à la bibliothèque de visages!",
"updatedFaceScore": "Score du visage ({{score}}) de {{name}} mis à jour avec succès",
"updatedFaceScore": "Score du visage mis à jour avec succès",
"deletedFace_one": "{{count}} visage supprimé avec succès",
"deletedFace_many": "{{count}} visages supprimés avec succès",
"deletedFace_other": "{{count}} visages supprimés avec succès",

View File

@ -174,11 +174,7 @@
"noCameras": {
"title": "Aucune caméra n'est configurée",
"description": "Pour commencer, connectez une caméra à Frigate.",
"buttonText": "Ajouter une caméra",
"restricted": {
"title": "Aucune caméra disponible",
"description": "Vous n'avez pas la permission de visionner les caméras de ce groupe."
}
"buttonText": "Ajouter une caméra"
},
"snapshot": {
"takeSnapshot": "Télécharger un instantané",

View File

@ -5,7 +5,7 @@
"camera": "Paramètres des caméras - Frigate",
"classification": "Paramètres de classification - Frigate",
"motionTuner": "Réglage de la détection de mouvement - Frigate",
"general": "Paramètres de l'interface utilisateur - Frigate",
"general": "Paramètres généraux - Frigate",
"masksAndZones": "Éditeur de masques et de zones - Frigate",
"object": "Débogage - Frigate",
"frigatePlus": "Paramètres Frigate+ - Frigate",
@ -41,7 +41,7 @@
"noCamera": "Aucune caméra"
},
"general": {
"title": "Paramètres de l'interface utilisateur",
"title": "Paramètres généraux",
"liveDashboard": {
"title": "Tableau de bord en direct",
"automaticLiveView": {
@ -55,10 +55,6 @@
"displayCameraNames": {
"label": "Toujours afficher les noms des caméras",
"desc": "Toujours afficher les noms des caméras dans une puce sur le tableau de bord de la vue en direct multi-caméras"
},
"liveFallbackTimeout": {
"label": "Délai d'attente avant repli (Lecteur en direct)",
"desc": "Lorsque le flux en direct haute qualité d'une caméra est indisponible, le lecteur bascule en mode faible bande passante après ce nombre de secondes. Par défaut : 3."
}
},
"storedLayouts": {
@ -897,9 +893,9 @@
"createRole": "Rôle {{role}} créé avec succès",
"updateCameras": "Caméras mis à jour pour le rôle {{role}}",
"deleteRole": "Rôle {{role}} supprimé avec succès",
"userRolesUpdated_one": "{{count}} utilisateur affecté à ce rôle a été mis à jour avec des droits \"Observateur\", et a accès à toutes les caméras.",
"userRolesUpdated_many": "{{count}} utilisateurs affectés à ce rôle ont été mis à jour avec des droits \"Observateur\", et ont accès à toutes les caméras.",
"userRolesUpdated_other": "{{count}} utilisateurs affectés à ce rôle ont été mis à jour avec des droits \"Observateur\", et ont accès à toutes les caméras."
"userRolesUpdated_one": "{{count}} utilisateurs affectés à ce rôle ont été mis à jour avec des droits \"Observateur\", et ont accès à toutes les caméras.",
"userRolesUpdated_many": "",
"userRolesUpdated_other": ""
},
"error": {
"createRoleFailed": "Échec dans la création du rôle : {{errorMessage}}",
@ -946,8 +942,7 @@
"steps": {
"nameAndConnection": "Nom et connexion",
"streamConfiguration": "Configuration du flux",
"validationAndTesting": "Validation et tests",
"probeOrSnapshot": "Sondage ou Instantané"
"validationAndTesting": "Validation et tests"
},
"save": {
"success": "Nouvelle caméra {{cameraName}} enregistrée avec succès",
@ -964,7 +959,7 @@
"testFailed": "Échec du test de flux : {{error}}"
},
"step1": {
"description": "Saisissez les détails de votre caméra et choisissez d'interroger la caméra ou de sélectionner manuellement la marque.",
"description": "Saisissez les détails de votre caméra et testez la connexion.",
"cameraName": "Nom de la caméra",
"cameraNamePlaceholder": "par ex., porte_entree ou apercu_cour_arriere",
"host": "Hôte / Adresse IP",
@ -1004,19 +999,10 @@
"testing": {
"probingMetadata": "Vérification des métadonnées de la caméra en cours...",
"fetchingSnapshot": "Récupération de l'instantané de la caméra en cours..."
},
"connectionSettings": "Paramètres de connexion",
"detectionMethod": "Méthode de détection du flux",
"onvifPort": "Port ONVIF",
"probeMode": "Interroger la caméra",
"manualMode": "Sélection manuelle",
"detectionMethodDescription": "Interrogez la caméra avec ONVIF (si pris en charge) pour trouver les URL de flux de la caméra, ou sélectionnez manuellement la marque de la caméra pour utiliser des URL prédéfinies. Pour saisir une URL RTSP personnalisée, choisissez la méthode manuelle et sélectionnez \"Autre\".",
"onvifPortDescription": "Pour les caméras prenant en charge ONVIF, il s'agit généralement de 80 ou 8080.",
"useDigestAuth": "Utiliser l'authentification Digest",
"useDigestAuthDescription": "Utilisez l'authentification Digest HTTP pour ONVIF. Certaines caméras peuvent nécessiter un nom d'utilisateur/mot de passe ONVIF dédié au lieu de l'utilisateur administrateur standard."
}
},
"step2": {
"description": "Interrogez la caméra pour les flux disponibles ou configurez des paramètres manuels en fonction de la méthode de détection sélectionnée.",
"description": "Définissez les rôles du flux et ajoutez des flux supplémentaires pour votre caméra.",
"streamsTitle": "Flux de caméra",
"addStream": "Ajouter un flux",
"addAnotherStream": "Ajouter un autre flux",
@ -1035,8 +1021,8 @@
"detect": "Détection d'objets"
},
"testStream": "Tester la connexion",
"testSuccess": "Test de connexion réussi!",
"testFailed": "Échec du test de connexion. Veuillez vérifier votre saisie et réessayer.",
"testSuccess": "Test du flux réussi!",
"testFailed": "Échec du test du flux",
"testFailedTitle": "Échec du test",
"connected": "Connecté",
"notConnected": "Non connecté",
@ -1052,42 +1038,10 @@
"featuresPopover": {
"title": "Fonctionnalités du flux",
"description": "Utilisez la rediffusion du flux go2rtc pour réduire le nombre de connexions à votre caméra."
},
"streamDetails": "Détails du flux",
"probing": "Interrogation de la caméra en cours...",
"retry": "Réessayer",
"testing": {
"probingMetadata": "Interrogation des métadonnées de la caméra en cours...",
"fetchingSnapshot": "Récupération de l'instantané de la caméra en cours..."
},
"probeFailed": "Impossible d'interroger la caméra : {{error}}",
"probingDevice": "Interrogation de l'appareil en cours...",
"probeSuccessful": "Interrogation réussie",
"probeError": "Erreur d'interrogation",
"probeNoSuccess": "Échec de l'interrogation",
"deviceInfo": "Informations sur l'appareil",
"manufacturer": "Fabricant",
"model": "Modèle",
"firmware": "Micrologiciel",
"profiles": "Profils",
"ptzSupport": "Prise en charge PTZ",
"autotrackingSupport": "Prise en charge du suivi automatique",
"presets": "Préréglages",
"rtspCandidates": "Candidats RTSP",
"rtspCandidatesDescription": "Les URL RTSP suivantes ont été trouvées lors de l'interrogation de la caméra. Testez la connexion pour afficher les métadonnées du flux.",
"noRtspCandidates": "Aucune URL RTSP n'a été trouvée sur la caméra. Vos identifiants sont peut-être incorrects, ou la caméra ne prend peut-être pas en charge ONVIF ou la méthode utilisée pour récupérer les URL RTSP. Revenez en arrière et saisissez l'URL RTSP manuellement.",
"candidateStreamTitle": "Candidat {{number}}",
"useCandidate": "Utiliser",
"uriCopy": "Copier",
"uriCopied": "URI copiée dans le presse-papiers",
"testConnection": "Tester la connexion",
"toggleUriView": "Cliquer pour basculer l'affichage de l'URI complet",
"errors": {
"hostRequired": "L'hôte/adresse IP est requis."
}
},
"step3": {
"description": "Configurez les rôles des flux et ajoutez des flux supplémentaires pour votre caméra.",
"description": "Validation et analyse finales avant l'enregistrement de votre nouvelle caméra. Connectez chaque flux avant d'enregistrer.",
"validationTitle": "Validation du flux",
"connectAllStreams": "Connecter tous les flux",
"reconnectionSuccess": "Reconnexion réussie.",
@ -1128,91 +1082,7 @@
},
"valid": "Valide",
"ffmpegModule": "Utiliser le mode de compatibilité du flux",
"ffmpegModuleDescription": "Si le flux ne se charge pas après plusieurs tentatives, essayez d'activer cette option. Lorsqu'elle est activée, Frigate utilisera le module ffmpeg avec go2rtc. Cela peut offrir une meilleure compatibilité avec certains flux de caméra.",
"streamsTitle": "Flux de la caméra",
"addStream": "Ajouter un flux",
"addAnotherStream": "Ajouter un autre flux",
"streamUrl": "URL du flux",
"streamUrlPlaceholder": "rtsp://nomdutilisateur:motdepasse@hote:port/chemin",
"selectStream": "Sélectionner un flux",
"searchCandidates": "Rechercher des candidats",
"noStreamFound": "Aucun flux trouvé",
"url": "URL",
"resolution": "Résolution",
"selectResolution": "Sélectionner la résolution",
"quality": "Qualité",
"selectQuality": "Sélectionner la qualité",
"roleLabels": {
"detect": "Détection d'objet",
"record": "Enregistrement",
"audio": "Audio"
},
"testStream": "Tester la connexion",
"testSuccess": "Test du flux réussi!",
"testFailed": "Échec du test du flux",
"testFailedTitle": "Échec du test",
"connected": "Connecté",
"notConnected": "Non connecté",
"featuresTitle": "Fonctionnalités",
"go2rtc": "Réduire les connexions à la caméra",
"detectRoleWarning": "Au moins un flux doit avoir le rôle 'détection' pour continuer.",
"rolesPopover": {
"title": "Rôles du flux",
"detect": "Flux principal pour la détection d'objet",
"record": "Enregistre des segments du flux vidéo en fonction des paramètres de configuration",
"audio": "Flux pour la détection basée sur l'audio"
},
"featuresPopover": {
"title": "Fonctionnalités du flux",
"description": "Utiliser la rediffusion go2rtc pour réduire les connexions à votre caméra"
}
},
"step4": {
"description": "Validation et analyse finales avant d'enregistrer votre nouvelle caméra. Connectez chaque flux avant d'enregistrer.",
"validationTitle": "Validation du flux",
"connectAllStreams": "Connecter tous les flux",
"reconnectionSuccess": "Reconnexion réussie",
"reconnectionPartial": "Certains flux n'ont pas réussi à se reconnecter.",
"streamUnavailable": "Aperçu du flux non disponible",
"reload": "Recharger",
"connecting": "En cours de connexion...",
"streamTitle": "Flux {{number}}",
"valid": "Valide",
"failed": "Échec",
"notTested": "Non testé",
"connectStream": "Connecter",
"connectingStream": "En cours de connexion",
"disconnectStream": "Déconnecter",
"estimatedBandwidth": "Bande passante estimée",
"roles": "Rôles",
"ffmpegModule": "Utiliser le mode de compatibilité du flux",
"ffmpegModuleDescription": "Si le flux ne se charge pas après plusieurs tentatives, essayez d'activer cette option. Lorsqu'elle est activée, Frigate utilisera le module ffmpeg avec go2rtc. Cela peut offrir une meilleure compatibilité avec certains flux de caméra.",
"none": "Aucun",
"error": "Erreur",
"streamValidated": "Flux {{number}} validé avec succès",
"streamValidationFailed": "Échec de la validation du flux {{number}}",
"saveAndApply": "Enregistrer la nouvelle caméra",
"saveError": "Configuration invalide. Veuillez vérifier vos paramètres.",
"issues": {
"title": "Validation du flux",
"videoCodecGood": "Le codec vidéo est {{codec}}.",
"audioCodecGood": "Le codec audio est {{codec}}.",
"resolutionHigh": "Une résolution de {{resolution}} peut entraîner une utilisation accrue des ressources.",
"resolutionLow": "Une résolution de {{resolution}} peut être trop faible pour une détection fiable des petits objets.",
"noAudioWarning": "Aucun audio détecté pour ce flux, les enregistrements n'auront pas de son.",
"audioCodecRecordError": "Le codec audio AAC est requis pour prendre en charge l'audio dans les enregistrements.",
"audioCodecRequired": "Un flux audio est requis pour prendre en charge la détection audio.",
"restreamingWarning": "Réduire les connexions à la caméra pour le flux d'enregistrement peut légèrement augmenter l'utilisation du processeur.",
"brands": {
"reolink-rtsp": "Le RTSP Reolink n'est pas recommandé. Activez HTTP dans les paramètres du micrologiciel de la caméra et redémarrez l'assistant."
},
"dahua": {
"substreamWarning": "Le sous-flux 1 est limité à une basse résolution. De nombreuses caméras Dahua / Amcrest / EmpireTech prennent en charge des sous-flux supplémentaires qui doivent être activés dans les paramètres de la caméra. Il est recommandé de vérifier et d'utiliser ces flux s'ils sont disponibles."
},
"hikvision": {
"substreamWarning": "Le sous-flux 1 est limité à une basse résolution. De nombreuses caméras Hikvision prennent en charge des sous-flux supplémentaires qui doivent être activés dans les paramètres de la caméra. Il est recommandé de vérifier et d'utiliser ces flux s'ils sont disponibles."
}
}
"ffmpegModuleDescription": "Si le flux ne se charge pas après plusieurs tentatives, essayez d'activer cette option. Lorsqu'elle est activée, Frigate utilisera le module ffmpeg avec go2rtc. Cela peut offrir une meilleure compatibilité avec certains flux de caméra."
}
},
"cameraManagement": {

View File

@ -76,12 +76,7 @@
}
},
"npuUsage": "Utilisation NPU",
"npuMemory": "Mémoire NPU",
"intelGpuWarning": {
"title": "Avertissement relatif aux statistiques du GPU Intel",
"message": "Statistiques du GPU non disponibles",
"description": "Il s'agit d'un bug connu de l'outil de statistiques GPU d'Intel (intel_gpu_top) : il peut afficher à tort une utilisation de 0 %, même lorsque l'accélération matérielle et la détection d'objets fonctionnent correctement sur l'iGPU. Ce problème ne vient pas de Frigate. Vous pouvez redémarrer l'hôte pour rétablir temporairement l'affichage et confirmer le fonctionnement du GPU. Les performances ne sont pas affectées."
}
"npuMemory": "Mémoire NPU"
},
"otherProcesses": {
"title": "Autres processus",
@ -156,7 +151,7 @@
"copyToClipboard": "Données de la sonde copiées dans le presse-papiers"
},
"error": {
"unableToProbeCamera": "Impossible d'interroger la caméra : {{errorMessage}}"
"unableToProbeCamera": "Impossible d'inspecter la caméra : {{errorMessage}}"
}
}
},
@ -185,14 +180,7 @@
"yolov9_plate_detection": "Détection de plaques d'immatriculation YOLOv9",
"face_recognition": "Reconnaissance faciale",
"text_embedding": "Vitesse d'embedding de visage",
"yolov9_plate_detection_speed": "Vitesse de détection de plaques d'immatriculation YOLOv9",
"review_description": "Description de la revue",
"review_description_speed": "Vitesse de la description de la revue",
"review_description_events_per_second": "Description de la revue",
"object_description": "Description de l'objet",
"object_description_speed": "Vitesse de la description d'objet",
"object_description_events_per_second": "Description de l'objet"
},
"averageInf": "Temps d'inférence moyen"
"yolov9_plate_detection_speed": "Vitesse de détection de plaques d'immatriculation YOLOv9"
}
}
}

View File

@ -1,5 +1,5 @@
{
"filter": "Filter",
"filter": "Filtar",
"classes": {
"label": "Klase"
}

View File

@ -1,28 +1,5 @@
{
"description": {
"addFace": "Vodič za dodavanje nove kolekcije u Biblioteku lica."
},
"steps": {
"faceName": "Unesi Ime Lica",
"uploadFace": "Prenesi Sliku Lica",
"nextSteps": "Sljedeći Koraci",
"description": {
"uploadFace": "Prenesite sliku {{name}} koja prikazuje njezino lice iz prednjeg kuta. Slika ne mora biti obrezana samo na njezino lice."
}
},
"train": {
"title": "Nedavna Prepoznavanja",
"aria": "Odaberite nedavna prepoznavanja",
"empty": "Nema nedavnih pokušaja prepoznavanja lica"
},
"deleteFaceLibrary": {
"title": "Izbriši Ime",
"desc": "Jeste li sigurni da želite izbrisati kolekciju {{name}}? Ovim će se trajno izbrisati sva povezana lica."
},
"deleteFaceAttempts": {
"title": "Izbriši Lica",
"desc_one": "Jeste li sigurni da želite izbrisati {{count}} lice? Ova se radnja ne može poništiti.",
"desc_few": "Jeste li sigurni da želite izbrisati {{count}} lica? Ova se radnja ne može poništiti.",
"desc_other": "Jeste li sigurni da želite izbrisati {{count}} lica? Ova se radnja ne može poništiti."
}
}

View File

@ -1,4 +1,4 @@
{
"filter": "Filter",
"filter": "Filtar",
"export": "Izvoz"
}

View File

@ -4,8 +4,7 @@
"deleteClassificationAttempts": "Osztályozási képek törlése",
"deleteImages": "Képek törlése",
"trainModel": "Modell betanítása",
"deleteModels": "Modellek törlése",
"editModel": "Modell szerkesztése"
"deleteModels": "Modellek törlése"
},
"toast": {
"success": {

View File

@ -399,7 +399,7 @@
"mechanical_fan": "Ventilatore meccanico",
"air_conditioning": "Aria condizionata",
"cash_register": "Registratore di cassa",
"single-lens_reflex_camera": "Telecamera reflex a obiettivo singolo",
"single-lens_reflex_camera": "Fotocamera reflex a obiettivo singolo",
"tools": "Utensili",
"jackhammer": "Martello pneumatico",
"sawing": "Segare",

View File

@ -87,10 +87,7 @@
"formattedTimestampMonthDayYear": {
"12hour": "d MMM, yyyy",
"24hour": "d MMM, yyyy"
},
"inProgress": "In corso",
"invalidStartTime": "Ora di inizio non valida",
"invalidEndTime": "Ora di fine non valida"
}
},
"button": {
"cancel": "Annulla",
@ -127,8 +124,7 @@
"back": "Indietro",
"pictureInPicture": "Immagine nell'immagine",
"twoWayTalk": "Comunicazione bidirezionale",
"cameraAudio": "Audio della telecamera",
"continue": "Continua"
"cameraAudio": "Audio della telecamera"
},
"unit": {
"speed": {
@ -152,9 +148,7 @@
"back": "Vai indietro",
"hide": "Nascondi {{item}}",
"show": "Mostra {{item}}",
"ID": "ID",
"none": "Nessuna",
"all": "Tutte"
"ID": "ID"
},
"menu": {
"configuration": "Configurazione",
@ -256,8 +250,7 @@
"setPassword": "Imposta password"
},
"withSystem": "Sistema",
"faceLibrary": "Raccolta volti",
"classification": "Classificazione"
"faceLibrary": "Raccolta volti"
},
"pagination": {
"next": {

Some files were not shown because too many files have changed in this diff Show More